about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/tests
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/tests')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_asteroidal.py23
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_boundary.py154
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_bridges.py144
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_broadcasting.py82
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_chains.py141
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_chordal.py129
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_clique.py291
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cluster.py549
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_communicability.py80
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_core.py266
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_covering.py85
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cuts.py171
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cycles.py974
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_d_separation.py348
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dag.py835
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_measures.py774
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_regular.py85
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominance.py286
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominating.py46
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_efficiency.py58
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_euler.py314
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py686
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graphical.py163
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_hierarchy.py46
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_hybrid.py24
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_isolate.py26
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_link_prediction.py586
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py427
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_matching.py605
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_max_weight_clique.py179
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_mis.py62
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_moral.py15
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_node_classification.py140
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_non_randomness.py42
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_planar_drawing.py274
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_planarity.py535
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_polynomials.py57
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_reciprocity.py37
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_regular.py92
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_richclub.py149
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_similarity.py946
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_simple_paths.py803
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_smallworld.py78
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_smetric.py8
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_sparsifiers.py138
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_structuralholes.py137
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_summarization.py642
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_swap.py179
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_threshold.py269
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_time_dependent.py431
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_tournament.py163
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_triads.py289
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_vitality.py41
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_voronoi.py103
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_walks.py54
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_wiener.py123
57 files changed, 14384 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_asteroidal.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_asteroidal.py
new file mode 100644
index 00000000..67131b2d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_asteroidal.py
@@ -0,0 +1,23 @@
+import networkx as nx
+
+
+def test_is_at_free():
+    is_at_free = nx.asteroidal.is_at_free
+
+    cycle = nx.cycle_graph(6)
+    assert not is_at_free(cycle)
+
+    path = nx.path_graph(6)
+    assert is_at_free(path)
+
+    small_graph = nx.complete_graph(2)
+    assert is_at_free(small_graph)
+
+    petersen = nx.petersen_graph()
+    assert not is_at_free(petersen)
+
+    clique = nx.complete_graph(6)
+    assert is_at_free(clique)
+
+    line_clique = nx.line_graph(clique)
+    assert not is_at_free(line_clique)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_boundary.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_boundary.py
new file mode 100644
index 00000000..856be465
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_boundary.py
@@ -0,0 +1,154 @@
+"""Unit tests for the :mod:`networkx.algorithms.boundary` module."""
+
+from itertools import combinations
+
+import pytest
+
+import networkx as nx
+from networkx import convert_node_labels_to_integers as cnlti
+from networkx.utils import edges_equal
+
+
+class TestNodeBoundary:
+    """Unit tests for the :func:`~networkx.node_boundary` function."""
+
+    def test_null_graph(self):
+        """Tests that the null graph has empty node boundaries."""
+        null = nx.null_graph()
+        assert nx.node_boundary(null, []) == set()
+        assert nx.node_boundary(null, [], []) == set()
+        assert nx.node_boundary(null, [1, 2, 3]) == set()
+        assert nx.node_boundary(null, [1, 2, 3], [4, 5, 6]) == set()
+        assert nx.node_boundary(null, [1, 2, 3], [3, 4, 5]) == set()
+
+    def test_path_graph(self):
+        P10 = cnlti(nx.path_graph(10), first_label=1)
+        assert nx.node_boundary(P10, []) == set()
+        assert nx.node_boundary(P10, [], []) == set()
+        assert nx.node_boundary(P10, [1, 2, 3]) == {4}
+        assert nx.node_boundary(P10, [4, 5, 6]) == {3, 7}
+        assert nx.node_boundary(P10, [3, 4, 5, 6, 7]) == {2, 8}
+        assert nx.node_boundary(P10, [8, 9, 10]) == {7}
+        assert nx.node_boundary(P10, [4, 5, 6], [9, 10]) == set()
+
+    def test_complete_graph(self):
+        K10 = cnlti(nx.complete_graph(10), first_label=1)
+        assert nx.node_boundary(K10, []) == set()
+        assert nx.node_boundary(K10, [], []) == set()
+        assert nx.node_boundary(K10, [1, 2, 3]) == {4, 5, 6, 7, 8, 9, 10}
+        assert nx.node_boundary(K10, [4, 5, 6]) == {1, 2, 3, 7, 8, 9, 10}
+        assert nx.node_boundary(K10, [3, 4, 5, 6, 7]) == {1, 2, 8, 9, 10}
+        assert nx.node_boundary(K10, [4, 5, 6], []) == set()
+        assert nx.node_boundary(K10, K10) == set()
+        assert nx.node_boundary(K10, [1, 2, 3], [3, 4, 5]) == {4, 5}
+
+    def test_petersen(self):
+        """Check boundaries in the petersen graph
+
+        cheeger(G,k)=min(|bdy(S)|/|S| for |S|=k, 0<k<=|V(G)|/2)
+
+        """
+
+        def cheeger(G, k):
+            return min(len(nx.node_boundary(G, nn)) / k for nn in combinations(G, k))
+
+        P = nx.petersen_graph()
+        assert cheeger(P, 1) == pytest.approx(3.00, abs=1e-2)
+        assert cheeger(P, 2) == pytest.approx(2.00, abs=1e-2)
+        assert cheeger(P, 3) == pytest.approx(1.67, abs=1e-2)
+        assert cheeger(P, 4) == pytest.approx(1.00, abs=1e-2)
+        assert cheeger(P, 5) == pytest.approx(0.80, abs=1e-2)
+
+    def test_directed(self):
+        """Tests the node boundary of a directed graph."""
+        G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)])
+        S = {0, 1}
+        boundary = nx.node_boundary(G, S)
+        expected = {2}
+        assert boundary == expected
+
+    def test_multigraph(self):
+        """Tests the node boundary of a multigraph."""
+        G = nx.MultiGraph(list(nx.cycle_graph(5).edges()) * 2)
+        S = {0, 1}
+        boundary = nx.node_boundary(G, S)
+        expected = {2, 4}
+        assert boundary == expected
+
+    def test_multidigraph(self):
+        """Tests the edge boundary of a multidigraph."""
+        edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]
+        G = nx.MultiDiGraph(edges * 2)
+        S = {0, 1}
+        boundary = nx.node_boundary(G, S)
+        expected = {2}
+        assert boundary == expected
+
+
+class TestEdgeBoundary:
+    """Unit tests for the :func:`~networkx.edge_boundary` function."""
+
+    def test_null_graph(self):
+        null = nx.null_graph()
+        assert list(nx.edge_boundary(null, [])) == []
+        assert list(nx.edge_boundary(null, [], [])) == []
+        assert list(nx.edge_boundary(null, [1, 2, 3])) == []
+        assert list(nx.edge_boundary(null, [1, 2, 3], [4, 5, 6])) == []
+        assert list(nx.edge_boundary(null, [1, 2, 3], [3, 4, 5])) == []
+
+    def test_path_graph(self):
+        P10 = cnlti(nx.path_graph(10), first_label=1)
+        assert list(nx.edge_boundary(P10, [])) == []
+        assert list(nx.edge_boundary(P10, [], [])) == []
+        assert list(nx.edge_boundary(P10, [1, 2, 3])) == [(3, 4)]
+        assert sorted(nx.edge_boundary(P10, [4, 5, 6])) == [(4, 3), (6, 7)]
+        assert sorted(nx.edge_boundary(P10, [3, 4, 5, 6, 7])) == [(3, 2), (7, 8)]
+        assert list(nx.edge_boundary(P10, [8, 9, 10])) == [(8, 7)]
+        assert sorted(nx.edge_boundary(P10, [4, 5, 6], [9, 10])) == []
+        assert list(nx.edge_boundary(P10, [1, 2, 3], [3, 4, 5])) == [(2, 3), (3, 4)]
+
+    def test_complete_graph(self):
+        K10 = cnlti(nx.complete_graph(10), first_label=1)
+
+        def ilen(iterable):
+            return sum(1 for i in iterable)
+
+        assert list(nx.edge_boundary(K10, [])) == []
+        assert list(nx.edge_boundary(K10, [], [])) == []
+        assert ilen(nx.edge_boundary(K10, [1, 2, 3])) == 21
+        assert ilen(nx.edge_boundary(K10, [4, 5, 6, 7])) == 24
+        assert ilen(nx.edge_boundary(K10, [3, 4, 5, 6, 7])) == 25
+        assert ilen(nx.edge_boundary(K10, [8, 9, 10])) == 21
+        assert edges_equal(
+            nx.edge_boundary(K10, [4, 5, 6], [9, 10]),
+            [(4, 9), (4, 10), (5, 9), (5, 10), (6, 9), (6, 10)],
+        )
+        assert edges_equal(
+            nx.edge_boundary(K10, [1, 2, 3], [3, 4, 5]),
+            [(1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5)],
+        )
+
+    def test_directed(self):
+        """Tests the edge boundary of a directed graph."""
+        G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)])
+        S = {0, 1}
+        boundary = list(nx.edge_boundary(G, S))
+        expected = [(1, 2)]
+        assert boundary == expected
+
+    def test_multigraph(self):
+        """Tests the edge boundary of a multigraph."""
+        G = nx.MultiGraph(list(nx.cycle_graph(5).edges()) * 2)
+        S = {0, 1}
+        boundary = list(nx.edge_boundary(G, S))
+        expected = [(0, 4), (0, 4), (1, 2), (1, 2)]
+        assert boundary == expected
+
+    def test_multidigraph(self):
+        """Tests the edge boundary of a multidigraph."""
+        edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]
+        G = nx.MultiDiGraph(edges * 2)
+        S = {0, 1}
+        boundary = list(nx.edge_boundary(G, S))
+        expected = [(1, 2), (1, 2)]
+        assert boundary == expected
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_bridges.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_bridges.py
new file mode 100644
index 00000000..b47f5860
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_bridges.py
@@ -0,0 +1,144 @@
+"""Unit tests for bridge-finding algorithms."""
+
+import pytest
+
+import networkx as nx
+
+
+class TestBridges:
+    """Unit tests for the bridge-finding function."""
+
+    def test_single_bridge(self):
+        edges = [
+            # DFS tree edges.
+            (1, 2),
+            (2, 3),
+            (3, 4),
+            (3, 5),
+            (5, 6),
+            (6, 7),
+            (7, 8),
+            (5, 9),
+            (9, 10),
+            # Nontree edges.
+            (1, 3),
+            (1, 4),
+            (2, 5),
+            (5, 10),
+            (6, 8),
+        ]
+        G = nx.Graph(edges)
+        source = 1
+        bridges = list(nx.bridges(G, source))
+        assert bridges == [(5, 6)]
+
+    def test_barbell_graph(self):
+        # The (3, 0) barbell graph has two triangles joined by a single edge.
+        G = nx.barbell_graph(3, 0)
+        source = 0
+        bridges = list(nx.bridges(G, source))
+        assert bridges == [(2, 3)]
+
+    def test_multiedge_bridge(self):
+        edges = [
+            (0, 1),
+            (0, 2),
+            (1, 2),
+            (1, 2),
+            (2, 3),
+            (3, 4),
+            (3, 4),
+        ]
+        G = nx.MultiGraph(edges)
+        assert list(nx.bridges(G)) == [(2, 3)]
+
+
+class TestHasBridges:
+    """Unit tests for the has bridges function."""
+
+    def test_single_bridge(self):
+        edges = [
+            # DFS tree edges.
+            (1, 2),
+            (2, 3),
+            (3, 4),
+            (3, 5),
+            (5, 6),  # The only bridge edge
+            (6, 7),
+            (7, 8),
+            (5, 9),
+            (9, 10),
+            # Nontree edges.
+            (1, 3),
+            (1, 4),
+            (2, 5),
+            (5, 10),
+            (6, 8),
+        ]
+        G = nx.Graph(edges)
+        assert nx.has_bridges(G)  # Default root
+        assert nx.has_bridges(G, root=1)  # arbitrary root in G
+
+    def test_has_bridges_raises_root_not_in_G(self):
+        G = nx.Graph()
+        G.add_nodes_from([1, 2, 3])
+        with pytest.raises(nx.NodeNotFound):
+            nx.has_bridges(G, root=6)
+
+    def test_multiedge_bridge(self):
+        edges = [
+            (0, 1),
+            (0, 2),
+            (1, 2),
+            (1, 2),
+            (2, 3),
+            (3, 4),
+            (3, 4),
+        ]
+        G = nx.MultiGraph(edges)
+        assert nx.has_bridges(G)
+        # Make every edge a multiedge
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert not nx.has_bridges(G)
+
+    def test_bridges_multiple_components(self):
+        G = nx.Graph()
+        nx.add_path(G, [0, 1, 2])  # One connected component
+        nx.add_path(G, [4, 5, 6])  # Another connected component
+        assert list(nx.bridges(G, root=4)) == [(4, 5), (5, 6)]
+
+
+class TestLocalBridges:
+    """Unit tests for the local_bridge function."""
+
+    @classmethod
+    def setup_class(cls):
+        cls.BB = nx.barbell_graph(4, 0)
+        cls.square = nx.cycle_graph(4)
+        cls.tri = nx.cycle_graph(3)
+
+    def test_nospan(self):
+        expected = {(3, 4), (4, 3)}
+        assert next(nx.local_bridges(self.BB, with_span=False)) in expected
+        assert set(nx.local_bridges(self.square, with_span=False)) == self.square.edges
+        assert list(nx.local_bridges(self.tri, with_span=False)) == []
+
+    def test_no_weight(self):
+        inf = float("inf")
+        expected = {(3, 4, inf), (4, 3, inf)}
+        assert next(nx.local_bridges(self.BB)) in expected
+        expected = {(u, v, 3) for u, v in self.square.edges}
+        assert set(nx.local_bridges(self.square)) == expected
+        assert list(nx.local_bridges(self.tri)) == []
+
+    def test_weight(self):
+        inf = float("inf")
+        G = self.square.copy()
+
+        G.edges[1, 2]["weight"] = 2
+        expected = {(u, v, 5 - wt) for u, v, wt in G.edges(data="weight", default=1)}
+        assert set(nx.local_bridges(G, weight="weight")) == expected
+
+        expected = {(u, v, 6) for u, v in G.edges}
+        lb = nx.local_bridges(G, weight=lambda u, v, d: 2)
+        assert set(lb) == expected
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_broadcasting.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_broadcasting.py
new file mode 100644
index 00000000..73bf83c8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_broadcasting.py
@@ -0,0 +1,82 @@
+"""Unit tests for the broadcasting module."""
+
+import math
+
+import networkx as nx
+
+
+def test_example_tree_broadcast():
+    """
+    Test the BROADCAST algorithm on the example in the paper titled: "Information Dissemination in Trees"
+    """
+    edge_list = [
+        (0, 1),
+        (1, 2),
+        (2, 7),
+        (3, 4),
+        (5, 4),
+        (4, 7),
+        (6, 7),
+        (7, 9),
+        (8, 9),
+        (9, 13),
+        (13, 14),
+        (14, 15),
+        (14, 16),
+        (14, 17),
+        (13, 11),
+        (11, 10),
+        (11, 12),
+        (13, 18),
+        (18, 19),
+        (18, 20),
+    ]
+    G = nx.Graph(edge_list)
+    b_T, b_C = nx.tree_broadcast_center(G)
+    assert b_T == 6
+    assert b_C == {13, 9}
+    # test broadcast time from specific vertex
+    assert nx.tree_broadcast_time(G, 17) == 8
+    assert nx.tree_broadcast_time(G, 3) == 9
+    # test broadcast time of entire tree
+    assert nx.tree_broadcast_time(G) == 10
+
+
+def test_path_broadcast():
+    for i in range(2, 12):
+        G = nx.path_graph(i)
+        b_T, b_C = nx.tree_broadcast_center(G)
+        assert b_T == math.ceil(i / 2)
+        assert b_C == {
+            math.ceil(i / 2),
+            math.floor(i / 2),
+            math.ceil(i / 2 - 1),
+            math.floor(i / 2 - 1),
+        }
+        assert nx.tree_broadcast_time(G) == i - 1
+
+
+def test_empty_graph_broadcast():
+    H = nx.empty_graph(1)
+    b_T, b_C = nx.tree_broadcast_center(H)
+    assert b_T == 0
+    assert b_C == {0}
+    assert nx.tree_broadcast_time(H) == 0
+
+
+def test_star_broadcast():
+    for i in range(4, 12):
+        G = nx.star_graph(i)
+        b_T, b_C = nx.tree_broadcast_center(G)
+        assert b_T == i
+        assert b_C == set(G.nodes())
+        assert nx.tree_broadcast_time(G) == b_T
+
+
+def test_binomial_tree_broadcast():
+    for i in range(2, 8):
+        G = nx.binomial_tree(i)
+        b_T, b_C = nx.tree_broadcast_center(G)
+        assert b_T == i
+        assert b_C == {0, 2 ** (i - 1)}
+        assert nx.tree_broadcast_time(G) == 2 * i - 1
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_chains.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_chains.py
new file mode 100644
index 00000000..09b4c734
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_chains.py
@@ -0,0 +1,141 @@
+"""Unit tests for the chain decomposition functions."""
+
+from itertools import cycle, islice
+
+import pytest
+
+import networkx as nx
+
+
+def cycles(seq):
+    """Yields cyclic permutations of the given sequence.
+
+    For example::
+
+        >>> list(cycles("abc"))
+        [('a', 'b', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b')]
+
+    """
+    n = len(seq)
+    cycled_seq = cycle(seq)
+    for x in seq:
+        yield tuple(islice(cycled_seq, n))
+        next(cycled_seq)
+
+
+def cyclic_equals(seq1, seq2):
+    """Decide whether two sequences are equal up to cyclic permutations.
+
+    For example::
+
+        >>> cyclic_equals("xyz", "zxy")
+        True
+        >>> cyclic_equals("xyz", "zyx")
+        False
+
+    """
+    # Cast seq2 to a tuple since `cycles()` yields tuples.
+    seq2 = tuple(seq2)
+    return any(x == tuple(seq2) for x in cycles(seq1))
+
+
+class TestChainDecomposition:
+    """Unit tests for the chain decomposition function."""
+
+    def assertContainsChain(self, chain, expected):
+        # A cycle could be expressed in two different orientations, one
+        # forward and one backward, so we need to check for cyclic
+        # equality in both orientations.
+        reversed_chain = list(reversed([tuple(reversed(e)) for e in chain]))
+        for candidate in expected:
+            if cyclic_equals(chain, candidate):
+                break
+            if cyclic_equals(reversed_chain, candidate):
+                break
+        else:
+            self.fail("chain not found")
+
+    def test_decomposition(self):
+        edges = [
+            # DFS tree edges.
+            (1, 2),
+            (2, 3),
+            (3, 4),
+            (3, 5),
+            (5, 6),
+            (6, 7),
+            (7, 8),
+            (5, 9),
+            (9, 10),
+            # Nontree edges.
+            (1, 3),
+            (1, 4),
+            (2, 5),
+            (5, 10),
+            (6, 8),
+        ]
+        G = nx.Graph(edges)
+        expected = [
+            [(1, 3), (3, 2), (2, 1)],
+            [(1, 4), (4, 3)],
+            [(2, 5), (5, 3)],
+            [(5, 10), (10, 9), (9, 5)],
+            [(6, 8), (8, 7), (7, 6)],
+        ]
+        chains = list(nx.chain_decomposition(G, root=1))
+        assert len(chains) == len(expected)
+
+    # This chain decomposition isn't unique
+    #        for chain in chains:
+    #            print(chain)
+    #            self.assertContainsChain(chain, expected)
+
+    def test_barbell_graph(self):
+        # The (3, 0) barbell graph has two triangles joined by a single edge.
+        G = nx.barbell_graph(3, 0)
+        chains = list(nx.chain_decomposition(G, root=0))
+        expected = [[(0, 1), (1, 2), (2, 0)], [(3, 4), (4, 5), (5, 3)]]
+        assert len(chains) == len(expected)
+        for chain in chains:
+            self.assertContainsChain(chain, expected)
+
+    def test_disconnected_graph(self):
+        """Test for a graph with multiple connected components."""
+        G = nx.barbell_graph(3, 0)
+        H = nx.barbell_graph(3, 0)
+        mapping = dict(zip(range(6), "abcdef"))
+        nx.relabel_nodes(H, mapping, copy=False)
+        G = nx.union(G, H)
+        chains = list(nx.chain_decomposition(G))
+        expected = [
+            [(0, 1), (1, 2), (2, 0)],
+            [(3, 4), (4, 5), (5, 3)],
+            [("a", "b"), ("b", "c"), ("c", "a")],
+            [("d", "e"), ("e", "f"), ("f", "d")],
+        ]
+        assert len(chains) == len(expected)
+        for chain in chains:
+            self.assertContainsChain(chain, expected)
+
+    def test_disconnected_graph_root_node(self):
+        """Test for a single component of a disconnected graph."""
+        G = nx.barbell_graph(3, 0)
+        H = nx.barbell_graph(3, 0)
+        mapping = dict(zip(range(6), "abcdef"))
+        nx.relabel_nodes(H, mapping, copy=False)
+        G = nx.union(G, H)
+        chains = list(nx.chain_decomposition(G, root="a"))
+        expected = [
+            [("a", "b"), ("b", "c"), ("c", "a")],
+            [("d", "e"), ("e", "f"), ("f", "d")],
+        ]
+        assert len(chains) == len(expected)
+        for chain in chains:
+            self.assertContainsChain(chain, expected)
+
+    def test_chain_decomposition_root_not_in_G(self):
+        """Test chain decomposition when root is not in graph"""
+        G = nx.Graph()
+        G.add_nodes_from([1, 2, 3])
+        with pytest.raises(nx.NodeNotFound):
+            nx.has_bridges(G, root=6)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_chordal.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_chordal.py
new file mode 100644
index 00000000..148b22f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_chordal.py
@@ -0,0 +1,129 @@
+import pytest
+
+import networkx as nx
+
+
+class TestMCS:
+    @classmethod
+    def setup_class(cls):
+        # simple graph
+        connected_chordal_G = nx.Graph()
+        connected_chordal_G.add_edges_from(
+            [
+                (1, 2),
+                (1, 3),
+                (2, 3),
+                (2, 4),
+                (3, 4),
+                (3, 5),
+                (3, 6),
+                (4, 5),
+                (4, 6),
+                (5, 6),
+            ]
+        )
+        cls.connected_chordal_G = connected_chordal_G
+
+        chordal_G = nx.Graph()
+        chordal_G.add_edges_from(
+            [
+                (1, 2),
+                (1, 3),
+                (2, 3),
+                (2, 4),
+                (3, 4),
+                (3, 5),
+                (3, 6),
+                (4, 5),
+                (4, 6),
+                (5, 6),
+                (7, 8),
+            ]
+        )
+        chordal_G.add_node(9)
+        cls.chordal_G = chordal_G
+
+        non_chordal_G = nx.Graph()
+        non_chordal_G.add_edges_from([(1, 2), (1, 3), (2, 4), (2, 5), (3, 4), (3, 5)])
+        cls.non_chordal_G = non_chordal_G
+
+        self_loop_G = nx.Graph()
+        self_loop_G.add_edges_from([(1, 1)])
+        cls.self_loop_G = self_loop_G
+
+    @pytest.mark.parametrize("G", (nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()))
+    def test_is_chordal_not_implemented(self, G):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            nx.is_chordal(G)
+
+    def test_is_chordal(self):
+        assert not nx.is_chordal(self.non_chordal_G)
+        assert nx.is_chordal(self.chordal_G)
+        assert nx.is_chordal(self.connected_chordal_G)
+        assert nx.is_chordal(nx.Graph())
+        assert nx.is_chordal(nx.complete_graph(3))
+        assert nx.is_chordal(nx.cycle_graph(3))
+        assert not nx.is_chordal(nx.cycle_graph(5))
+        assert nx.is_chordal(self.self_loop_G)
+
+    def test_induced_nodes(self):
+        G = nx.generators.classic.path_graph(10)
+        Induced_nodes = nx.find_induced_nodes(G, 1, 9, 2)
+        assert Induced_nodes == {1, 2, 3, 4, 5, 6, 7, 8, 9}
+        pytest.raises(
+            nx.NetworkXTreewidthBoundExceeded, nx.find_induced_nodes, G, 1, 9, 1
+        )
+        Induced_nodes = nx.find_induced_nodes(self.chordal_G, 1, 6)
+        assert Induced_nodes == {1, 2, 4, 6}
+        pytest.raises(nx.NetworkXError, nx.find_induced_nodes, self.non_chordal_G, 1, 5)
+
+    def test_graph_treewidth(self):
+        with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"):
+            nx.chordal_graph_treewidth(self.non_chordal_G)
+
+    def test_chordal_find_cliques(self):
+        cliques = {
+            frozenset([9]),
+            frozenset([7, 8]),
+            frozenset([1, 2, 3]),
+            frozenset([2, 3, 4]),
+            frozenset([3, 4, 5, 6]),
+        }
+        assert set(nx.chordal_graph_cliques(self.chordal_G)) == cliques
+        with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"):
+            set(nx.chordal_graph_cliques(self.non_chordal_G))
+        with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"):
+            set(nx.chordal_graph_cliques(self.self_loop_G))
+
+    def test_chordal_find_cliques_path(self):
+        G = nx.path_graph(10)
+        cliqueset = nx.chordal_graph_cliques(G)
+        for u, v in G.edges():
+            assert frozenset([u, v]) in cliqueset or frozenset([v, u]) in cliqueset
+
+    def test_chordal_find_cliquesCC(self):
+        cliques = {frozenset([1, 2, 3]), frozenset([2, 3, 4]), frozenset([3, 4, 5, 6])}
+        cgc = nx.chordal_graph_cliques
+        assert set(cgc(self.connected_chordal_G)) == cliques
+
+    def test_complete_to_chordal_graph(self):
+        fgrg = nx.fast_gnp_random_graph
+        test_graphs = [
+            nx.barbell_graph(6, 2),
+            nx.cycle_graph(15),
+            nx.wheel_graph(20),
+            nx.grid_graph([10, 4]),
+            nx.ladder_graph(15),
+            nx.star_graph(5),
+            nx.bull_graph(),
+            fgrg(20, 0.3, seed=1),
+        ]
+        for G in test_graphs:
+            H, a = nx.complete_to_chordal_graph(G)
+            assert nx.is_chordal(H)
+            assert len(a) == H.number_of_nodes()
+            if nx.is_chordal(G):
+                assert G.number_of_edges() == H.number_of_edges()
+                assert set(a.values()) == {0}
+            else:
+                assert len(set(a.values())) == H.number_of_nodes()
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_clique.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_clique.py
new file mode 100644
index 00000000..3bee2109
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_clique.py
@@ -0,0 +1,291 @@
+import pytest
+
+import networkx as nx
+from networkx import convert_node_labels_to_integers as cnlti
+
+
+class TestCliques:
+    def setup_method(self):
+        z = [3, 4, 3, 4, 2, 4, 2, 1, 1, 1, 1]
+        self.G = cnlti(nx.generators.havel_hakimi_graph(z), first_label=1)
+        self.cl = list(nx.find_cliques(self.G))
+        H = nx.complete_graph(6)
+        H = nx.relabel_nodes(H, {i: i + 1 for i in range(6)})
+        H.remove_edges_from([(2, 6), (2, 5), (2, 4), (1, 3), (5, 3)])
+        self.H = H
+
+    def test_find_cliques1(self):
+        cl = list(nx.find_cliques(self.G))
+        rcl = nx.find_cliques_recursive(self.G)
+        expected = [[2, 6, 1, 3], [2, 6, 4], [5, 4, 7], [8, 9], [10, 11]]
+        assert sorted(map(sorted, cl)) == sorted(map(sorted, rcl))
+        assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
+
+    def test_selfloops(self):
+        self.G.add_edge(1, 1)
+        cl = list(nx.find_cliques(self.G))
+        rcl = list(nx.find_cliques_recursive(self.G))
+        assert set(map(frozenset, cl)) == set(map(frozenset, rcl))
+        answer = [{2, 6, 1, 3}, {2, 6, 4}, {5, 4, 7}, {8, 9}, {10, 11}]
+        assert len(answer) == len(cl)
+        assert all(set(c) in answer for c in cl)
+
+    def test_find_cliques2(self):
+        hcl = list(nx.find_cliques(self.H))
+        assert sorted(map(sorted, hcl)) == [[1, 2], [1, 4, 5, 6], [2, 3], [3, 4, 6]]
+
+    def test_find_cliques3(self):
+        # all cliques are [[2, 6, 1, 3], [2, 6, 4], [5, 4, 7], [8, 9], [10, 11]]
+
+        cl = list(nx.find_cliques(self.G, [2]))
+        rcl = nx.find_cliques_recursive(self.G, [2])
+        expected = [[2, 6, 1, 3], [2, 6, 4]]
+        assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected))
+        assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
+
+        cl = list(nx.find_cliques(self.G, [2, 3]))
+        rcl = nx.find_cliques_recursive(self.G, [2, 3])
+        expected = [[2, 6, 1, 3]]
+        assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected))
+        assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
+
+        cl = list(nx.find_cliques(self.G, [2, 6, 4]))
+        rcl = nx.find_cliques_recursive(self.G, [2, 6, 4])
+        expected = [[2, 6, 4]]
+        assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected))
+        assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
+
+        cl = list(nx.find_cliques(self.G, [2, 6, 4]))
+        rcl = nx.find_cliques_recursive(self.G, [2, 6, 4])
+        expected = [[2, 6, 4]]
+        assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected))
+        assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
+
+        with pytest.raises(ValueError):
+            list(nx.find_cliques(self.G, [2, 6, 4, 1]))
+
+        with pytest.raises(ValueError):
+            list(nx.find_cliques_recursive(self.G, [2, 6, 4, 1]))
+
+    def test_number_of_cliques(self):
+        G = self.G
+        assert nx.number_of_cliques(G, 1) == 1
+        assert list(nx.number_of_cliques(G, [1]).values()) == [1]
+        assert list(nx.number_of_cliques(G, [1, 2]).values()) == [1, 2]
+        assert nx.number_of_cliques(G, [1, 2]) == {1: 1, 2: 2}
+        assert nx.number_of_cliques(G, 2) == 2
+        assert nx.number_of_cliques(G) == {
+            1: 1,
+            2: 2,
+            3: 1,
+            4: 2,
+            5: 1,
+            6: 2,
+            7: 1,
+            8: 1,
+            9: 1,
+            10: 1,
+            11: 1,
+        }
+        assert nx.number_of_cliques(G, nodes=list(G)) == {
+            1: 1,
+            2: 2,
+            3: 1,
+            4: 2,
+            5: 1,
+            6: 2,
+            7: 1,
+            8: 1,
+            9: 1,
+            10: 1,
+            11: 1,
+        }
+        assert nx.number_of_cliques(G, nodes=[2, 3, 4]) == {2: 2, 3: 1, 4: 2}
+        assert nx.number_of_cliques(G, cliques=self.cl) == {
+            1: 1,
+            2: 2,
+            3: 1,
+            4: 2,
+            5: 1,
+            6: 2,
+            7: 1,
+            8: 1,
+            9: 1,
+            10: 1,
+            11: 1,
+        }
+        assert nx.number_of_cliques(G, list(G), cliques=self.cl) == {
+            1: 1,
+            2: 2,
+            3: 1,
+            4: 2,
+            5: 1,
+            6: 2,
+            7: 1,
+            8: 1,
+            9: 1,
+            10: 1,
+            11: 1,
+        }
+
+    def test_node_clique_number(self):
+        G = self.G
+        assert nx.node_clique_number(G, 1) == 4
+        assert list(nx.node_clique_number(G, [1]).values()) == [4]
+        assert list(nx.node_clique_number(G, [1, 2]).values()) == [4, 4]
+        assert nx.node_clique_number(G, [1, 2]) == {1: 4, 2: 4}
+        assert nx.node_clique_number(G, 1) == 4
+        assert nx.node_clique_number(G) == {
+            1: 4,
+            2: 4,
+            3: 4,
+            4: 3,
+            5: 3,
+            6: 4,
+            7: 3,
+            8: 2,
+            9: 2,
+            10: 2,
+            11: 2,
+        }
+        assert nx.node_clique_number(G, cliques=self.cl) == {
+            1: 4,
+            2: 4,
+            3: 4,
+            4: 3,
+            5: 3,
+            6: 4,
+            7: 3,
+            8: 2,
+            9: 2,
+            10: 2,
+            11: 2,
+        }
+        assert nx.node_clique_number(G, [1, 2], cliques=self.cl) == {1: 4, 2: 4}
+        assert nx.node_clique_number(G, 1, cliques=self.cl) == 4
+
+    def test_make_clique_bipartite(self):
+        G = self.G
+        B = nx.make_clique_bipartite(G)
+        assert sorted(B) == [-5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+        # Project onto the nodes of the original graph.
+        H = nx.projected_graph(B, range(1, 12))
+        assert H.adj == G.adj
+        # Project onto the nodes representing the cliques.
+        H1 = nx.projected_graph(B, range(-5, 0))
+        # Relabel the negative numbers as positive ones.
+        H1 = nx.relabel_nodes(H1, {-v: v for v in range(1, 6)})
+        assert sorted(H1) == [1, 2, 3, 4, 5]
+
+    def test_make_max_clique_graph(self):
+        """Tests that the maximal clique graph is the same as the bipartite
+        clique graph after being projected onto the nodes representing the
+        cliques.
+
+        """
+        G = self.G
+        B = nx.make_clique_bipartite(G)
+        # Project onto the nodes representing the cliques.
+        H1 = nx.projected_graph(B, range(-5, 0))
+        # Relabel the negative numbers as nonnegative ones, starting at
+        # 0.
+        H1 = nx.relabel_nodes(H1, {-v: v - 1 for v in range(1, 6)})
+        H2 = nx.make_max_clique_graph(G)
+        assert H1.adj == H2.adj
+
+    def test_directed(self):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            next(nx.find_cliques(nx.DiGraph()))
+
+    def test_find_cliques_trivial(self):
+        G = nx.Graph()
+        assert sorted(nx.find_cliques(G)) == []
+        assert sorted(nx.find_cliques_recursive(G)) == []
+
+    def test_make_max_clique_graph_create_using(self):
+        G = nx.Graph([(1, 2), (3, 1), (4, 1), (5, 6)])
+        E = nx.Graph([(0, 1), (0, 2), (1, 2)])
+        E.add_node(3)
+        assert nx.is_isomorphic(nx.make_max_clique_graph(G, create_using=nx.Graph), E)
+
+
+class TestEnumerateAllCliques:
+    def test_paper_figure_4(self):
+        # Same graph as given in Fig. 4 of paper enumerate_all_cliques is
+        # based on.
+        # http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=1559964&isnumber=33129
+        G = nx.Graph()
+        edges_fig_4 = [
+            ("a", "b"),
+            ("a", "c"),
+            ("a", "d"),
+            ("a", "e"),
+            ("b", "c"),
+            ("b", "d"),
+            ("b", "e"),
+            ("c", "d"),
+            ("c", "e"),
+            ("d", "e"),
+            ("f", "b"),
+            ("f", "c"),
+            ("f", "g"),
+            ("g", "f"),
+            ("g", "c"),
+            ("g", "d"),
+            ("g", "e"),
+        ]
+        G.add_edges_from(edges_fig_4)
+
+        cliques = list(nx.enumerate_all_cliques(G))
+        clique_sizes = list(map(len, cliques))
+        assert sorted(clique_sizes) == clique_sizes
+
+        expected_cliques = [
+            ["a"],
+            ["b"],
+            ["c"],
+            ["d"],
+            ["e"],
+            ["f"],
+            ["g"],
+            ["a", "b"],
+            ["a", "b", "d"],
+            ["a", "b", "d", "e"],
+            ["a", "b", "e"],
+            ["a", "c"],
+            ["a", "c", "d"],
+            ["a", "c", "d", "e"],
+            ["a", "c", "e"],
+            ["a", "d"],
+            ["a", "d", "e"],
+            ["a", "e"],
+            ["b", "c"],
+            ["b", "c", "d"],
+            ["b", "c", "d", "e"],
+            ["b", "c", "e"],
+            ["b", "c", "f"],
+            ["b", "d"],
+            ["b", "d", "e"],
+            ["b", "e"],
+            ["b", "f"],
+            ["c", "d"],
+            ["c", "d", "e"],
+            ["c", "d", "e", "g"],
+            ["c", "d", "g"],
+            ["c", "e"],
+            ["c", "e", "g"],
+            ["c", "f"],
+            ["c", "f", "g"],
+            ["c", "g"],
+            ["d", "e"],
+            ["d", "e", "g"],
+            ["d", "g"],
+            ["e", "g"],
+            ["f", "g"],
+            ["a", "b", "c"],
+            ["a", "b", "c", "d"],
+            ["a", "b", "c", "d", "e"],
+            ["a", "b", "c", "e"],
+        ]
+
+        assert sorted(map(sorted, cliques)) == sorted(map(sorted, expected_cliques))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cluster.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cluster.py
new file mode 100644
index 00000000..b656ba81
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cluster.py
@@ -0,0 +1,549 @@
+import pytest
+
+import networkx as nx
+
+
+class TestTriangles:
+    def test_empty(self):
+        G = nx.Graph()
+        assert list(nx.triangles(G).values()) == []
+
+    def test_path(self):
+        G = nx.path_graph(10)
+        assert list(nx.triangles(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+        assert nx.triangles(G) == {
+            0: 0,
+            1: 0,
+            2: 0,
+            3: 0,
+            4: 0,
+            5: 0,
+            6: 0,
+            7: 0,
+            8: 0,
+            9: 0,
+        }
+
+    def test_cubical(self):
+        G = nx.cubical_graph()
+        assert list(nx.triangles(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0]
+        assert nx.triangles(G, 1) == 0
+        assert list(nx.triangles(G, [1, 2]).values()) == [0, 0]
+        assert nx.triangles(G, 1) == 0
+        assert nx.triangles(G, [1, 2]) == {1: 0, 2: 0}
+
+    def test_k5(self):
+        G = nx.complete_graph(5)
+        assert list(nx.triangles(G).values()) == [6, 6, 6, 6, 6]
+        assert sum(nx.triangles(G).values()) / 3 == 10
+        assert nx.triangles(G, 1) == 6
+        G.remove_edge(1, 2)
+        assert list(nx.triangles(G).values()) == [5, 3, 3, 5, 5]
+        assert nx.triangles(G, 1) == 3
+        G.add_edge(3, 3)  # ignore self-edges
+        assert list(nx.triangles(G).values()) == [5, 3, 3, 5, 5]
+        assert nx.triangles(G, 3) == 5
+
+
+class TestDirectedClustering:
+    def test_clustering(self):
+        G = nx.DiGraph()
+        assert list(nx.clustering(G).values()) == []
+        assert nx.clustering(G) == {}
+
+    def test_path(self):
+        G = nx.path_graph(10, create_using=nx.DiGraph())
+        assert list(nx.clustering(G).values()) == [
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+        ]
+        assert nx.clustering(G) == {
+            0: 0,
+            1: 0,
+            2: 0,
+            3: 0,
+            4: 0,
+            5: 0,
+            6: 0,
+            7: 0,
+            8: 0,
+            9: 0,
+        }
+        assert nx.clustering(G, 0) == 0
+
+    def test_k5(self):
+        G = nx.complete_graph(5, create_using=nx.DiGraph())
+        assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1]
+        assert nx.average_clustering(G) == 1
+        G.remove_edge(1, 2)
+        assert list(nx.clustering(G).values()) == [
+            11 / 12,
+            1,
+            1,
+            11 / 12,
+            11 / 12,
+        ]
+        assert nx.clustering(G, [1, 4]) == {1: 1, 4: 11 / 12}
+        G.remove_edge(2, 1)
+        assert list(nx.clustering(G).values()) == [
+            5 / 6,
+            1,
+            1,
+            5 / 6,
+            5 / 6,
+        ]
+        assert nx.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
+        assert nx.clustering(G, 4) == 5 / 6
+
+    def test_triangle_and_edge(self):
+        G = nx.cycle_graph(3, create_using=nx.DiGraph())
+        G.add_edge(0, 4)
+        assert nx.clustering(G)[0] == 1 / 6
+
+
+class TestDirectedWeightedClustering:
+    @classmethod
+    def setup_class(cls):
+        global np
+        np = pytest.importorskip("numpy")
+
+    def test_clustering(self):
+        G = nx.DiGraph()
+        assert list(nx.clustering(G, weight="weight").values()) == []
+        assert nx.clustering(G) == {}
+
+    def test_path(self):
+        G = nx.path_graph(10, create_using=nx.DiGraph())
+        assert list(nx.clustering(G, weight="weight").values()) == [
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+        ]
+        assert nx.clustering(G, weight="weight") == {
+            0: 0,
+            1: 0,
+            2: 0,
+            3: 0,
+            4: 0,
+            5: 0,
+            6: 0,
+            7: 0,
+            8: 0,
+            9: 0,
+        }
+
+    def test_k5(self):
+        G = nx.complete_graph(5, create_using=nx.DiGraph())
+        assert list(nx.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
+        assert nx.average_clustering(G, weight="weight") == 1
+        G.remove_edge(1, 2)
+        assert list(nx.clustering(G, weight="weight").values()) == [
+            11 / 12,
+            1,
+            1,
+            11 / 12,
+            11 / 12,
+        ]
+        assert nx.clustering(G, [1, 4], weight="weight") == {1: 1, 4: 11 / 12}
+        G.remove_edge(2, 1)
+        assert list(nx.clustering(G, weight="weight").values()) == [
+            5 / 6,
+            1,
+            1,
+            5 / 6,
+            5 / 6,
+        ]
+        assert nx.clustering(G, [1, 4], weight="weight") == {
+            1: 1,
+            4: 0.83333333333333337,
+        }
+
+    def test_triangle_and_edge(self):
+        G = nx.cycle_graph(3, create_using=nx.DiGraph())
+        G.add_edge(0, 4, weight=2)
+        assert nx.clustering(G)[0] == 1 / 6
+        # Relaxed comparisons to allow graphblas-algorithms to pass tests
+        np.testing.assert_allclose(nx.clustering(G, weight="weight")[0], 1 / 12)
+        np.testing.assert_allclose(nx.clustering(G, 0, weight="weight"), 1 / 12)
+
+
+class TestWeightedClustering:
+    @classmethod
+    def setup_class(cls):
+        global np
+        np = pytest.importorskip("numpy")
+
+    def test_clustering(self):
+        G = nx.Graph()
+        assert list(nx.clustering(G, weight="weight").values()) == []
+        assert nx.clustering(G) == {}
+
+    def test_path(self):
+        G = nx.path_graph(10)
+        assert list(nx.clustering(G, weight="weight").values()) == [
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+        ]
+        assert nx.clustering(G, weight="weight") == {
+            0: 0,
+            1: 0,
+            2: 0,
+            3: 0,
+            4: 0,
+            5: 0,
+            6: 0,
+            7: 0,
+            8: 0,
+            9: 0,
+        }
+
+    def test_cubical(self):
+        G = nx.cubical_graph()
+        assert list(nx.clustering(G, weight="weight").values()) == [
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+        ]
+        assert nx.clustering(G, 1) == 0
+        assert list(nx.clustering(G, [1, 2], weight="weight").values()) == [0, 0]
+        assert nx.clustering(G, 1, weight="weight") == 0
+        assert nx.clustering(G, [1, 2], weight="weight") == {1: 0, 2: 0}
+
+    def test_k5(self):
+        G = nx.complete_graph(5)
+        assert list(nx.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
+        assert nx.average_clustering(G, weight="weight") == 1
+        G.remove_edge(1, 2)
+        assert list(nx.clustering(G, weight="weight").values()) == [
+            5 / 6,
+            1,
+            1,
+            5 / 6,
+            5 / 6,
+        ]
+        assert nx.clustering(G, [1, 4], weight="weight") == {
+            1: 1,
+            4: 0.83333333333333337,
+        }
+
+    def test_triangle_and_edge(self):
+        G = nx.cycle_graph(3)
+        G.add_edge(0, 4, weight=2)
+        assert nx.clustering(G)[0] == 1 / 3
+        np.testing.assert_allclose(nx.clustering(G, weight="weight")[0], 1 / 6)
+        np.testing.assert_allclose(nx.clustering(G, 0, weight="weight"), 1 / 6)
+
+    def test_triangle_and_signed_edge(self):
+        G = nx.cycle_graph(3)
+        G.add_edge(0, 1, weight=-1)
+        G.add_edge(3, 0, weight=0)
+        assert nx.clustering(G)[0] == 1 / 3
+        assert nx.clustering(G, weight="weight")[0] == -1 / 3
+
+
+class TestClustering:
+    @classmethod
+    def setup_class(cls):
+        pytest.importorskip("numpy")
+
+    def test_clustering(self):
+        G = nx.Graph()
+        assert list(nx.clustering(G).values()) == []
+        assert nx.clustering(G) == {}
+
+    def test_path(self):
+        G = nx.path_graph(10)
+        assert list(nx.clustering(G).values()) == [
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+        ]
+        assert nx.clustering(G) == {
+            0: 0,
+            1: 0,
+            2: 0,
+            3: 0,
+            4: 0,
+            5: 0,
+            6: 0,
+            7: 0,
+            8: 0,
+            9: 0,
+        }
+
+    def test_cubical(self):
+        G = nx.cubical_graph()
+        assert list(nx.clustering(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0]
+        assert nx.clustering(G, 1) == 0
+        assert list(nx.clustering(G, [1, 2]).values()) == [0, 0]
+        assert nx.clustering(G, 1) == 0
+        assert nx.clustering(G, [1, 2]) == {1: 0, 2: 0}
+
+    def test_k5(self):
+        G = nx.complete_graph(5)
+        assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1]
+        assert nx.average_clustering(G) == 1
+        G.remove_edge(1, 2)
+        assert list(nx.clustering(G).values()) == [
+            5 / 6,
+            1,
+            1,
+            5 / 6,
+            5 / 6,
+        ]
+        assert nx.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
+
+    def test_k5_signed(self):
+        G = nx.complete_graph(5)
+        assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1]
+        assert nx.average_clustering(G) == 1
+        G.remove_edge(1, 2)
+        G.add_edge(0, 1, weight=-1)
+        assert list(nx.clustering(G, weight="weight").values()) == [
+            1 / 6,
+            -1 / 3,
+            1,
+            3 / 6,
+            3 / 6,
+        ]
+
+
+class TestTransitivity:
+    def test_transitivity(self):
+        G = nx.Graph()
+        assert nx.transitivity(G) == 0
+
+    def test_path(self):
+        G = nx.path_graph(10)
+        assert nx.transitivity(G) == 0
+
+    def test_cubical(self):
+        G = nx.cubical_graph()
+        assert nx.transitivity(G) == 0
+
+    def test_k5(self):
+        G = nx.complete_graph(5)
+        assert nx.transitivity(G) == 1
+        G.remove_edge(1, 2)
+        assert nx.transitivity(G) == 0.875
+
+
+class TestSquareClustering:
+    def test_clustering(self):
+        G = nx.Graph()
+        assert list(nx.square_clustering(G).values()) == []
+        assert nx.square_clustering(G) == {}
+
+    def test_path(self):
+        G = nx.path_graph(10)
+        assert list(nx.square_clustering(G).values()) == [
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+        ]
+        assert nx.square_clustering(G) == {
+            0: 0,
+            1: 0,
+            2: 0,
+            3: 0,
+            4: 0,
+            5: 0,
+            6: 0,
+            7: 0,
+            8: 0,
+            9: 0,
+        }
+
+    def test_cubical(self):
+        G = nx.cubical_graph()
+        assert list(nx.square_clustering(G).values()) == [
+            1 / 3,
+            1 / 3,
+            1 / 3,
+            1 / 3,
+            1 / 3,
+            1 / 3,
+            1 / 3,
+            1 / 3,
+        ]
+        assert list(nx.square_clustering(G, [1, 2]).values()) == [1 / 3, 1 / 3]
+        assert nx.square_clustering(G, [1])[1] == 1 / 3
+        assert nx.square_clustering(G, 1) == 1 / 3
+        assert nx.square_clustering(G, [1, 2]) == {1: 1 / 3, 2: 1 / 3}
+
+    def test_k5(self):
+        G = nx.complete_graph(5)
+        assert list(nx.square_clustering(G).values()) == [1, 1, 1, 1, 1]
+
+    def test_bipartite_k5(self):
+        G = nx.complete_bipartite_graph(5, 5)
+        assert list(nx.square_clustering(G).values()) == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+
+    def test_lind_square_clustering(self):
+        """Test C4 for figure 1 Lind et al (2005)"""
+        G = nx.Graph(
+            [
+                (1, 2),
+                (1, 3),
+                (1, 6),
+                (1, 7),
+                (2, 4),
+                (2, 5),
+                (3, 4),
+                (3, 5),
+                (6, 7),
+                (7, 8),
+                (6, 8),
+                (7, 9),
+                (7, 10),
+                (6, 11),
+                (6, 12),
+                (2, 13),
+                (2, 14),
+                (3, 15),
+                (3, 16),
+            ]
+        )
+        G1 = G.subgraph([1, 2, 3, 4, 5, 13, 14, 15, 16])
+        G2 = G.subgraph([1, 6, 7, 8, 9, 10, 11, 12])
+        assert nx.square_clustering(G, [1])[1] == 3 / 43
+        assert nx.square_clustering(G1, [1])[1] == 2 / 6
+        assert nx.square_clustering(G2, [1])[1] == 1 / 5
+
+    def test_peng_square_clustering(self):
+        """Test eq2 for figure 1 Peng et al (2008)"""
+        G = nx.Graph([(1, 2), (1, 3), (2, 4), (3, 4), (3, 5), (3, 6)])
+        assert nx.square_clustering(G, [1])[1] == 1 / 3
+
+    def test_self_loops_square_clustering(self):
+        G = nx.path_graph(5)
+        assert nx.square_clustering(G) == {0: 0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0}
+        G.add_edges_from([(0, 0), (1, 1), (2, 2)])
+        assert nx.square_clustering(G) == {0: 1, 1: 0.5, 2: 0.2, 3: 0.0, 4: 0}
+
+
+class TestAverageClustering:
+    @classmethod
+    def setup_class(cls):
+        pytest.importorskip("numpy")
+
+    def test_empty(self):
+        G = nx.Graph()
+        with pytest.raises(ZeroDivisionError):
+            nx.average_clustering(G)
+
+    def test_average_clustering(self):
+        G = nx.cycle_graph(3)
+        G.add_edge(2, 3)
+        assert nx.average_clustering(G) == (1 + 1 + 1 / 3) / 4
+        assert nx.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 4
+        assert nx.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 3
+        assert nx.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 3
+        assert nx.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 3
+        assert nx.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 2
+
+    def test_average_clustering_signed(self):
+        G = nx.cycle_graph(3)
+        G.add_edge(2, 3)
+        G.add_edge(0, 1, weight=-1)
+        assert nx.average_clustering(G, weight="weight") == (-1 - 1 - 1 / 3) / 4
+        assert (
+            nx.average_clustering(G, weight="weight", count_zeros=True)
+            == (-1 - 1 - 1 / 3) / 4
+        )
+        assert (
+            nx.average_clustering(G, weight="weight", count_zeros=False)
+            == (-1 - 1 - 1 / 3) / 3
+        )
+
+
+class TestDirectedAverageClustering:
+    @classmethod
+    def setup_class(cls):
+        pytest.importorskip("numpy")
+
+    def test_empty(self):
+        G = nx.DiGraph()
+        with pytest.raises(ZeroDivisionError):
+            nx.average_clustering(G)
+
+    def test_average_clustering(self):
+        G = nx.cycle_graph(3, create_using=nx.DiGraph())
+        G.add_edge(2, 3)
+        assert nx.average_clustering(G) == (1 + 1 + 1 / 3) / 8
+        assert nx.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 8
+        assert nx.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 6
+        assert nx.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 6
+        assert nx.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 6
+        assert nx.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 4
+
+
+class TestGeneralizedDegree:
+    def test_generalized_degree(self):
+        G = nx.Graph()
+        assert nx.generalized_degree(G) == {}
+
+    def test_path(self):
+        G = nx.path_graph(5)
+        assert nx.generalized_degree(G, 0) == {0: 1}
+        assert nx.generalized_degree(G, 1) == {0: 2}
+
+    def test_cubical(self):
+        G = nx.cubical_graph()
+        assert nx.generalized_degree(G, 0) == {0: 3}
+
+    def test_k5(self):
+        G = nx.complete_graph(5)
+        assert nx.generalized_degree(G, 0) == {3: 4}
+        G.remove_edge(0, 1)
+        assert nx.generalized_degree(G, 0) == {2: 3}
+        assert nx.generalized_degree(G, [1, 2]) == {1: {2: 3}, 2: {2: 2, 3: 2}}
+        assert nx.generalized_degree(G) == {
+            0: {2: 3},
+            1: {2: 3},
+            2: {2: 2, 3: 2},
+            3: {2: 2, 3: 2},
+            4: {2: 2, 3: 2},
+        }
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_communicability.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_communicability.py
new file mode 100644
index 00000000..0f447094
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_communicability.py
@@ -0,0 +1,80 @@
+from collections import defaultdict
+
+import pytest
+
+pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+import networkx as nx
+from networkx.algorithms.communicability_alg import communicability, communicability_exp
+
+
+class TestCommunicability:
+    def test_communicability(self):
+        answer = {
+            0: {0: 1.5430806348152435, 1: 1.1752011936438012},
+            1: {0: 1.1752011936438012, 1: 1.5430806348152435},
+        }
+        #        answer={(0, 0): 1.5430806348152435,
+        #                (0, 1): 1.1752011936438012,
+        #                (1, 0): 1.1752011936438012,
+        #                (1, 1): 1.5430806348152435}
+
+        result = communicability(nx.path_graph(2))
+        for k1, val in result.items():
+            for k2 in val:
+                assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7)
+
+    def test_communicability2(self):
+        answer_orig = {
+            ("1", "1"): 1.6445956054135658,
+            ("1", "Albert"): 0.7430186221096251,
+            ("1", "Aric"): 0.7430186221096251,
+            ("1", "Dan"): 1.6208126320442937,
+            ("1", "Franck"): 0.42639707170035257,
+            ("Albert", "1"): 0.7430186221096251,
+            ("Albert", "Albert"): 2.4368257358712189,
+            ("Albert", "Aric"): 1.4368257358712191,
+            ("Albert", "Dan"): 2.0472097037446453,
+            ("Albert", "Franck"): 1.8340111678944691,
+            ("Aric", "1"): 0.7430186221096251,
+            ("Aric", "Albert"): 1.4368257358712191,
+            ("Aric", "Aric"): 2.4368257358712193,
+            ("Aric", "Dan"): 2.0472097037446457,
+            ("Aric", "Franck"): 1.8340111678944691,
+            ("Dan", "1"): 1.6208126320442937,
+            ("Dan", "Albert"): 2.0472097037446453,
+            ("Dan", "Aric"): 2.0472097037446457,
+            ("Dan", "Dan"): 3.1306328496328168,
+            ("Dan", "Franck"): 1.4860372442192515,
+            ("Franck", "1"): 0.42639707170035257,
+            ("Franck", "Albert"): 1.8340111678944691,
+            ("Franck", "Aric"): 1.8340111678944691,
+            ("Franck", "Dan"): 1.4860372442192515,
+            ("Franck", "Franck"): 2.3876142275231915,
+        }
+
+        answer = defaultdict(dict)
+        for (k1, k2), v in answer_orig.items():
+            answer[k1][k2] = v
+
+        G1 = nx.Graph(
+            [
+                ("Franck", "Aric"),
+                ("Aric", "Dan"),
+                ("Dan", "Albert"),
+                ("Albert", "Franck"),
+                ("Dan", "1"),
+                ("Franck", "Albert"),
+            ]
+        )
+
+        result = communicability(G1)
+        for k1, val in result.items():
+            for k2 in val:
+                assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7)
+
+        result = communicability_exp(G1)
+        for k1, val in result.items():
+            for k2 in val:
+                assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_core.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_core.py
new file mode 100644
index 00000000..726e98a7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_core.py
@@ -0,0 +1,266 @@
+import pytest
+
+import networkx as nx
+from networkx.utils import nodes_equal
+
+
+class TestCore:
+    @classmethod
+    def setup_class(cls):
+        # G is the example graph in Figure 1 from Batagelj and
+        # Zaversnik's paper titled An O(m) Algorithm for Cores
+        # Decomposition of Networks, 2003,
+        # http://arXiv.org/abs/cs/0310049.  With nodes labeled as
+        # shown, the 3-core is given by nodes 1-8, the 2-core by nodes
+        # 9-16, the 1-core by nodes 17-20 and node 21 is in the
+        # 0-core.
+        t1 = nx.convert_node_labels_to_integers(nx.tetrahedral_graph(), 1)
+        t2 = nx.convert_node_labels_to_integers(t1, 5)
+        G = nx.union(t1, t2)
+        G.add_edges_from(
+            [
+                (3, 7),
+                (2, 11),
+                (11, 5),
+                (11, 12),
+                (5, 12),
+                (12, 19),
+                (12, 18),
+                (3, 9),
+                (7, 9),
+                (7, 10),
+                (9, 10),
+                (9, 20),
+                (17, 13),
+                (13, 14),
+                (14, 15),
+                (15, 16),
+                (16, 13),
+            ]
+        )
+        G.add_node(21)
+        cls.G = G
+
+        # Create the graph H resulting from the degree sequence
+        # [0, 1, 2, 2, 2, 2, 3] when using the Havel-Hakimi algorithm.
+
+        degseq = [0, 1, 2, 2, 2, 2, 3]
+        H = nx.havel_hakimi_graph(degseq)
+        mapping = {6: 0, 0: 1, 4: 3, 5: 6, 3: 4, 1: 2, 2: 5}
+        cls.H = nx.relabel_nodes(H, mapping)
+
+    def test_trivial(self):
+        """Empty graph"""
+        G = nx.Graph()
+        assert nx.core_number(G) == {}
+
+    def test_core_number(self):
+        core = nx.core_number(self.G)
+        nodes_by_core = [sorted(n for n in core if core[n] == val) for val in range(4)]
+        assert nodes_equal(nodes_by_core[0], [21])
+        assert nodes_equal(nodes_by_core[1], [17, 18, 19, 20])
+        assert nodes_equal(nodes_by_core[2], [9, 10, 11, 12, 13, 14, 15, 16])
+        assert nodes_equal(nodes_by_core[3], [1, 2, 3, 4, 5, 6, 7, 8])
+
+    def test_core_number2(self):
+        core = nx.core_number(self.H)
+        nodes_by_core = [sorted(n for n in core if core[n] == val) for val in range(3)]
+        assert nodes_equal(nodes_by_core[0], [0])
+        assert nodes_equal(nodes_by_core[1], [1, 3])
+        assert nodes_equal(nodes_by_core[2], [2, 4, 5, 6])
+
+    def test_core_number_multigraph(self):
+        G = nx.complete_graph(3)
+        G = nx.MultiGraph(G)
+        G.add_edge(1, 2)
+        with pytest.raises(
+            nx.NetworkXNotImplemented, match="not implemented for multigraph type"
+        ):
+            nx.core_number(G)
+
+    def test_core_number_self_loop(self):
+        G = nx.cycle_graph(3)
+        G.add_edge(0, 0)
+        with pytest.raises(
+            nx.NetworkXNotImplemented, match="Input graph has self loops"
+        ):
+            nx.core_number(G)
+
+    def test_directed_core_number(self):
+        """core number had a bug for directed graphs found in issue #1959"""
+        # small example where too timid edge removal can make cn[2] = 3
+        G = nx.DiGraph()
+        edges = [(1, 2), (2, 1), (2, 3), (2, 4), (3, 4), (4, 3)]
+        G.add_edges_from(edges)
+        assert nx.core_number(G) == {1: 2, 2: 2, 3: 2, 4: 2}
+        # small example where too aggressive edge removal can make cn[2] = 2
+        more_edges = [(1, 5), (3, 5), (4, 5), (3, 6), (4, 6), (5, 6)]
+        G.add_edges_from(more_edges)
+        assert nx.core_number(G) == {1: 3, 2: 3, 3: 3, 4: 3, 5: 3, 6: 3}
+
+    def test_main_core(self):
+        main_core_subgraph = nx.k_core(self.H)
+        assert sorted(main_core_subgraph.nodes()) == [2, 4, 5, 6]
+
+    def test_k_core(self):
+        # k=0
+        k_core_subgraph = nx.k_core(self.H, k=0)
+        assert sorted(k_core_subgraph.nodes()) == sorted(self.H.nodes())
+        # k=1
+        k_core_subgraph = nx.k_core(self.H, k=1)
+        assert sorted(k_core_subgraph.nodes()) == [1, 2, 3, 4, 5, 6]
+        # k = 2
+        k_core_subgraph = nx.k_core(self.H, k=2)
+        assert sorted(k_core_subgraph.nodes()) == [2, 4, 5, 6]
+
+    def test_k_core_multigraph(self):
+        core_number = nx.core_number(self.H)
+        H = nx.MultiGraph(self.H)
+        with pytest.deprecated_call():
+            nx.k_core(H, k=0, core_number=core_number)
+
+    def test_main_crust(self):
+        main_crust_subgraph = nx.k_crust(self.H)
+        assert sorted(main_crust_subgraph.nodes()) == [0, 1, 3]
+
+    def test_k_crust(self):
+        # k = 0
+        k_crust_subgraph = nx.k_crust(self.H, k=2)
+        assert sorted(k_crust_subgraph.nodes()) == sorted(self.H.nodes())
+        # k=1
+        k_crust_subgraph = nx.k_crust(self.H, k=1)
+        assert sorted(k_crust_subgraph.nodes()) == [0, 1, 3]
+        # k=2
+        k_crust_subgraph = nx.k_crust(self.H, k=0)
+        assert sorted(k_crust_subgraph.nodes()) == [0]
+
+    def test_k_crust_multigraph(self):
+        core_number = nx.core_number(self.H)
+        H = nx.MultiGraph(self.H)
+        with pytest.deprecated_call():
+            nx.k_crust(H, k=0, core_number=core_number)
+
+    def test_main_shell(self):
+        main_shell_subgraph = nx.k_shell(self.H)
+        assert sorted(main_shell_subgraph.nodes()) == [2, 4, 5, 6]
+
+    def test_k_shell(self):
+        # k=0
+        k_shell_subgraph = nx.k_shell(self.H, k=2)
+        assert sorted(k_shell_subgraph.nodes()) == [2, 4, 5, 6]
+        # k=1
+        k_shell_subgraph = nx.k_shell(self.H, k=1)
+        assert sorted(k_shell_subgraph.nodes()) == [1, 3]
+        # k=2
+        k_shell_subgraph = nx.k_shell(self.H, k=0)
+        assert sorted(k_shell_subgraph.nodes()) == [0]
+
+    def test_k_shell_multigraph(self):
+        core_number = nx.core_number(self.H)
+        H = nx.MultiGraph(self.H)
+        with pytest.deprecated_call():
+            nx.k_shell(H, k=0, core_number=core_number)
+
+    def test_k_corona(self):
+        # k=0
+        k_corona_subgraph = nx.k_corona(self.H, k=2)
+        assert sorted(k_corona_subgraph.nodes()) == [2, 4, 5, 6]
+        # k=1
+        k_corona_subgraph = nx.k_corona(self.H, k=1)
+        assert sorted(k_corona_subgraph.nodes()) == [1]
+        # k=2
+        k_corona_subgraph = nx.k_corona(self.H, k=0)
+        assert sorted(k_corona_subgraph.nodes()) == [0]
+
+    def test_k_corona_multigraph(self):
+        core_number = nx.core_number(self.H)
+        H = nx.MultiGraph(self.H)
+        with pytest.deprecated_call():
+            nx.k_corona(H, k=0, core_number=core_number)
+
+    def test_k_truss(self):
+        # k=-1
+        k_truss_subgraph = nx.k_truss(self.G, -1)
+        assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21))
+        # k=0
+        k_truss_subgraph = nx.k_truss(self.G, 0)
+        assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21))
+        # k=1
+        k_truss_subgraph = nx.k_truss(self.G, 1)
+        assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21))
+        # k=2
+        k_truss_subgraph = nx.k_truss(self.G, 2)
+        assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21))
+        # k=3
+        k_truss_subgraph = nx.k_truss(self.G, 3)
+        assert sorted(k_truss_subgraph.nodes()) == list(range(1, 13))
+
+        k_truss_subgraph = nx.k_truss(self.G, 4)
+        assert sorted(k_truss_subgraph.nodes()) == list(range(1, 9))
+
+        k_truss_subgraph = nx.k_truss(self.G, 5)
+        assert sorted(k_truss_subgraph.nodes()) == []
+
+    def test_k_truss_digraph(self):
+        G = nx.complete_graph(3)
+        G = nx.DiGraph(G)
+        G.add_edge(2, 1)
+        with pytest.raises(
+            nx.NetworkXNotImplemented, match="not implemented for directed type"
+        ):
+            nx.k_truss(G, k=1)
+
+    def test_k_truss_multigraph(self):
+        G = nx.complete_graph(3)
+        G = nx.MultiGraph(G)
+        G.add_edge(1, 2)
+        with pytest.raises(
+            nx.NetworkXNotImplemented, match="not implemented for multigraph type"
+        ):
+            nx.k_truss(G, k=1)
+
+    def test_k_truss_self_loop(self):
+        G = nx.cycle_graph(3)
+        G.add_edge(0, 0)
+        with pytest.raises(
+            nx.NetworkXNotImplemented, match="Input graph has self loops"
+        ):
+            nx.k_truss(G, k=1)
+
+    def test_onion_layers(self):
+        layers = nx.onion_layers(self.G)
+        nodes_by_layer = [
+            sorted(n for n in layers if layers[n] == val) for val in range(1, 7)
+        ]
+        assert nodes_equal(nodes_by_layer[0], [21])
+        assert nodes_equal(nodes_by_layer[1], [17, 18, 19, 20])
+        assert nodes_equal(nodes_by_layer[2], [10, 12, 13, 14, 15, 16])
+        assert nodes_equal(nodes_by_layer[3], [9, 11])
+        assert nodes_equal(nodes_by_layer[4], [1, 2, 4, 5, 6, 8])
+        assert nodes_equal(nodes_by_layer[5], [3, 7])
+
+    def test_onion_digraph(self):
+        G = nx.complete_graph(3)
+        G = nx.DiGraph(G)
+        G.add_edge(2, 1)
+        with pytest.raises(
+            nx.NetworkXNotImplemented, match="not implemented for directed type"
+        ):
+            nx.onion_layers(G)
+
+    def test_onion_multigraph(self):
+        G = nx.complete_graph(3)
+        G = nx.MultiGraph(G)
+        G.add_edge(1, 2)
+        with pytest.raises(
+            nx.NetworkXNotImplemented, match="not implemented for multigraph type"
+        ):
+            nx.onion_layers(G)
+
+    def test_onion_self_loop(self):
+        G = nx.cycle_graph(3)
+        G.add_edge(0, 0)
+        with pytest.raises(
+            nx.NetworkXNotImplemented, match="Input graph contains self loops"
+        ):
+            nx.onion_layers(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_covering.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_covering.py
new file mode 100644
index 00000000..b2f97a86
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_covering.py
@@ -0,0 +1,85 @@
+import pytest
+
+import networkx as nx
+
+
+class TestMinEdgeCover:
+    """Tests for :func:`networkx.algorithms.min_edge_cover`"""
+
+    def test_empty_graph(self):
+        G = nx.Graph()
+        assert nx.min_edge_cover(G) == set()
+
+    def test_graph_with_loop(self):
+        G = nx.Graph()
+        G.add_edge(0, 0)
+        assert nx.min_edge_cover(G) == {(0, 0)}
+
+    def test_graph_with_isolated_v(self):
+        G = nx.Graph()
+        G.add_node(1)
+        with pytest.raises(
+            nx.NetworkXException,
+            match="Graph has a node with no edge incident on it, so no edge cover exists.",
+        ):
+            nx.min_edge_cover(G)
+
+    def test_graph_single_edge(self):
+        G = nx.Graph([(0, 1)])
+        assert nx.min_edge_cover(G) in ({(0, 1)}, {(1, 0)})
+
+    def test_graph_two_edge_path(self):
+        G = nx.path_graph(3)
+        min_cover = nx.min_edge_cover(G)
+        assert len(min_cover) == 2
+        for u, v in G.edges:
+            assert (u, v) in min_cover or (v, u) in min_cover
+
+    def test_bipartite_explicit(self):
+        G = nx.Graph()
+        G.add_nodes_from([1, 2, 3, 4], bipartite=0)
+        G.add_nodes_from(["a", "b", "c"], bipartite=1)
+        G.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")])
+        # Use bipartite method by prescribing the algorithm
+        min_cover = nx.min_edge_cover(
+            G, nx.algorithms.bipartite.matching.eppstein_matching
+        )
+        assert nx.is_edge_cover(G, min_cover)
+        assert len(min_cover) == 8
+        # Use the default method which is not specialized for bipartite
+        min_cover2 = nx.min_edge_cover(G)
+        assert nx.is_edge_cover(G, min_cover2)
+        assert len(min_cover2) == 4
+
+    def test_complete_graph_even(self):
+        G = nx.complete_graph(10)
+        min_cover = nx.min_edge_cover(G)
+        assert nx.is_edge_cover(G, min_cover)
+        assert len(min_cover) == 5
+
+    def test_complete_graph_odd(self):
+        G = nx.complete_graph(11)
+        min_cover = nx.min_edge_cover(G)
+        assert nx.is_edge_cover(G, min_cover)
+        assert len(min_cover) == 6
+
+
+class TestIsEdgeCover:
+    """Tests for :func:`networkx.algorithms.is_edge_cover`"""
+
+    def test_empty_graph(self):
+        G = nx.Graph()
+        assert nx.is_edge_cover(G, set())
+
+    def test_graph_with_loop(self):
+        G = nx.Graph()
+        G.add_edge(1, 1)
+        assert nx.is_edge_cover(G, {(1, 1)})
+
+    def test_graph_single_edge(self):
+        G = nx.Graph()
+        G.add_edge(0, 1)
+        assert nx.is_edge_cover(G, {(0, 0), (1, 1)})
+        assert nx.is_edge_cover(G, {(0, 1), (1, 0)})
+        assert nx.is_edge_cover(G, {(0, 1)})
+        assert not nx.is_edge_cover(G, {(0, 0)})
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cuts.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cuts.py
new file mode 100644
index 00000000..923efa50
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cuts.py
@@ -0,0 +1,171 @@
+"""Unit tests for the :mod:`networkx.algorithms.cuts` module."""
+
+import networkx as nx
+
+
+class TestCutSize:
+    """Unit tests for the :func:`~networkx.cut_size` function."""
+
+    def test_symmetric(self):
+        """Tests that the cut size is symmetric."""
+        G = nx.barbell_graph(3, 0)
+        S = {0, 1, 4}
+        T = {2, 3, 5}
+        assert nx.cut_size(G, S, T) == 4
+        assert nx.cut_size(G, T, S) == 4
+
+    def test_single_edge(self):
+        """Tests for a cut of a single edge."""
+        G = nx.barbell_graph(3, 0)
+        S = {0, 1, 2}
+        T = {3, 4, 5}
+        assert nx.cut_size(G, S, T) == 1
+        assert nx.cut_size(G, T, S) == 1
+
+    def test_directed(self):
+        """Tests that each directed edge is counted once in the cut."""
+        G = nx.barbell_graph(3, 0).to_directed()
+        S = {0, 1, 2}
+        T = {3, 4, 5}
+        assert nx.cut_size(G, S, T) == 2
+        assert nx.cut_size(G, T, S) == 2
+
+    def test_directed_symmetric(self):
+        """Tests that a cut in a directed graph is symmetric."""
+        G = nx.barbell_graph(3, 0).to_directed()
+        S = {0, 1, 4}
+        T = {2, 3, 5}
+        assert nx.cut_size(G, S, T) == 8
+        assert nx.cut_size(G, T, S) == 8
+
+    def test_multigraph(self):
+        """Tests that parallel edges are each counted for a cut."""
+        G = nx.MultiGraph(["ab", "ab"])
+        assert nx.cut_size(G, {"a"}, {"b"}) == 2
+
+
+class TestVolume:
+    """Unit tests for the :func:`~networkx.volume` function."""
+
+    def test_graph(self):
+        G = nx.cycle_graph(4)
+        assert nx.volume(G, {0, 1}) == 4
+
+    def test_digraph(self):
+        G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 0)])
+        assert nx.volume(G, {0, 1}) == 2
+
+    def test_multigraph(self):
+        edges = list(nx.cycle_graph(4).edges())
+        G = nx.MultiGraph(edges * 2)
+        assert nx.volume(G, {0, 1}) == 8
+
+    def test_multidigraph(self):
+        edges = [(0, 1), (1, 2), (2, 3), (3, 0)]
+        G = nx.MultiDiGraph(edges * 2)
+        assert nx.volume(G, {0, 1}) == 4
+
+    def test_barbell(self):
+        G = nx.barbell_graph(3, 0)
+        assert nx.volume(G, {0, 1, 2}) == 7
+        assert nx.volume(G, {3, 4, 5}) == 7
+
+
+class TestNormalizedCutSize:
+    """Unit tests for the :func:`~networkx.normalized_cut_size` function."""
+
+    def test_graph(self):
+        G = nx.path_graph(4)
+        S = {1, 2}
+        T = set(G) - S
+        size = nx.normalized_cut_size(G, S, T)
+        # The cut looks like this: o-{-o--o-}-o
+        expected = 2 * ((1 / 4) + (1 / 2))
+        assert expected == size
+        # Test with no input T
+        assert expected == nx.normalized_cut_size(G, S)
+
+    def test_directed(self):
+        G = nx.DiGraph([(0, 1), (1, 2), (2, 3)])
+        S = {1, 2}
+        T = set(G) - S
+        size = nx.normalized_cut_size(G, S, T)
+        # The cut looks like this: o-{->o-->o-}->o
+        expected = 2 * ((1 / 2) + (1 / 1))
+        assert expected == size
+        # Test with no input T
+        assert expected == nx.normalized_cut_size(G, S)
+
+
+class TestConductance:
+    """Unit tests for the :func:`~networkx.conductance` function."""
+
+    def test_graph(self):
+        G = nx.barbell_graph(5, 0)
+        # Consider the singleton sets containing the "bridge" nodes.
+        # There is only one cut edge, and each set has volume five.
+        S = {4}
+        T = {5}
+        conductance = nx.conductance(G, S, T)
+        expected = 1 / 5
+        assert expected == conductance
+        # Test with no input T
+        G2 = nx.barbell_graph(3, 0)
+        # There is only one cut edge, and each set has volume seven.
+        S2 = {0, 1, 2}
+        assert nx.conductance(G2, S2) == 1 / 7
+
+
+class TestEdgeExpansion:
+    """Unit tests for the :func:`~networkx.edge_expansion` function."""
+
+    def test_graph(self):
+        G = nx.barbell_graph(5, 0)
+        S = set(range(5))
+        T = set(G) - S
+        expansion = nx.edge_expansion(G, S, T)
+        expected = 1 / 5
+        assert expected == expansion
+        # Test with no input T
+        assert expected == nx.edge_expansion(G, S)
+
+
+class TestNodeExpansion:
+    """Unit tests for the :func:`~networkx.node_expansion` function."""
+
+    def test_graph(self):
+        G = nx.path_graph(8)
+        S = {3, 4, 5}
+        expansion = nx.node_expansion(G, S)
+        # The neighborhood of S has cardinality five, and S has
+        # cardinality three.
+        expected = 5 / 3
+        assert expected == expansion
+
+
+class TestBoundaryExpansion:
+    """Unit tests for the :func:`~networkx.boundary_expansion` function."""
+
+    def test_graph(self):
+        G = nx.complete_graph(10)
+        S = set(range(4))
+        expansion = nx.boundary_expansion(G, S)
+        # The node boundary of S has cardinality six, and S has
+        # cardinality three.
+        expected = 6 / 4
+        assert expected == expansion
+
+
+class TestMixingExpansion:
+    """Unit tests for the :func:`~networkx.mixing_expansion` function."""
+
+    def test_graph(self):
+        G = nx.barbell_graph(5, 0)
+        S = set(range(5))
+        T = set(G) - S
+        expansion = nx.mixing_expansion(G, S, T)
+        # There is one cut edge, and the total number of edges in the
+        # graph is twice the total number of edges in a clique of size
+        # five, plus one more for the bridge.
+        expected = 1 / (2 * (5 * 4 + 1))
+        assert expected == expansion
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cycles.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cycles.py
new file mode 100644
index 00000000..dd21405f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_cycles.py
@@ -0,0 +1,974 @@
+from itertools import chain, islice, tee
+from math import inf
+from random import shuffle
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.traversal.edgedfs import FORWARD, REVERSE
+
+
+def check_independent(basis):
+    if len(basis) == 0:
+        return
+
+    np = pytest.importorskip("numpy")
+    sp = pytest.importorskip("scipy")  # Required by incidence_matrix
+
+    H = nx.Graph()
+    for b in basis:
+        nx.add_cycle(H, b)
+    inc = nx.incidence_matrix(H, oriented=True)
+    rank = np.linalg.matrix_rank(inc.toarray(), tol=None, hermitian=False)
+    assert inc.shape[1] - rank == len(basis)
+
+
+class TestCycles:
+    @classmethod
+    def setup_class(cls):
+        G = nx.Graph()
+        nx.add_cycle(G, [0, 1, 2, 3])
+        nx.add_cycle(G, [0, 3, 4, 5])
+        nx.add_cycle(G, [0, 1, 6, 7, 8])
+        G.add_edge(8, 9)
+        cls.G = G
+
+    def is_cyclic_permutation(self, a, b):
+        n = len(a)
+        if len(b) != n:
+            return False
+        l = a + a
+        return any(l[i : i + n] == b for i in range(n))
+
+    def test_cycle_basis(self):
+        G = self.G
+        cy = nx.cycle_basis(G, 0)
+        sort_cy = sorted(sorted(c) for c in cy)
+        assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
+        cy = nx.cycle_basis(G, 1)
+        sort_cy = sorted(sorted(c) for c in cy)
+        assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
+        cy = nx.cycle_basis(G, 9)
+        sort_cy = sorted(sorted(c) for c in cy)
+        assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
+        # test disconnected graphs
+        nx.add_cycle(G, "ABC")
+        cy = nx.cycle_basis(G, 9)
+        sort_cy = sorted(sorted(c) for c in cy[:-1]) + [sorted(cy[-1])]
+        assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5], ["A", "B", "C"]]
+
+    def test_cycle_basis2(self):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            G = nx.DiGraph()
+            cy = nx.cycle_basis(G, 0)
+
+    def test_cycle_basis3(self):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            G = nx.MultiGraph()
+            cy = nx.cycle_basis(G, 0)
+
+    def test_cycle_basis_ordered(self):
+        # see gh-6654 replace sets with (ordered) dicts
+        G = nx.cycle_graph(5)
+        G.update(nx.cycle_graph(range(3, 8)))
+        cbG = nx.cycle_basis(G)
+
+        perm = {1: 0, 0: 1}  # switch 0 and 1
+        H = nx.relabel_nodes(G, perm)
+        cbH = [[perm.get(n, n) for n in cyc] for cyc in nx.cycle_basis(H)]
+        assert cbG == cbH
+
+    def test_cycle_basis_self_loop(self):
+        """Tests the function for graphs with self loops"""
+        G = nx.Graph()
+        nx.add_cycle(G, [0, 1, 2, 3])
+        nx.add_cycle(G, [0, 0, 6, 2])
+        cy = nx.cycle_basis(G)
+        sort_cy = sorted(sorted(c) for c in cy)
+        assert sort_cy == [[0], [0, 1, 2], [0, 2, 3], [0, 2, 6]]
+
+    def test_simple_cycles(self):
+        edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
+        G = nx.DiGraph(edges)
+        cc = sorted(nx.simple_cycles(G))
+        ca = [[0], [0, 1, 2], [0, 2], [1, 2], [2]]
+        assert len(cc) == len(ca)
+        for c in cc:
+            assert any(self.is_cyclic_permutation(c, rc) for rc in ca)
+
+    def test_simple_cycles_singleton(self):
+        G = nx.Graph([(0, 0)])  # self-loop
+        assert list(nx.simple_cycles(G)) == [[0]]
+
+    def test_unsortable(self):
+        # this test ensures that graphs whose nodes without an intrinsic
+        # ordering do not cause issues
+        G = nx.DiGraph()
+        nx.add_cycle(G, ["a", 1])
+        c = list(nx.simple_cycles(G))
+        assert len(c) == 1
+
+    def test_simple_cycles_small(self):
+        G = nx.DiGraph()
+        nx.add_cycle(G, [1, 2, 3])
+        c = sorted(nx.simple_cycles(G))
+        assert len(c) == 1
+        assert self.is_cyclic_permutation(c[0], [1, 2, 3])
+        nx.add_cycle(G, [10, 20, 30])
+        cc = sorted(nx.simple_cycles(G))
+        assert len(cc) == 2
+        ca = [[1, 2, 3], [10, 20, 30]]
+        for c in cc:
+            assert any(self.is_cyclic_permutation(c, rc) for rc in ca)
+
+    def test_simple_cycles_empty(self):
+        G = nx.DiGraph()
+        assert list(nx.simple_cycles(G)) == []
+
+    def worst_case_graph(self, k):
+        # see figure 1 in Johnson's paper
+        # this graph has exactly 3k simple cycles
+        G = nx.DiGraph()
+        for n in range(2, k + 2):
+            G.add_edge(1, n)
+            G.add_edge(n, k + 2)
+        G.add_edge(2 * k + 1, 1)
+        for n in range(k + 2, 2 * k + 2):
+            G.add_edge(n, 2 * k + 2)
+            G.add_edge(n, n + 1)
+        G.add_edge(2 * k + 3, k + 2)
+        for n in range(2 * k + 3, 3 * k + 3):
+            G.add_edge(2 * k + 2, n)
+            G.add_edge(n, 3 * k + 3)
+        G.add_edge(3 * k + 3, 2 * k + 2)
+        return G
+
+    def test_worst_case_graph(self):
+        # see figure 1 in Johnson's paper
+        for k in range(3, 10):
+            G = self.worst_case_graph(k)
+            l = len(list(nx.simple_cycles(G)))
+            assert l == 3 * k
+
+    def test_recursive_simple_and_not(self):
+        for k in range(2, 10):
+            G = self.worst_case_graph(k)
+            cc = sorted(nx.simple_cycles(G))
+            rcc = sorted(nx.recursive_simple_cycles(G))
+            assert len(cc) == len(rcc)
+            for c in cc:
+                assert any(self.is_cyclic_permutation(c, r) for r in rcc)
+            for rc in rcc:
+                assert any(self.is_cyclic_permutation(rc, c) for c in cc)
+
+    def test_simple_graph_with_reported_bug(self):
+        G = nx.DiGraph()
+        edges = [
+            (0, 2),
+            (0, 3),
+            (1, 0),
+            (1, 3),
+            (2, 1),
+            (2, 4),
+            (3, 2),
+            (3, 4),
+            (4, 0),
+            (4, 1),
+            (4, 5),
+            (5, 0),
+            (5, 1),
+            (5, 2),
+            (5, 3),
+        ]
+        G.add_edges_from(edges)
+        cc = sorted(nx.simple_cycles(G))
+        assert len(cc) == 26
+        rcc = sorted(nx.recursive_simple_cycles(G))
+        assert len(cc) == len(rcc)
+        for c in cc:
+            assert any(self.is_cyclic_permutation(c, rc) for rc in rcc)
+        for rc in rcc:
+            assert any(self.is_cyclic_permutation(rc, c) for c in cc)
+
+
+def pairwise(iterable):
+    a, b = tee(iterable)
+    next(b, None)
+    return zip(a, b)
+
+
+def cycle_edges(c):
+    return pairwise(chain(c, islice(c, 1)))
+
+
+def directed_cycle_edgeset(c):
+    return frozenset(cycle_edges(c))
+
+
+def undirected_cycle_edgeset(c):
+    if len(c) == 1:
+        return frozenset(cycle_edges(c))
+    return frozenset(map(frozenset, cycle_edges(c)))
+
+
+def multigraph_cycle_edgeset(c):
+    if len(c) <= 2:
+        return frozenset(cycle_edges(c))
+    else:
+        return frozenset(map(frozenset, cycle_edges(c)))
+
+
+class TestCycleEnumeration:
+    @staticmethod
+    def K(n):
+        return nx.complete_graph(n)
+
+    @staticmethod
+    def D(n):
+        return nx.complete_graph(n).to_directed()
+
+    @staticmethod
+    def edgeset_function(g):
+        if g.is_directed():
+            return directed_cycle_edgeset
+        elif g.is_multigraph():
+            return multigraph_cycle_edgeset
+        else:
+            return undirected_cycle_edgeset
+
+    def check_cycle(self, g, c, es, cache, source, original_c, length_bound, chordless):
+        if length_bound is not None and len(c) > length_bound:
+            raise RuntimeError(
+                f"computed cycle {original_c} exceeds length bound {length_bound}"
+            )
+        if source == "computed":
+            if es in cache:
+                raise RuntimeError(
+                    f"computed cycle {original_c} has already been found!"
+                )
+            else:
+                cache[es] = tuple(original_c)
+        else:
+            if es in cache:
+                cache.pop(es)
+            else:
+                raise RuntimeError(f"expected cycle {original_c} was not computed")
+
+        if not all(g.has_edge(*e) for e in es):
+            raise RuntimeError(
+                f"{source} claimed cycle {original_c} is not a cycle of g"
+            )
+        if chordless and len(g.subgraph(c).edges) > len(c):
+            raise RuntimeError(f"{source} cycle {original_c} is not chordless")
+
+    def check_cycle_algorithm(
+        self,
+        g,
+        expected_cycles,
+        length_bound=None,
+        chordless=False,
+        algorithm=None,
+    ):
+        if algorithm is None:
+            algorithm = nx.chordless_cycles if chordless else nx.simple_cycles
+
+        # note: we shuffle the labels of g to rule out accidentally-correct
+        # behavior which occurred during the development of chordless cycle
+        # enumeration algorithms
+
+        relabel = list(range(len(g)))
+        shuffle(relabel)
+        label = dict(zip(g, relabel))
+        unlabel = dict(zip(relabel, g))
+        h = nx.relabel_nodes(g, label, copy=True)
+
+        edgeset = self.edgeset_function(h)
+
+        params = {}
+        if length_bound is not None:
+            params["length_bound"] = length_bound
+
+        cycle_cache = {}
+        for c in algorithm(h, **params):
+            original_c = [unlabel[x] for x in c]
+            es = edgeset(c)
+            self.check_cycle(
+                h, c, es, cycle_cache, "computed", original_c, length_bound, chordless
+            )
+
+        if isinstance(expected_cycles, int):
+            if len(cycle_cache) != expected_cycles:
+                raise RuntimeError(
+                    f"expected {expected_cycles} cycles, got {len(cycle_cache)}"
+                )
+            return
+        for original_c in expected_cycles:
+            c = [label[x] for x in original_c]
+            es = edgeset(c)
+            self.check_cycle(
+                h, c, es, cycle_cache, "expected", original_c, length_bound, chordless
+            )
+
+        if len(cycle_cache):
+            for c in cycle_cache.values():
+                raise RuntimeError(
+                    f"computed cycle {c} is valid but not in the expected cycle set!"
+                )
+
+    def check_cycle_enumeration_integer_sequence(
+        self,
+        g_family,
+        cycle_counts,
+        length_bound=None,
+        chordless=False,
+        algorithm=None,
+    ):
+        for g, num_cycles in zip(g_family, cycle_counts):
+            self.check_cycle_algorithm(
+                g,
+                num_cycles,
+                length_bound=length_bound,
+                chordless=chordless,
+                algorithm=algorithm,
+            )
+
+    def test_directed_chordless_cycle_digons(self):
+        g = nx.DiGraph()
+        nx.add_cycle(g, range(5))
+        nx.add_cycle(g, range(5)[::-1])
+        g.add_edge(0, 0)
+        expected_cycles = [(0,), (1, 2), (2, 3), (3, 4)]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True)
+
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=2)
+
+        expected_cycles = [c for c in expected_cycles if len(c) < 2]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=1)
+
+    def test_directed_chordless_cycle_undirected(self):
+        g = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (5, 1), (0, 2)])
+        expected_cycles = [(0, 2, 3, 4, 5), (1, 2, 3, 4, 5)]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True)
+
+        g = nx.DiGraph()
+        nx.add_cycle(g, range(5))
+        nx.add_cycle(g, range(4, 9))
+        g.add_edge(7, 3)
+        expected_cycles = [(0, 1, 2, 3, 4), (3, 4, 5, 6, 7), (4, 5, 6, 7, 8)]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True)
+
+        g.add_edge(3, 7)
+        expected_cycles = [(0, 1, 2, 3, 4), (3, 7), (4, 5, 6, 7, 8)]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True)
+
+        expected_cycles = [(3, 7)]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=4)
+
+        g.remove_edge(7, 3)
+        expected_cycles = [(0, 1, 2, 3, 4), (4, 5, 6, 7, 8)]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True)
+
+        g = nx.DiGraph((i, j) for i in range(10) for j in range(i))
+        expected_cycles = []
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True)
+
+    def test_chordless_cycles_directed(self):
+        G = nx.DiGraph()
+        nx.add_cycle(G, range(5))
+        nx.add_cycle(G, range(4, 12))
+        expected = [[*range(5)], [*range(4, 12)]]
+        self.check_cycle_algorithm(G, expected, chordless=True)
+        self.check_cycle_algorithm(
+            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
+        )
+
+        G.add_edge(7, 3)
+        expected.append([*range(3, 8)])
+        self.check_cycle_algorithm(G, expected, chordless=True)
+        self.check_cycle_algorithm(
+            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
+        )
+
+        G.add_edge(3, 7)
+        expected[-1] = [7, 3]
+        self.check_cycle_algorithm(G, expected, chordless=True)
+        self.check_cycle_algorithm(
+            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
+        )
+
+        expected.pop()
+        G.remove_edge(7, 3)
+        self.check_cycle_algorithm(G, expected, chordless=True)
+        self.check_cycle_algorithm(
+            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
+        )
+
+    def test_directed_chordless_cycle_diclique(self):
+        g_family = [self.D(n) for n in range(10)]
+        expected_cycles = [(n * n - n) // 2 for n in range(10)]
+        self.check_cycle_enumeration_integer_sequence(
+            g_family, expected_cycles, chordless=True
+        )
+
+        expected_cycles = [(n * n - n) // 2 for n in range(10)]
+        self.check_cycle_enumeration_integer_sequence(
+            g_family, expected_cycles, length_bound=2
+        )
+
+    def test_directed_chordless_loop_blockade(self):
+        g = nx.DiGraph((i, i) for i in range(10))
+        nx.add_cycle(g, range(10))
+        expected_cycles = [(i,) for i in range(10)]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True)
+
+        self.check_cycle_algorithm(g, expected_cycles, length_bound=1)
+
+        g = nx.MultiDiGraph(g)
+        g.add_edges_from((i, i) for i in range(0, 10, 2))
+        expected_cycles = [(i,) for i in range(1, 10, 2)]
+        self.check_cycle_algorithm(g, expected_cycles, chordless=True)
+
+    def test_simple_cycles_notable_clique_sequences(self):
+        # A000292: Number of labeled graphs on n+3 nodes that are triangles.
+        g_family = [self.K(n) for n in range(2, 12)]
+        expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220]
+        self.check_cycle_enumeration_integer_sequence(
+            g_family, expected, length_bound=3
+        )
+
+        def triangles(g, **kwargs):
+            yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 3)
+
+        # directed complete graphs have twice as many triangles thanks to reversal
+        g_family = [self.D(n) for n in range(2, 12)]
+        expected = [2 * e for e in expected]
+        self.check_cycle_enumeration_integer_sequence(
+            g_family, expected, length_bound=3, algorithm=triangles
+        )
+
+        def four_cycles(g, **kwargs):
+            yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 4)
+
+        # A050534: the number of 4-cycles in the complete graph K_{n+1}
+        expected = [0, 0, 0, 3, 15, 45, 105, 210, 378, 630, 990]
+        g_family = [self.K(n) for n in range(1, 12)]
+        self.check_cycle_enumeration_integer_sequence(
+            g_family, expected, length_bound=4, algorithm=four_cycles
+        )
+
+        # directed complete graphs have twice as many 4-cycles thanks to reversal
+        expected = [2 * e for e in expected]
+        g_family = [self.D(n) for n in range(1, 15)]
+        self.check_cycle_enumeration_integer_sequence(
+            g_family, expected, length_bound=4, algorithm=four_cycles
+        )
+
+        # A006231: the number of elementary circuits in a complete directed graph with n nodes
+        expected = [0, 1, 5, 20, 84, 409, 2365]
+        g_family = [self.D(n) for n in range(1, 8)]
+        self.check_cycle_enumeration_integer_sequence(g_family, expected)
+
+        # A002807: Number of cycles in the complete graph on n nodes K_{n}.
+        expected = [0, 0, 0, 1, 7, 37, 197, 1172]
+        g_family = [self.K(n) for n in range(8)]
+        self.check_cycle_enumeration_integer_sequence(g_family, expected)
+
+    def test_directed_chordless_cycle_parallel_multiedges(self):
+        g = nx.MultiGraph()
+
+        nx.add_cycle(g, range(5))
+        expected = [[*range(5)]]
+        self.check_cycle_algorithm(g, expected, chordless=True)
+
+        nx.add_cycle(g, range(5))
+        expected = [*cycle_edges(range(5))]
+        self.check_cycle_algorithm(g, expected, chordless=True)
+
+        nx.add_cycle(g, range(5))
+        expected = []
+        self.check_cycle_algorithm(g, expected, chordless=True)
+
+        g = nx.MultiDiGraph()
+
+        nx.add_cycle(g, range(5))
+        expected = [[*range(5)]]
+        self.check_cycle_algorithm(g, expected, chordless=True)
+
+        nx.add_cycle(g, range(5))
+        self.check_cycle_algorithm(g, [], chordless=True)
+
+        nx.add_cycle(g, range(5))
+        self.check_cycle_algorithm(g, [], chordless=True)
+
+        g = nx.MultiDiGraph()
+
+        nx.add_cycle(g, range(5))
+        nx.add_cycle(g, range(5)[::-1])
+        expected = [*cycle_edges(range(5))]
+        self.check_cycle_algorithm(g, expected, chordless=True)
+
+        nx.add_cycle(g, range(5))
+        self.check_cycle_algorithm(g, [], chordless=True)
+
+    def test_chordless_cycles_graph(self):
+        G = nx.Graph()
+        nx.add_cycle(G, range(5))
+        nx.add_cycle(G, range(4, 12))
+        expected = [[*range(5)], [*range(4, 12)]]
+        self.check_cycle_algorithm(G, expected, chordless=True)
+        self.check_cycle_algorithm(
+            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
+        )
+
+        G.add_edge(7, 3)
+        expected.append([*range(3, 8)])
+        expected.append([4, 3, 7, 8, 9, 10, 11])
+        self.check_cycle_algorithm(G, expected, chordless=True)
+        self.check_cycle_algorithm(
+            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
+        )
+
+    def test_chordless_cycles_giant_hamiltonian(self):
+        # ... o - e - o - e - o ... # o = odd, e = even
+        # ... ---/ \-----/ \--- ... # <-- "long" edges
+        #
+        # each long edge belongs to exactly one triangle, and one giant cycle
+        # of length n/2.  The remaining edges each belong to a triangle
+
+        n = 1000
+        assert n % 2 == 0
+        G = nx.Graph()
+        for v in range(n):
+            if not v % 2:
+                G.add_edge(v, (v + 2) % n)
+            G.add_edge(v, (v + 1) % n)
+
+        expected = [[*range(0, n, 2)]] + [
+            [x % n for x in range(i, i + 3)] for i in range(0, n, 2)
+        ]
+        self.check_cycle_algorithm(G, expected, chordless=True)
+        self.check_cycle_algorithm(
+            G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True
+        )
+
+        # ... o -> e -> o -> e -> o ... # o = odd, e = even
+        # ... <---/ \---<---/ \---< ... # <-- "long" edges
+        #
+        # this time, we orient the short and long edges in opposition
+        # the cycle structure of this graph is the same, but we need to reverse
+        # the long one in our representation.  Also, we need to drop the size
+        # because our partitioning algorithm uses strongly connected components
+        # instead of separating graphs by their strong articulation points
+
+        n = 100
+        assert n % 2 == 0
+        G = nx.DiGraph()
+        for v in range(n):
+            G.add_edge(v, (v + 1) % n)
+            if not v % 2:
+                G.add_edge((v + 2) % n, v)
+
+        expected = [[*range(n - 2, -2, -2)]] + [
+            [x % n for x in range(i, i + 3)] for i in range(0, n, 2)
+        ]
+        self.check_cycle_algorithm(G, expected, chordless=True)
+        self.check_cycle_algorithm(
+            G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True
+        )
+
+    def test_simple_cycles_acyclic_tournament(self):
+        n = 10
+        G = nx.DiGraph((x, y) for x in range(n) for y in range(x))
+        self.check_cycle_algorithm(G, [])
+        self.check_cycle_algorithm(G, [], chordless=True)
+
+        for k in range(n + 1):
+            self.check_cycle_algorithm(G, [], length_bound=k)
+            self.check_cycle_algorithm(G, [], length_bound=k, chordless=True)
+
+    def test_simple_cycles_graph(self):
+        testG = nx.cycle_graph(8)
+        cyc1 = tuple(range(8))
+        self.check_cycle_algorithm(testG, [cyc1])
+
+        testG.add_edge(4, -1)
+        nx.add_path(testG, [3, -2, -3, -4])
+        self.check_cycle_algorithm(testG, [cyc1])
+
+        testG.update(nx.cycle_graph(range(8, 16)))
+        cyc2 = tuple(range(8, 16))
+        self.check_cycle_algorithm(testG, [cyc1, cyc2])
+
+        testG.update(nx.cycle_graph(range(4, 12)))
+        cyc3 = tuple(range(4, 12))
+        expected = {
+            (0, 1, 2, 3, 4, 5, 6, 7),  # cyc1
+            (8, 9, 10, 11, 12, 13, 14, 15),  # cyc2
+            (4, 5, 6, 7, 8, 9, 10, 11),  # cyc3
+            (4, 5, 6, 7, 8, 15, 14, 13, 12, 11),  # cyc2 + cyc3
+            (0, 1, 2, 3, 4, 11, 10, 9, 8, 7),  # cyc1 + cyc3
+            (0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7),  # cyc1 + cyc2 + cyc3
+        }
+        self.check_cycle_algorithm(testG, expected)
+        assert len(expected) == (2**3 - 1) - 1  # 1 disjoint comb: cyc1 + cyc2
+
+        # Basis size = 5 (2 loops overlapping gives 5 small loops
+        #        E
+        #       / \         Note: A-F = 10-15
+        #    1-2-3-4-5
+        #    / |   |  \   cyc1=012DAB -- left
+        #   0  D   F  6   cyc2=234E   -- top
+        #   \  |   |  /   cyc3=45678F -- right
+        #    B-A-9-8-7    cyc4=89AC   -- bottom
+        #       \ /       cyc5=234F89AD -- middle
+        #        C
+        #
+        # combinations of 5 basis elements: 2^5 - 1  (one includes no cycles)
+        #
+        # disjoint combs: (11 total) not simple cycles
+        #   Any pair not including cyc5 => choose(4, 2) = 6
+        #   Any triple not including cyc5 => choose(4, 3) = 4
+        #   Any quad not including cyc5 => choose(4, 4) = 1
+        #
+        # we expect 31 - 11 = 20 simple cycles
+        #
+        testG = nx.cycle_graph(12)
+        testG.update(nx.cycle_graph([12, 10, 13, 2, 14, 4, 15, 8]).edges)
+        expected = (2**5 - 1) - 11  # 11 disjoint combinations
+        self.check_cycle_algorithm(testG, expected)
+
+    def test_simple_cycles_bounded(self):
+        # iteratively construct a cluster of nested cycles running in the same direction
+        # there should be one cycle of every length
+        d = nx.DiGraph()
+        expected = []
+        for n in range(10):
+            nx.add_cycle(d, range(n))
+            expected.append(n)
+            for k, e in enumerate(expected):
+                self.check_cycle_algorithm(d, e, length_bound=k)
+
+        # iteratively construct a path of undirected cycles, connected at articulation
+        # points.  there should be one cycle of every length except 2: no digons
+        g = nx.Graph()
+        top = 0
+        expected = []
+        for n in range(10):
+            expected.append(n if n < 2 else n - 1)
+            if n == 2:
+                # no digons in undirected graphs
+                continue
+            nx.add_cycle(g, range(top, top + n))
+            top += n
+            for k, e in enumerate(expected):
+                self.check_cycle_algorithm(g, e, length_bound=k)
+
+    def test_simple_cycles_bound_corner_cases(self):
+        G = nx.cycle_graph(4)
+        DG = nx.cycle_graph(4, create_using=nx.DiGraph)
+        assert list(nx.simple_cycles(G, length_bound=0)) == []
+        assert list(nx.simple_cycles(DG, length_bound=0)) == []
+        assert list(nx.chordless_cycles(G, length_bound=0)) == []
+        assert list(nx.chordless_cycles(DG, length_bound=0)) == []
+
+    def test_simple_cycles_bound_error(self):
+        with pytest.raises(ValueError):
+            G = nx.DiGraph()
+            for c in nx.simple_cycles(G, -1):
+                assert False
+
+        with pytest.raises(ValueError):
+            G = nx.Graph()
+            for c in nx.simple_cycles(G, -1):
+                assert False
+
+        with pytest.raises(ValueError):
+            G = nx.Graph()
+            for c in nx.chordless_cycles(G, -1):
+                assert False
+
+        with pytest.raises(ValueError):
+            G = nx.DiGraph()
+            for c in nx.chordless_cycles(G, -1):
+                assert False
+
+    def test_chordless_cycles_clique(self):
+        g_family = [self.K(n) for n in range(2, 15)]
+        expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220, 286, 364]
+        self.check_cycle_enumeration_integer_sequence(
+            g_family, expected, chordless=True
+        )
+
+        # directed cliques have as many digons as undirected graphs have edges
+        expected = [(n * n - n) // 2 for n in range(15)]
+        g_family = [self.D(n) for n in range(15)]
+        self.check_cycle_enumeration_integer_sequence(
+            g_family, expected, chordless=True
+        )
+
+
+# These tests might fail with hash randomization since they depend on
+# edge_dfs. For more information, see the comments in:
+#    networkx/algorithms/traversal/tests/test_edgedfs.py
+
+
+class TestFindCycle:
+    @classmethod
+    def setup_class(cls):
+        cls.nodes = [0, 1, 2, 3]
+        cls.edges = [(-1, 0), (0, 1), (1, 0), (1, 0), (2, 1), (3, 1)]
+
+    def test_graph_nocycle(self):
+        G = nx.Graph(self.edges)
+        pytest.raises(nx.exception.NetworkXNoCycle, nx.find_cycle, G, self.nodes)
+
+    def test_graph_cycle(self):
+        G = nx.Graph(self.edges)
+        G.add_edge(2, 0)
+        x = list(nx.find_cycle(G, self.nodes))
+        x_ = [(0, 1), (1, 2), (2, 0)]
+        assert x == x_
+
+    def test_graph_orientation_none(self):
+        G = nx.Graph(self.edges)
+        G.add_edge(2, 0)
+        x = list(nx.find_cycle(G, self.nodes, orientation=None))
+        x_ = [(0, 1), (1, 2), (2, 0)]
+        assert x == x_
+
+    def test_graph_orientation_original(self):
+        G = nx.Graph(self.edges)
+        G.add_edge(2, 0)
+        x = list(nx.find_cycle(G, self.nodes, orientation="original"))
+        x_ = [(0, 1, FORWARD), (1, 2, FORWARD), (2, 0, FORWARD)]
+        assert x == x_
+
+    def test_digraph(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.find_cycle(G, self.nodes))
+        x_ = [(0, 1), (1, 0)]
+        assert x == x_
+
+    def test_digraph_orientation_none(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.find_cycle(G, self.nodes, orientation=None))
+        x_ = [(0, 1), (1, 0)]
+        assert x == x_
+
+    def test_digraph_orientation_original(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.find_cycle(G, self.nodes, orientation="original"))
+        x_ = [(0, 1, FORWARD), (1, 0, FORWARD)]
+        assert x == x_
+
+    def test_multigraph(self):
+        G = nx.MultiGraph(self.edges)
+        x = list(nx.find_cycle(G, self.nodes))
+        x_ = [(0, 1, 0), (1, 0, 1)]  # or (1, 0, 2)
+        # Hash randomization...could be any edge.
+        assert x[0] == x_[0]
+        assert x[1][:2] == x_[1][:2]
+
+    def test_multidigraph(self):
+        G = nx.MultiDiGraph(self.edges)
+        x = list(nx.find_cycle(G, self.nodes))
+        x_ = [(0, 1, 0), (1, 0, 0)]  # (1, 0, 1)
+        assert x[0] == x_[0]
+        assert x[1][:2] == x_[1][:2]
+
+    def test_digraph_ignore(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.find_cycle(G, self.nodes, orientation="ignore"))
+        x_ = [(0, 1, FORWARD), (1, 0, FORWARD)]
+        assert x == x_
+
+    def test_digraph_reverse(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.find_cycle(G, self.nodes, orientation="reverse"))
+        x_ = [(1, 0, REVERSE), (0, 1, REVERSE)]
+        assert x == x_
+
+    def test_multidigraph_ignore(self):
+        G = nx.MultiDiGraph(self.edges)
+        x = list(nx.find_cycle(G, self.nodes, orientation="ignore"))
+        x_ = [(0, 1, 0, FORWARD), (1, 0, 0, FORWARD)]  # or (1, 0, 1, 1)
+        assert x[0] == x_[0]
+        assert x[1][:2] == x_[1][:2]
+        assert x[1][3] == x_[1][3]
+
+    def test_multidigraph_ignore2(self):
+        # Loop traversed an edge while ignoring its orientation.
+        G = nx.MultiDiGraph([(0, 1), (1, 2), (1, 2)])
+        x = list(nx.find_cycle(G, [0, 1, 2], orientation="ignore"))
+        x_ = [(1, 2, 0, FORWARD), (1, 2, 1, REVERSE)]
+        assert x == x_
+
+    def test_multidigraph_original(self):
+        # Node 2 doesn't need to be searched again from visited from 4.
+        # The goal here is to cover the case when 2 to be researched from 4,
+        # when 4 is visited from the first time (so we must make sure that 4
+        # is not visited from 2, and hence, we respect the edge orientation).
+        G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 3), (4, 2)])
+        pytest.raises(
+            nx.exception.NetworkXNoCycle,
+            nx.find_cycle,
+            G,
+            [0, 1, 2, 3, 4],
+            orientation="original",
+        )
+
+    def test_dag(self):
+        G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
+        pytest.raises(
+            nx.exception.NetworkXNoCycle, nx.find_cycle, G, orientation="original"
+        )
+        x = list(nx.find_cycle(G, orientation="ignore"))
+        assert x == [(0, 1, FORWARD), (1, 2, FORWARD), (0, 2, REVERSE)]
+
+    def test_prev_explored(self):
+        # https://github.com/networkx/networkx/issues/2323
+
+        G = nx.DiGraph()
+        G.add_edges_from([(1, 0), (2, 0), (1, 2), (2, 1)])
+        pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G, source=0)
+        x = list(nx.find_cycle(G, 1))
+        x_ = [(1, 2), (2, 1)]
+        assert x == x_
+
+        x = list(nx.find_cycle(G, 2))
+        x_ = [(2, 1), (1, 2)]
+        assert x == x_
+
+        x = list(nx.find_cycle(G))
+        x_ = [(1, 2), (2, 1)]
+        assert x == x_
+
+    def test_no_cycle(self):
+        # https://github.com/networkx/networkx/issues/2439
+
+        G = nx.DiGraph()
+        G.add_edges_from([(1, 2), (2, 0), (3, 1), (3, 2)])
+        pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G, source=0)
+        pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G)
+
+
+def assert_basis_equal(a, b):
+    assert sorted(a) == sorted(b)
+
+
+class TestMinimumCycleBasis:
+    @classmethod
+    def setup_class(cls):
+        T = nx.Graph()
+        nx.add_cycle(T, [1, 2, 3, 4], weight=1)
+        T.add_edge(2, 4, weight=5)
+        cls.diamond_graph = T
+
+    def test_unweighted_diamond(self):
+        mcb = nx.minimum_cycle_basis(self.diamond_graph)
+        assert_basis_equal(mcb, [[2, 4, 1], [3, 4, 2]])
+
+    def test_weighted_diamond(self):
+        mcb = nx.minimum_cycle_basis(self.diamond_graph, weight="weight")
+        assert_basis_equal(mcb, [[2, 4, 1], [4, 3, 2, 1]])
+
+    def test_dimensionality(self):
+        # checks |MCB|=|E|-|V|+|NC|
+        ntrial = 10
+        for seed in range(1234, 1234 + ntrial):
+            rg = nx.erdos_renyi_graph(10, 0.3, seed=seed)
+            nnodes = rg.number_of_nodes()
+            nedges = rg.number_of_edges()
+            ncomp = nx.number_connected_components(rg)
+
+            mcb = nx.minimum_cycle_basis(rg)
+            assert len(mcb) == nedges - nnodes + ncomp
+            check_independent(mcb)
+
+    def test_complete_graph(self):
+        cg = nx.complete_graph(5)
+        mcb = nx.minimum_cycle_basis(cg)
+        assert all(len(cycle) == 3 for cycle in mcb)
+        check_independent(mcb)
+
+    def test_tree_graph(self):
+        tg = nx.balanced_tree(3, 3)
+        assert not nx.minimum_cycle_basis(tg)
+
+    def test_petersen_graph(self):
+        G = nx.petersen_graph()
+        mcb = list(nx.minimum_cycle_basis(G))
+        expected = [
+            [4, 9, 7, 5, 0],
+            [1, 2, 3, 4, 0],
+            [1, 6, 8, 5, 0],
+            [4, 3, 8, 5, 0],
+            [1, 6, 9, 4, 0],
+            [1, 2, 7, 5, 0],
+        ]
+        assert len(mcb) == len(expected)
+        assert all(c in expected for c in mcb)
+
+        # check that order of the nodes is a path
+        for c in mcb:
+            assert all(G.has_edge(u, v) for u, v in nx.utils.pairwise(c, cyclic=True))
+        # check independence of the basis
+        check_independent(mcb)
+
+    def test_gh6787_variable_weighted_complete_graph(self):
+        N = 8
+        cg = nx.complete_graph(N)
+        cg.add_weighted_edges_from([(u, v, 9) for u, v in cg.edges])
+        cg.add_weighted_edges_from([(u, v, 1) for u, v in nx.cycle_graph(N).edges])
+        mcb = nx.minimum_cycle_basis(cg, weight="weight")
+        check_independent(mcb)
+
+    def test_gh6787_and_edge_attribute_names(self):
+        G = nx.cycle_graph(4)
+        G.add_weighted_edges_from([(0, 2, 10), (1, 3, 10)], weight="dist")
+        expected = [[1, 3, 0], [3, 2, 1, 0], [1, 2, 0]]
+        mcb = list(nx.minimum_cycle_basis(G, weight="dist"))
+        assert len(mcb) == len(expected)
+        assert all(c in expected for c in mcb)
+
+        # test not using a weight with weight attributes
+        expected = [[1, 3, 0], [1, 2, 0], [3, 2, 0]]
+        mcb = list(nx.minimum_cycle_basis(G))
+        assert len(mcb) == len(expected)
+        assert all(c in expected for c in mcb)
+
+
+class TestGirth:
+    @pytest.mark.parametrize(
+        ("G", "expected"),
+        (
+            (nx.chvatal_graph(), 4),
+            (nx.tutte_graph(), 4),
+            (nx.petersen_graph(), 5),
+            (nx.heawood_graph(), 6),
+            (nx.pappus_graph(), 6),
+            (nx.random_labeled_tree(10, seed=42), inf),
+            (nx.empty_graph(10), inf),
+            (nx.Graph(chain(cycle_edges(range(5)), cycle_edges(range(6, 10)))), 4),
+            (
+                nx.Graph(
+                    [
+                        (0, 6),
+                        (0, 8),
+                        (0, 9),
+                        (1, 8),
+                        (2, 8),
+                        (2, 9),
+                        (4, 9),
+                        (5, 9),
+                        (6, 8),
+                        (6, 9),
+                        (7, 8),
+                    ]
+                ),
+                3,
+            ),
+        ),
+    )
+    def test_girth(self, G, expected):
+        assert nx.girth(G) == expected
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_d_separation.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_d_separation.py
new file mode 100644
index 00000000..6f629713
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_d_separation.py
@@ -0,0 +1,348 @@
+from itertools import combinations
+
+import pytest
+
+import networkx as nx
+
+
+def path_graph():
+    """Return a path graph of length three."""
+    G = nx.path_graph(3, create_using=nx.DiGraph)
+    G.graph["name"] = "path"
+    nx.freeze(G)
+    return G
+
+
+def fork_graph():
+    """Return a three node fork graph."""
+    G = nx.DiGraph(name="fork")
+    G.add_edges_from([(0, 1), (0, 2)])
+    nx.freeze(G)
+    return G
+
+
+def collider_graph():
+    """Return a collider/v-structure graph with three nodes."""
+    G = nx.DiGraph(name="collider")
+    G.add_edges_from([(0, 2), (1, 2)])
+    nx.freeze(G)
+    return G
+
+
+def naive_bayes_graph():
+    """Return a simply Naive Bayes PGM graph."""
+    G = nx.DiGraph(name="naive_bayes")
+    G.add_edges_from([(0, 1), (0, 2), (0, 3), (0, 4)])
+    nx.freeze(G)
+    return G
+
+
+def asia_graph():
+    """Return the 'Asia' PGM graph."""
+    G = nx.DiGraph(name="asia")
+    G.add_edges_from(
+        [
+            ("asia", "tuberculosis"),
+            ("smoking", "cancer"),
+            ("smoking", "bronchitis"),
+            ("tuberculosis", "either"),
+            ("cancer", "either"),
+            ("either", "xray"),
+            ("either", "dyspnea"),
+            ("bronchitis", "dyspnea"),
+        ]
+    )
+    nx.freeze(G)
+    return G
+
+
+@pytest.fixture(name="path_graph")
+def path_graph_fixture():
+    return path_graph()
+
+
+@pytest.fixture(name="fork_graph")
+def fork_graph_fixture():
+    return fork_graph()
+
+
+@pytest.fixture(name="collider_graph")
+def collider_graph_fixture():
+    return collider_graph()
+
+
+@pytest.fixture(name="naive_bayes_graph")
+def naive_bayes_graph_fixture():
+    return naive_bayes_graph()
+
+
+@pytest.fixture(name="asia_graph")
+def asia_graph_fixture():
+    return asia_graph()
+
+
+@pytest.fixture()
+def large_collider_graph():
+    edge_list = [("A", "B"), ("C", "B"), ("B", "D"), ("D", "E"), ("B", "F"), ("G", "E")]
+    G = nx.DiGraph(edge_list)
+    return G
+
+
+@pytest.fixture()
+def chain_and_fork_graph():
+    edge_list = [("A", "B"), ("B", "C"), ("B", "D"), ("D", "C")]
+    G = nx.DiGraph(edge_list)
+    return G
+
+
+@pytest.fixture()
+def no_separating_set_graph():
+    edge_list = [("A", "B")]
+    G = nx.DiGraph(edge_list)
+    return G
+
+
+@pytest.fixture()
+def large_no_separating_set_graph():
+    edge_list = [("A", "B"), ("C", "A"), ("C", "B")]
+    G = nx.DiGraph(edge_list)
+    return G
+
+
+@pytest.fixture()
+def collider_trek_graph():
+    edge_list = [("A", "B"), ("C", "B"), ("C", "D")]
+    G = nx.DiGraph(edge_list)
+    return G
+
+
+@pytest.mark.parametrize(
+    "graph",
+    [path_graph(), fork_graph(), collider_graph(), naive_bayes_graph(), asia_graph()],
+)
+def test_markov_condition(graph):
+    """Test that the Markov condition holds for each PGM graph."""
+    for node in graph.nodes:
+        parents = set(graph.predecessors(node))
+        non_descendants = graph.nodes - nx.descendants(graph, node) - {node} - parents
+        assert nx.is_d_separator(graph, {node}, non_descendants, parents)
+
+
+def test_path_graph_dsep(path_graph):
+    """Example-based test of d-separation for path_graph."""
+    assert nx.is_d_separator(path_graph, {0}, {2}, {1})
+    assert not nx.is_d_separator(path_graph, {0}, {2}, set())
+
+
+def test_fork_graph_dsep(fork_graph):
+    """Example-based test of d-separation for fork_graph."""
+    assert nx.is_d_separator(fork_graph, {1}, {2}, {0})
+    assert not nx.is_d_separator(fork_graph, {1}, {2}, set())
+
+
+def test_collider_graph_dsep(collider_graph):
+    """Example-based test of d-separation for collider_graph."""
+    assert nx.is_d_separator(collider_graph, {0}, {1}, set())
+    assert not nx.is_d_separator(collider_graph, {0}, {1}, {2})
+
+
+def test_naive_bayes_dsep(naive_bayes_graph):
+    """Example-based test of d-separation for naive_bayes_graph."""
+    for u, v in combinations(range(1, 5), 2):
+        assert nx.is_d_separator(naive_bayes_graph, {u}, {v}, {0})
+        assert not nx.is_d_separator(naive_bayes_graph, {u}, {v}, set())
+
+
+def test_asia_graph_dsep(asia_graph):
+    """Example-based test of d-separation for asia_graph."""
+    assert nx.is_d_separator(
+        asia_graph, {"asia", "smoking"}, {"dyspnea", "xray"}, {"bronchitis", "either"}
+    )
+    assert nx.is_d_separator(
+        asia_graph, {"tuberculosis", "cancer"}, {"bronchitis"}, {"smoking", "xray"}
+    )
+
+
+def test_undirected_graphs_are_not_supported():
+    """
+    Test that undirected graphs are not supported.
+
+    d-separation and its related algorithms do not apply in
+    the case of undirected graphs.
+    """
+    g = nx.path_graph(3, nx.Graph)
+    with pytest.raises(nx.NetworkXNotImplemented):
+        nx.is_d_separator(g, {0}, {1}, {2})
+    with pytest.raises(nx.NetworkXNotImplemented):
+        nx.is_minimal_d_separator(g, {0}, {1}, {2})
+    with pytest.raises(nx.NetworkXNotImplemented):
+        nx.find_minimal_d_separator(g, {0}, {1})
+
+
+def test_cyclic_graphs_raise_error():
+    """
+    Test that cycle graphs should cause erroring.
+
+    This is because PGMs assume a directed acyclic graph.
+    """
+    g = nx.cycle_graph(3, nx.DiGraph)
+    with pytest.raises(nx.NetworkXError):
+        nx.is_d_separator(g, {0}, {1}, {2})
+    with pytest.raises(nx.NetworkXError):
+        nx.find_minimal_d_separator(g, {0}, {1})
+    with pytest.raises(nx.NetworkXError):
+        nx.is_minimal_d_separator(g, {0}, {1}, {2})
+
+
+def test_invalid_nodes_raise_error(asia_graph):
+    """
+    Test that graphs that have invalid nodes passed in raise errors.
+    """
+    # Check both set and node arguments
+    with pytest.raises(nx.NodeNotFound):
+        nx.is_d_separator(asia_graph, {0}, {1}, {2})
+    with pytest.raises(nx.NodeNotFound):
+        nx.is_d_separator(asia_graph, 0, 1, 2)
+    with pytest.raises(nx.NodeNotFound):
+        nx.is_minimal_d_separator(asia_graph, {0}, {1}, {2})
+    with pytest.raises(nx.NodeNotFound):
+        nx.is_minimal_d_separator(asia_graph, 0, 1, 2)
+    with pytest.raises(nx.NodeNotFound):
+        nx.find_minimal_d_separator(asia_graph, {0}, {1})
+    with pytest.raises(nx.NodeNotFound):
+        nx.find_minimal_d_separator(asia_graph, 0, 1)
+
+
+def test_nondisjoint_node_sets_raise_error(collider_graph):
+    """
+    Test that error is raised when node sets aren't disjoint.
+    """
+    with pytest.raises(nx.NetworkXError):
+        nx.is_d_separator(collider_graph, 0, 1, 0)
+    with pytest.raises(nx.NetworkXError):
+        nx.is_d_separator(collider_graph, 0, 2, 0)
+    with pytest.raises(nx.NetworkXError):
+        nx.is_d_separator(collider_graph, 0, 0, 1)
+    with pytest.raises(nx.NetworkXError):
+        nx.is_d_separator(collider_graph, 1, 0, 0)
+    with pytest.raises(nx.NetworkXError):
+        nx.find_minimal_d_separator(collider_graph, 0, 0)
+    with pytest.raises(nx.NetworkXError):
+        nx.find_minimal_d_separator(collider_graph, 0, 1, included=0)
+    with pytest.raises(nx.NetworkXError):
+        nx.find_minimal_d_separator(collider_graph, 1, 0, included=0)
+    with pytest.raises(nx.NetworkXError):
+        nx.is_minimal_d_separator(collider_graph, 0, 0, set())
+    with pytest.raises(nx.NetworkXError):
+        nx.is_minimal_d_separator(collider_graph, 0, 1, set(), included=0)
+    with pytest.raises(nx.NetworkXError):
+        nx.is_minimal_d_separator(collider_graph, 1, 0, set(), included=0)
+
+
+def test_is_minimal_d_separator(
+    large_collider_graph,
+    chain_and_fork_graph,
+    no_separating_set_graph,
+    large_no_separating_set_graph,
+    collider_trek_graph,
+):
+    # Case 1:
+    # create a graph A -> B <- C
+    # B -> D -> E;
+    # B -> F;
+    # G -> E;
+    assert not nx.is_d_separator(large_collider_graph, {"B"}, {"E"}, set())
+
+    # minimal set of the corresponding graph
+    # for B and E should be (D,)
+    Zmin = nx.find_minimal_d_separator(large_collider_graph, "B", "E")
+    # check that the minimal d-separator is a d-separating set
+    assert nx.is_d_separator(large_collider_graph, "B", "E", Zmin)
+    # the minimal separating set should also pass the test for minimality
+    assert nx.is_minimal_d_separator(large_collider_graph, "B", "E", Zmin)
+    # function should also work with set arguments
+    assert nx.is_minimal_d_separator(large_collider_graph, {"A", "B"}, {"G", "E"}, Zmin)
+    assert Zmin == {"D"}
+
+    # Case 2:
+    # create a graph A -> B -> C
+    # B -> D -> C;
+    assert not nx.is_d_separator(chain_and_fork_graph, {"A"}, {"C"}, set())
+    Zmin = nx.find_minimal_d_separator(chain_and_fork_graph, "A", "C")
+
+    # the minimal separating set should pass the test for minimality
+    assert nx.is_minimal_d_separator(chain_and_fork_graph, "A", "C", Zmin)
+    assert Zmin == {"B"}
+    Znotmin = Zmin.union({"D"})
+    assert not nx.is_minimal_d_separator(chain_and_fork_graph, "A", "C", Znotmin)
+
+    # Case 3:
+    # create a graph A -> B
+
+    # there is no m-separating set between A and B at all, so
+    # no minimal m-separating set can exist
+    assert not nx.is_d_separator(no_separating_set_graph, {"A"}, {"B"}, set())
+    assert nx.find_minimal_d_separator(no_separating_set_graph, "A", "B") is None
+
+    # Case 4:
+    # create a graph A -> B with A <- C -> B
+
+    # there is no m-separating set between A and B at all, so
+    # no minimal m-separating set can exist
+    # however, the algorithm will initially propose C as a
+    # minimal (but invalid) separating set
+    assert not nx.is_d_separator(large_no_separating_set_graph, {"A"}, {"B"}, {"C"})
+    assert nx.find_minimal_d_separator(large_no_separating_set_graph, "A", "B") is None
+
+    # Test `included` and `excluded` args
+    # create graph A -> B <- C -> D
+    assert nx.find_minimal_d_separator(collider_trek_graph, "A", "D", included="B") == {
+        "B",
+        "C",
+    }
+    assert (
+        nx.find_minimal_d_separator(
+            collider_trek_graph, "A", "D", included="B", restricted="B"
+        )
+        is None
+    )
+
+
+def test_is_minimal_d_separator_checks_dsep():
+    """Test that is_minimal_d_separator checks for d-separation as well."""
+    g = nx.DiGraph()
+    g.add_edges_from(
+        [
+            ("A", "B"),
+            ("A", "E"),
+            ("B", "C"),
+            ("B", "D"),
+            ("D", "C"),
+            ("D", "F"),
+            ("E", "D"),
+            ("E", "F"),
+        ]
+    )
+
+    assert not nx.is_d_separator(g, {"C"}, {"F"}, {"D"})
+
+    # since {'D'} and {} are not d-separators, we return false
+    assert not nx.is_minimal_d_separator(g, "C", "F", {"D"})
+    assert not nx.is_minimal_d_separator(g, "C", "F", set())
+
+
+def test__reachable(large_collider_graph):
+    reachable = nx.algorithms.d_separation._reachable
+    g = large_collider_graph
+    x = {"F", "D"}
+    ancestors = {"A", "B", "C", "D", "F"}
+    assert reachable(g, x, ancestors, {"B"}) == {"B", "F", "D"}
+    assert reachable(g, x, ancestors, set()) == ancestors
+
+
+def test_deprecations():
+    G = nx.DiGraph([(0, 1), (1, 2)])
+    with pytest.deprecated_call():
+        nx.d_separated(G, 0, 2, {1})
+    with pytest.deprecated_call():
+        z = nx.minimal_d_separator(G, 0, 2)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dag.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dag.py
new file mode 100644
index 00000000..e98a6a0e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dag.py
@@ -0,0 +1,835 @@
+from collections import deque
+from itertools import combinations, permutations
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal, pairwise
+
+
+# Recipe from the itertools documentation.
+def _consume(iterator):
+    "Consume the iterator entirely."
+    # Feed the entire iterator into a zero-length deque.
+    deque(iterator, maxlen=0)
+
+
+class TestDagLongestPath:
+    """Unit tests computing the longest path in a directed acyclic graph."""
+
+    def test_empty(self):
+        G = nx.DiGraph()
+        assert nx.dag_longest_path(G) == []
+
+    def test_unweighted1(self):
+        edges = [(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (3, 7)]
+        G = nx.DiGraph(edges)
+        assert nx.dag_longest_path(G) == [1, 2, 3, 5, 6]
+
+    def test_unweighted2(self):
+        edges = [(1, 2), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)]
+        G = nx.DiGraph(edges)
+        assert nx.dag_longest_path(G) == [1, 2, 3, 4, 5]
+
+    def test_weighted(self):
+        G = nx.DiGraph()
+        edges = [(1, 2, -5), (2, 3, 1), (3, 4, 1), (4, 5, 0), (3, 5, 4), (1, 6, 2)]
+        G.add_weighted_edges_from(edges)
+        assert nx.dag_longest_path(G) == [2, 3, 5]
+
+    def test_undirected_not_implemented(self):
+        G = nx.Graph()
+        pytest.raises(nx.NetworkXNotImplemented, nx.dag_longest_path, G)
+
+    def test_unorderable_nodes(self):
+        """Tests that computing the longest path does not depend on
+        nodes being orderable.
+
+        For more information, see issue #1989.
+
+        """
+        # Create the directed path graph on four nodes in a diamond shape,
+        # with nodes represented as (unorderable) Python objects.
+        nodes = [object() for n in range(4)]
+        G = nx.DiGraph()
+        G.add_edge(nodes[0], nodes[1])
+        G.add_edge(nodes[0], nodes[2])
+        G.add_edge(nodes[2], nodes[3])
+        G.add_edge(nodes[1], nodes[3])
+
+        # this will raise NotImplementedError when nodes need to be ordered
+        nx.dag_longest_path(G)
+
+    def test_multigraph_unweighted(self):
+        edges = [(1, 2), (2, 3), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)]
+        G = nx.MultiDiGraph(edges)
+        assert nx.dag_longest_path(G) == [1, 2, 3, 4, 5]
+
+    def test_multigraph_weighted(self):
+        G = nx.MultiDiGraph()
+        edges = [
+            (1, 2, 2),
+            (2, 3, 2),
+            (1, 3, 1),
+            (1, 3, 5),
+            (1, 3, 2),
+        ]
+        G.add_weighted_edges_from(edges)
+        assert nx.dag_longest_path(G) == [1, 3]
+
+    def test_multigraph_weighted_default_weight(self):
+        G = nx.MultiDiGraph([(1, 2), (2, 3)])  # Unweighted edges
+        G.add_weighted_edges_from([(1, 3, 1), (1, 3, 5), (1, 3, 2)])
+
+        # Default value for default weight is 1
+        assert nx.dag_longest_path(G) == [1, 3]
+        assert nx.dag_longest_path(G, default_weight=3) == [1, 2, 3]
+
+
+class TestDagLongestPathLength:
+    """Unit tests for computing the length of a longest path in a
+    directed acyclic graph.
+
+    """
+
+    def test_unweighted(self):
+        edges = [(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (5, 7)]
+        G = nx.DiGraph(edges)
+        assert nx.dag_longest_path_length(G) == 4
+
+        edges = [(1, 2), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)]
+        G = nx.DiGraph(edges)
+        assert nx.dag_longest_path_length(G) == 4
+
+        # test degenerate graphs
+        G = nx.DiGraph()
+        G.add_node(1)
+        assert nx.dag_longest_path_length(G) == 0
+
+    def test_undirected_not_implemented(self):
+        G = nx.Graph()
+        pytest.raises(nx.NetworkXNotImplemented, nx.dag_longest_path_length, G)
+
+    def test_weighted(self):
+        edges = [(1, 2, -5), (2, 3, 1), (3, 4, 1), (4, 5, 0), (3, 5, 4), (1, 6, 2)]
+        G = nx.DiGraph()
+        G.add_weighted_edges_from(edges)
+        assert nx.dag_longest_path_length(G) == 5
+
+    def test_multigraph_unweighted(self):
+        edges = [(1, 2), (2, 3), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)]
+        G = nx.MultiDiGraph(edges)
+        assert nx.dag_longest_path_length(G) == 4
+
+    def test_multigraph_weighted(self):
+        G = nx.MultiDiGraph()
+        edges = [
+            (1, 2, 2),
+            (2, 3, 2),
+            (1, 3, 1),
+            (1, 3, 5),
+            (1, 3, 2),
+        ]
+        G.add_weighted_edges_from(edges)
+        assert nx.dag_longest_path_length(G) == 5
+
+
+class TestDAG:
+    @classmethod
+    def setup_class(cls):
+        pass
+
+    def test_topological_sort1(self):
+        DG = nx.DiGraph([(1, 2), (1, 3), (2, 3)])
+
+        for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]:
+            assert tuple(algorithm(DG)) == (1, 2, 3)
+
+        DG.add_edge(3, 2)
+
+        for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]:
+            pytest.raises(nx.NetworkXUnfeasible, _consume, algorithm(DG))
+
+        DG.remove_edge(2, 3)
+
+        for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]:
+            assert tuple(algorithm(DG)) == (1, 3, 2)
+
+        DG.remove_edge(3, 2)
+
+        assert tuple(nx.topological_sort(DG)) in {(1, 2, 3), (1, 3, 2)}
+        assert tuple(nx.lexicographical_topological_sort(DG)) == (1, 2, 3)
+
+    def test_is_directed_acyclic_graph(self):
+        G = nx.generators.complete_graph(2)
+        assert not nx.is_directed_acyclic_graph(G)
+        assert not nx.is_directed_acyclic_graph(G.to_directed())
+        assert not nx.is_directed_acyclic_graph(nx.Graph([(3, 4), (4, 5)]))
+        assert nx.is_directed_acyclic_graph(nx.DiGraph([(3, 4), (4, 5)]))
+
+    def test_topological_sort2(self):
+        DG = nx.DiGraph(
+            {
+                1: [2],
+                2: [3],
+                3: [4],
+                4: [5],
+                5: [1],
+                11: [12],
+                12: [13],
+                13: [14],
+                14: [15],
+            }
+        )
+        pytest.raises(nx.NetworkXUnfeasible, _consume, nx.topological_sort(DG))
+
+        assert not nx.is_directed_acyclic_graph(DG)
+
+        DG.remove_edge(1, 2)
+        _consume(nx.topological_sort(DG))
+        assert nx.is_directed_acyclic_graph(DG)
+
+    def test_topological_sort3(self):
+        DG = nx.DiGraph()
+        DG.add_edges_from([(1, i) for i in range(2, 5)])
+        DG.add_edges_from([(2, i) for i in range(5, 9)])
+        DG.add_edges_from([(6, i) for i in range(9, 12)])
+        DG.add_edges_from([(4, i) for i in range(12, 15)])
+
+        def validate(order):
+            assert isinstance(order, list)
+            assert set(order) == set(DG)
+            for u, v in combinations(order, 2):
+                assert not nx.has_path(DG, v, u)
+
+        validate(list(nx.topological_sort(DG)))
+
+        DG.add_edge(14, 1)
+        pytest.raises(nx.NetworkXUnfeasible, _consume, nx.topological_sort(DG))
+
+    def test_topological_sort4(self):
+        G = nx.Graph()
+        G.add_edge(1, 2)
+        # Only directed graphs can be topologically sorted.
+        pytest.raises(nx.NetworkXError, _consume, nx.topological_sort(G))
+
+    def test_topological_sort5(self):
+        G = nx.DiGraph()
+        G.add_edge(0, 1)
+        assert list(nx.topological_sort(G)) == [0, 1]
+
+    def test_topological_sort6(self):
+        for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]:
+
+            def runtime_error():
+                DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+                first = True
+                for x in algorithm(DG):
+                    if first:
+                        first = False
+                        DG.add_edge(5 - x, 5)
+
+            def unfeasible_error():
+                DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+                first = True
+                for x in algorithm(DG):
+                    if first:
+                        first = False
+                        DG.remove_node(4)
+
+            def runtime_error2():
+                DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+                first = True
+                for x in algorithm(DG):
+                    if first:
+                        first = False
+                        DG.remove_node(2)
+
+            pytest.raises(RuntimeError, runtime_error)
+            pytest.raises(RuntimeError, runtime_error2)
+            pytest.raises(nx.NetworkXUnfeasible, unfeasible_error)
+
+    def test_all_topological_sorts_1(self):
+        DG = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5)])
+        assert list(nx.all_topological_sorts(DG)) == [[1, 2, 3, 4, 5]]
+
+    def test_all_topological_sorts_2(self):
+        DG = nx.DiGraph([(1, 3), (2, 1), (2, 4), (4, 3), (4, 5)])
+        assert sorted(nx.all_topological_sorts(DG)) == [
+            [2, 1, 4, 3, 5],
+            [2, 1, 4, 5, 3],
+            [2, 4, 1, 3, 5],
+            [2, 4, 1, 5, 3],
+            [2, 4, 5, 1, 3],
+        ]
+
+    def test_all_topological_sorts_3(self):
+        def unfeasible():
+            DG = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 2), (4, 5)])
+            # convert to list to execute generator
+            list(nx.all_topological_sorts(DG))
+
+        def not_implemented():
+            G = nx.Graph([(1, 2), (2, 3)])
+            # convert to list to execute generator
+            list(nx.all_topological_sorts(G))
+
+        def not_implemented_2():
+            G = nx.MultiGraph([(1, 2), (1, 2), (2, 3)])
+            list(nx.all_topological_sorts(G))
+
+        pytest.raises(nx.NetworkXUnfeasible, unfeasible)
+        pytest.raises(nx.NetworkXNotImplemented, not_implemented)
+        pytest.raises(nx.NetworkXNotImplemented, not_implemented_2)
+
+    def test_all_topological_sorts_4(self):
+        DG = nx.DiGraph()
+        for i in range(7):
+            DG.add_node(i)
+        assert sorted(map(list, permutations(DG.nodes))) == sorted(
+            nx.all_topological_sorts(DG)
+        )
+
+    def test_all_topological_sorts_multigraph_1(self):
+        DG = nx.MultiDiGraph([(1, 2), (1, 2), (2, 3), (3, 4), (3, 5), (3, 5), (3, 5)])
+        assert sorted(nx.all_topological_sorts(DG)) == sorted(
+            [[1, 2, 3, 4, 5], [1, 2, 3, 5, 4]]
+        )
+
+    def test_all_topological_sorts_multigraph_2(self):
+        N = 9
+        edges = []
+        for i in range(1, N):
+            edges.extend([(i, i + 1)] * i)
+        DG = nx.MultiDiGraph(edges)
+        assert list(nx.all_topological_sorts(DG)) == [list(range(1, N + 1))]
+
+    def test_ancestors(self):
+        G = nx.DiGraph()
+        ancestors = nx.algorithms.dag.ancestors
+        G.add_edges_from([(1, 2), (1, 3), (4, 2), (4, 3), (4, 5), (2, 6), (5, 6)])
+        assert ancestors(G, 6) == {1, 2, 4, 5}
+        assert ancestors(G, 3) == {1, 4}
+        assert ancestors(G, 1) == set()
+        pytest.raises(nx.NetworkXError, ancestors, G, 8)
+
+    def test_descendants(self):
+        G = nx.DiGraph()
+        descendants = nx.algorithms.dag.descendants
+        G.add_edges_from([(1, 2), (1, 3), (4, 2), (4, 3), (4, 5), (2, 6), (5, 6)])
+        assert descendants(G, 1) == {2, 3, 6}
+        assert descendants(G, 4) == {2, 3, 5, 6}
+        assert descendants(G, 3) == set()
+        pytest.raises(nx.NetworkXError, descendants, G, 8)
+
+    def test_transitive_closure(self):
+        G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        G = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
+        solution = [(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(sorted(nx.transitive_closure(G).edges()), soln)
+
+        G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
+
+        G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
+
+        G = nx.MultiDiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
+
+        # test if edge data is copied
+        G = nx.DiGraph([(1, 2, {"a": 3}), (2, 3, {"b": 0}), (3, 4)])
+        H = nx.transitive_closure(G)
+        for u, v in G.edges():
+            assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
+
+        k = 10
+        G = nx.DiGraph((i, i + 1, {"f": "b", "weight": i}) for i in range(k))
+        H = nx.transitive_closure(G)
+        for u, v in G.edges():
+            assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
+
+        G = nx.Graph()
+        with pytest.raises(nx.NetworkXError):
+            nx.transitive_closure(G, reflexive="wrong input")
+
+    def test_reflexive_transitive_closure(self):
+        G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
+        assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
+
+        G = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
+        assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
+
+        G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
+        solution = sorted([(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1)])
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(sorted(nx.transitive_closure(G).edges()), soln)
+        assert edges_equal(sorted(nx.transitive_closure(G, False).edges()), soln)
+        assert edges_equal(sorted(nx.transitive_closure(G, None).edges()), solution)
+        assert edges_equal(sorted(nx.transitive_closure(G, True).edges()), soln)
+
+        G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
+        assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
+
+        G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
+        assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
+
+        G = nx.MultiDiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        soln = sorted(solution + [(n, n) for n in G])
+        assert edges_equal(nx.transitive_closure(G).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
+        assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
+        assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
+
+    def test_transitive_closure_dag(self):
+        G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+        transitive_closure = nx.algorithms.dag.transitive_closure_dag
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+        assert edges_equal(transitive_closure(G).edges(), solution)
+        G = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
+        solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
+        assert edges_equal(transitive_closure(G).edges(), solution)
+        G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+        pytest.raises(nx.NetworkXNotImplemented, transitive_closure, G)
+
+        # test if edge data is copied
+        G = nx.DiGraph([(1, 2, {"a": 3}), (2, 3, {"b": 0}), (3, 4)])
+        H = transitive_closure(G)
+        for u, v in G.edges():
+            assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
+
+        k = 10
+        G = nx.DiGraph((i, i + 1, {"foo": "bar", "weight": i}) for i in range(k))
+        H = transitive_closure(G)
+        for u, v in G.edges():
+            assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
+
+    def test_transitive_reduction(self):
+        G = nx.DiGraph([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)])
+        transitive_reduction = nx.algorithms.dag.transitive_reduction
+        solution = [(1, 2), (2, 3), (3, 4)]
+        assert edges_equal(transitive_reduction(G).edges(), solution)
+        G = nx.DiGraph([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)])
+        transitive_reduction = nx.algorithms.dag.transitive_reduction
+        solution = [(1, 2), (2, 3), (2, 4)]
+        assert edges_equal(transitive_reduction(G).edges(), solution)
+        G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+        pytest.raises(nx.NetworkXNotImplemented, transitive_reduction, G)
+
+    def _check_antichains(self, solution, result):
+        sol = [frozenset(a) for a in solution]
+        res = [frozenset(a) for a in result]
+        assert set(sol) == set(res)
+
+    def test_antichains(self):
+        antichains = nx.algorithms.dag.antichains
+        G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+        solution = [[], [4], [3], [2], [1]]
+        self._check_antichains(list(antichains(G)), solution)
+        G = nx.DiGraph([(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (5, 7)])
+        solution = [
+            [],
+            [4],
+            [7],
+            [7, 4],
+            [6],
+            [6, 4],
+            [6, 7],
+            [6, 7, 4],
+            [5],
+            [5, 4],
+            [3],
+            [3, 4],
+            [2],
+            [1],
+        ]
+        self._check_antichains(list(antichains(G)), solution)
+        G = nx.DiGraph([(1, 2), (1, 3), (3, 4), (3, 5), (5, 6)])
+        solution = [
+            [],
+            [6],
+            [5],
+            [4],
+            [4, 6],
+            [4, 5],
+            [3],
+            [2],
+            [2, 6],
+            [2, 5],
+            [2, 4],
+            [2, 4, 6],
+            [2, 4, 5],
+            [2, 3],
+            [1],
+        ]
+        self._check_antichains(list(antichains(G)), solution)
+        G = nx.DiGraph({0: [1, 2], 1: [4], 2: [3], 3: [4]})
+        solution = [[], [4], [3], [2], [1], [1, 3], [1, 2], [0]]
+        self._check_antichains(list(antichains(G)), solution)
+        G = nx.DiGraph()
+        self._check_antichains(list(antichains(G)), [[]])
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2])
+        solution = [[], [0], [1], [1, 0], [2], [2, 0], [2, 1], [2, 1, 0]]
+        self._check_antichains(list(antichains(G)), solution)
+
+        def f(x):
+            return list(antichains(x))
+
+        G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+        pytest.raises(nx.NetworkXNotImplemented, f, G)
+        G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
+        pytest.raises(nx.NetworkXUnfeasible, f, G)
+
+    def test_lexicographical_topological_sort(self):
+        G = nx.DiGraph([(1, 2), (2, 3), (1, 4), (1, 5), (2, 6)])
+        assert list(nx.lexicographical_topological_sort(G)) == [1, 2, 3, 4, 5, 6]
+        assert list(nx.lexicographical_topological_sort(G, key=lambda x: x)) == [
+            1,
+            2,
+            3,
+            4,
+            5,
+            6,
+        ]
+        assert list(nx.lexicographical_topological_sort(G, key=lambda x: -x)) == [
+            1,
+            5,
+            4,
+            2,
+            6,
+            3,
+        ]
+
+    def test_lexicographical_topological_sort2(self):
+        """
+        Check the case of two or more nodes with same key value.
+        Want to avoid exception raised due to comparing nodes directly.
+        See Issue #3493
+        """
+
+        class Test_Node:
+            def __init__(self, n):
+                self.label = n
+                self.priority = 1
+
+            def __repr__(self):
+                return f"Node({self.label})"
+
+        def sorting_key(node):
+            return node.priority
+
+        test_nodes = [Test_Node(n) for n in range(4)]
+        G = nx.DiGraph()
+        edges = [(0, 1), (0, 2), (0, 3), (2, 3)]
+        G.add_edges_from((test_nodes[a], test_nodes[b]) for a, b in edges)
+
+        sorting = list(nx.lexicographical_topological_sort(G, key=sorting_key))
+        assert sorting == test_nodes
+
+
+def test_topological_generations():
+    G = nx.DiGraph(
+        {1: [2, 3], 2: [4, 5], 3: [7], 4: [], 5: [6, 7], 6: [], 7: []}
+    ).reverse()
+    # order within each generation is inconsequential
+    generations = [sorted(gen) for gen in nx.topological_generations(G)]
+    expected = [[4, 6, 7], [3, 5], [2], [1]]
+    assert generations == expected
+
+    MG = nx.MultiDiGraph(G.edges)
+    MG.add_edge(2, 1)
+    generations = [sorted(gen) for gen in nx.topological_generations(MG)]
+    assert generations == expected
+
+
+def test_topological_generations_empty():
+    G = nx.DiGraph()
+    assert list(nx.topological_generations(G)) == []
+
+
+def test_topological_generations_cycle():
+    G = nx.DiGraph([[2, 1], [3, 1], [1, 2]])
+    with pytest.raises(nx.NetworkXUnfeasible):
+        list(nx.topological_generations(G))
+
+
+def test_is_aperiodic_cycle():
+    G = nx.DiGraph()
+    nx.add_cycle(G, [1, 2, 3, 4])
+    assert not nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_cycle2():
+    G = nx.DiGraph()
+    nx.add_cycle(G, [1, 2, 3, 4])
+    nx.add_cycle(G, [3, 4, 5, 6, 7])
+    assert nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_cycle3():
+    G = nx.DiGraph()
+    nx.add_cycle(G, [1, 2, 3, 4])
+    nx.add_cycle(G, [3, 4, 5, 6])
+    assert not nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_cycle4():
+    G = nx.DiGraph()
+    nx.add_cycle(G, [1, 2, 3, 4])
+    G.add_edge(1, 3)
+    assert nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_selfloop():
+    G = nx.DiGraph()
+    nx.add_cycle(G, [1, 2, 3, 4])
+    G.add_edge(1, 1)
+    assert nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_undirected_raises():
+    G = nx.Graph()
+    pytest.raises(nx.NetworkXError, nx.is_aperiodic, G)
+
+
+def test_is_aperiodic_empty_graph():
+    G = nx.empty_graph(create_using=nx.DiGraph)
+    with pytest.raises(nx.NetworkXPointlessConcept, match="Graph has no nodes."):
+        nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_bipartite():
+    # Bipartite graph
+    G = nx.DiGraph(nx.davis_southern_women_graph())
+    assert not nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_rary_tree():
+    G = nx.full_rary_tree(3, 27, create_using=nx.DiGraph())
+    assert not nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_disconnected():
+    # disconnected graph
+    G = nx.DiGraph()
+    nx.add_cycle(G, [1, 2, 3, 4])
+    nx.add_cycle(G, [5, 6, 7, 8])
+    assert not nx.is_aperiodic(G)
+    G.add_edge(1, 3)
+    G.add_edge(5, 7)
+    assert nx.is_aperiodic(G)
+
+
+def test_is_aperiodic_disconnected2():
+    G = nx.DiGraph()
+    nx.add_cycle(G, [0, 1, 2])
+    G.add_edge(3, 3)
+    assert not nx.is_aperiodic(G)
+
+
+class TestDagToBranching:
+    """Unit tests for the :func:`networkx.dag_to_branching` function."""
+
+    def test_single_root(self):
+        """Tests that a directed acyclic graph with a single degree
+        zero node produces an arborescence.
+
+        """
+        G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3)])
+        B = nx.dag_to_branching(G)
+        expected = nx.DiGraph([(0, 1), (1, 3), (0, 2), (2, 4)])
+        assert nx.is_arborescence(B)
+        assert nx.is_isomorphic(B, expected)
+
+    def test_multiple_roots(self):
+        """Tests that a directed acyclic graph with multiple degree zero
+        nodes creates an arborescence with multiple (weakly) connected
+        components.
+
+        """
+        G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3), (5, 2)])
+        B = nx.dag_to_branching(G)
+        expected = nx.DiGraph([(0, 1), (1, 3), (0, 2), (2, 4), (5, 6), (6, 7)])
+        assert nx.is_branching(B)
+        assert not nx.is_arborescence(B)
+        assert nx.is_isomorphic(B, expected)
+
+    # # Attributes are not copied by this function. If they were, this would
+    # # be a good test to uncomment.
+    # def test_copy_attributes(self):
+    #     """Tests that node attributes are copied in the branching."""
+    #     G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3)])
+    #     for v in G:
+    #         G.node[v]['label'] = str(v)
+    #     B = nx.dag_to_branching(G)
+    #     # Determine the root node of the branching.
+    #     root = next(v for v, d in B.in_degree() if d == 0)
+    #     assert_equal(B.node[root]['label'], '0')
+    #     children = B[root]
+    #     # Get the left and right children, nodes 1 and 2, respectively.
+    #     left, right = sorted(children, key=lambda v: B.node[v]['label'])
+    #     assert_equal(B.node[left]['label'], '1')
+    #     assert_equal(B.node[right]['label'], '2')
+    #     # Get the left grandchild.
+    #     children = B[left]
+    #     assert_equal(len(children), 1)
+    #     left_grandchild = arbitrary_element(children)
+    #     assert_equal(B.node[left_grandchild]['label'], '3')
+    #     # Get the right grandchild.
+    #     children = B[right]
+    #     assert_equal(len(children), 1)
+    #     right_grandchild = arbitrary_element(children)
+    #     assert_equal(B.node[right_grandchild]['label'], '3')
+
+    def test_already_arborescence(self):
+        """Tests that a directed acyclic graph that is already an
+        arborescence produces an isomorphic arborescence as output.
+
+        """
+        A = nx.balanced_tree(2, 2, create_using=nx.DiGraph())
+        B = nx.dag_to_branching(A)
+        assert nx.is_isomorphic(A, B)
+
+    def test_already_branching(self):
+        """Tests that a directed acyclic graph that is already a
+        branching produces an isomorphic branching as output.
+
+        """
+        T1 = nx.balanced_tree(2, 2, create_using=nx.DiGraph())
+        T2 = nx.balanced_tree(2, 2, create_using=nx.DiGraph())
+        G = nx.disjoint_union(T1, T2)
+        B = nx.dag_to_branching(G)
+        assert nx.is_isomorphic(G, B)
+
+    def test_not_acyclic(self):
+        """Tests that a non-acyclic graph causes an exception."""
+        with pytest.raises(nx.HasACycle):
+            G = nx.DiGraph(pairwise("abc", cyclic=True))
+            nx.dag_to_branching(G)
+
+    def test_undirected(self):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            nx.dag_to_branching(nx.Graph())
+
+    def test_multigraph(self):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            nx.dag_to_branching(nx.MultiGraph())
+
+    def test_multidigraph(self):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            nx.dag_to_branching(nx.MultiDiGraph())
+
+
+def test_ancestors_descendants_undirected():
+    """Regression test to ensure ancestors and descendants work as expected on
+    undirected graphs."""
+    G = nx.path_graph(5)
+    nx.ancestors(G, 2) == nx.descendants(G, 2) == {0, 1, 3, 4}
+
+
+def test_compute_v_structures_raise():
+    G = nx.Graph()
+    with pytest.raises(nx.NetworkXNotImplemented, match="for undirected type"):
+        nx.compute_v_structures(G)
+
+
+def test_compute_v_structures():
+    edges = [(0, 1), (0, 2), (3, 2)]
+    G = nx.DiGraph(edges)
+
+    v_structs = set(nx.compute_v_structures(G))
+    assert len(v_structs) == 1
+    assert (0, 2, 3) in v_structs
+
+    edges = [("A", "B"), ("C", "B"), ("B", "D"), ("D", "E"), ("G", "E")]
+    G = nx.DiGraph(edges)
+    v_structs = set(nx.compute_v_structures(G))
+    assert len(v_structs) == 2
+
+
+def test_compute_v_structures_deprecated():
+    G = nx.DiGraph()
+    with pytest.deprecated_call():
+        nx.compute_v_structures(G)
+
+
+def test_v_structures_raise():
+    G = nx.Graph()
+    with pytest.raises(nx.NetworkXNotImplemented, match="for undirected type"):
+        nx.dag.v_structures(G)
+
+
+@pytest.mark.parametrize(
+    ("edgelist", "expected"),
+    (
+        (
+            [(0, 1), (0, 2), (3, 2)],
+            {(0, 2, 3)},
+        ),
+        (
+            [("A", "B"), ("C", "B"), ("D", "G"), ("D", "E"), ("G", "E")],
+            {("A", "B", "C")},
+        ),
+        ([(0, 1), (2, 1), (0, 2)], set()),  # adjacent parents case: see gh-7385
+    ),
+)
+def test_v_structures(edgelist, expected):
+    G = nx.DiGraph(edgelist)
+    v_structs = set(nx.dag.v_structures(G))
+    assert v_structs == expected
+
+
+def test_colliders_raise():
+    G = nx.Graph()
+    with pytest.raises(nx.NetworkXNotImplemented, match="for undirected type"):
+        nx.dag.colliders(G)
+
+
+@pytest.mark.parametrize(
+    ("edgelist", "expected"),
+    (
+        (
+            [(0, 1), (0, 2), (3, 2)],
+            {(0, 2, 3)},
+        ),
+        (
+            [("A", "B"), ("C", "B"), ("D", "G"), ("D", "E"), ("G", "E")],
+            {("A", "B", "C"), ("D", "E", "G")},
+        ),
+    ),
+)
+def test_colliders(edgelist, expected):
+    G = nx.DiGraph(edgelist)
+    colliders = set(nx.dag.colliders(G))
+    assert colliders == expected
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_measures.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_measures.py
new file mode 100644
index 00000000..0b3840fd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_measures.py
@@ -0,0 +1,774 @@
+import math
+from random import Random
+
+import pytest
+
+import networkx as nx
+from networkx import convert_node_labels_to_integers as cnlti
+from networkx.algorithms.distance_measures import _extrema_bounding
+
+
+def test__extrema_bounding_invalid_compute_kwarg():
+    G = nx.path_graph(3)
+    with pytest.raises(ValueError, match="compute must be one of"):
+        _extrema_bounding(G, compute="spam")
+
+
+class TestDistance:
+    def setup_method(self):
+        G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted")
+        self.G = G
+
+    def test_eccentricity(self):
+        assert nx.eccentricity(self.G, 1) == 6
+        e = nx.eccentricity(self.G)
+        assert e[1] == 6
+
+        sp = dict(nx.shortest_path_length(self.G))
+        e = nx.eccentricity(self.G, sp=sp)
+        assert e[1] == 6
+
+        e = nx.eccentricity(self.G, v=1)
+        assert e == 6
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1])
+        assert e[1] == 6
+        e = nx.eccentricity(self.G, v=[1, 2])
+        assert e[1] == 6
+
+        # test against graph with one node
+        G = nx.path_graph(1)
+        e = nx.eccentricity(G)
+        assert e[0] == 0
+        e = nx.eccentricity(G, v=0)
+        assert e == 0
+        pytest.raises(nx.NetworkXError, nx.eccentricity, G, 1)
+
+        # test against empty graph
+        G = nx.empty_graph()
+        e = nx.eccentricity(G)
+        assert e == {}
+
+    def test_diameter(self):
+        assert nx.diameter(self.G) == 6
+
+    def test_harmonic_diameter(self):
+        assert abs(nx.harmonic_diameter(self.G) - 2.0477815699658715) < 1e-12
+
+    def test_harmonic_diameter_empty(self):
+        assert math.isnan(nx.harmonic_diameter(nx.empty_graph()))
+
+    def test_harmonic_diameter_single_node(self):
+        assert math.isnan(nx.harmonic_diameter(nx.empty_graph(1)))
+
+    def test_harmonic_diameter_discrete(self):
+        assert math.isinf(nx.harmonic_diameter(nx.empty_graph(3)))
+
+    def test_harmonic_diameter_not_strongly_connected(self):
+        DG = nx.DiGraph()
+        DG.add_edge(0, 1)
+        assert nx.harmonic_diameter(DG) == 2
+
+    def test_radius(self):
+        assert nx.radius(self.G) == 4
+
+    def test_periphery(self):
+        assert set(nx.periphery(self.G)) == {1, 4, 13, 16}
+
+    def test_center(self):
+        assert set(nx.center(self.G)) == {6, 7, 10, 11}
+
+    def test_bound_diameter(self):
+        assert nx.diameter(self.G, usebounds=True) == 6
+
+    def test_bound_radius(self):
+        assert nx.radius(self.G, usebounds=True) == 4
+
+    def test_bound_periphery(self):
+        result = {1, 4, 13, 16}
+        assert set(nx.periphery(self.G, usebounds=True)) == result
+
+    def test_bound_center(self):
+        result = {6, 7, 10, 11}
+        assert set(nx.center(self.G, usebounds=True)) == result
+
+    def test_radius_exception(self):
+        G = nx.Graph()
+        G.add_edge(1, 2)
+        G.add_edge(3, 4)
+        pytest.raises(nx.NetworkXError, nx.diameter, G)
+
+    def test_eccentricity_infinite(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.Graph([(1, 2), (3, 4)])
+            e = nx.eccentricity(G)
+
+    def test_eccentricity_undirected_not_connected(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.Graph([(1, 2), (3, 4)])
+            e = nx.eccentricity(G, sp=1)
+
+    def test_eccentricity_directed_weakly_connected(self):
+        with pytest.raises(nx.NetworkXError):
+            DG = nx.DiGraph([(1, 2), (1, 3)])
+            nx.eccentricity(DG)
+
+
+class TestWeightedDistance:
+    def setup_method(self):
+        G = nx.Graph()
+        G.add_edge(0, 1, weight=0.6, cost=0.6, high_cost=6)
+        G.add_edge(0, 2, weight=0.2, cost=0.2, high_cost=2)
+        G.add_edge(2, 3, weight=0.1, cost=0.1, high_cost=1)
+        G.add_edge(2, 4, weight=0.7, cost=0.7, high_cost=7)
+        G.add_edge(2, 5, weight=0.9, cost=0.9, high_cost=9)
+        G.add_edge(1, 5, weight=0.3, cost=0.3, high_cost=3)
+        self.G = G
+        self.weight_fn = lambda v, u, e: 2
+
+    def test_eccentricity_weight_None(self):
+        assert nx.eccentricity(self.G, 1, weight=None) == 3
+        e = nx.eccentricity(self.G, weight=None)
+        assert e[1] == 3
+
+        e = nx.eccentricity(self.G, v=1, weight=None)
+        assert e == 3
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1], weight=None)
+        assert e[1] == 3
+        e = nx.eccentricity(self.G, v=[1, 2], weight=None)
+        assert e[1] == 3
+
+    def test_eccentricity_weight_attr(self):
+        assert nx.eccentricity(self.G, 1, weight="weight") == 1.5
+        e = nx.eccentricity(self.G, weight="weight")
+        assert (
+            e
+            == nx.eccentricity(self.G, weight="cost")
+            != nx.eccentricity(self.G, weight="high_cost")
+        )
+        assert e[1] == 1.5
+
+        e = nx.eccentricity(self.G, v=1, weight="weight")
+        assert e == 1.5
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1], weight="weight")
+        assert e[1] == 1.5
+        e = nx.eccentricity(self.G, v=[1, 2], weight="weight")
+        assert e[1] == 1.5
+
+    def test_eccentricity_weight_fn(self):
+        assert nx.eccentricity(self.G, 1, weight=self.weight_fn) == 6
+        e = nx.eccentricity(self.G, weight=self.weight_fn)
+        assert e[1] == 6
+
+        e = nx.eccentricity(self.G, v=1, weight=self.weight_fn)
+        assert e == 6
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1], weight=self.weight_fn)
+        assert e[1] == 6
+        e = nx.eccentricity(self.G, v=[1, 2], weight=self.weight_fn)
+        assert e[1] == 6
+
+    def test_diameter_weight_None(self):
+        assert nx.diameter(self.G, weight=None) == 3
+
+    def test_diameter_weight_attr(self):
+        assert (
+            nx.diameter(self.G, weight="weight")
+            == nx.diameter(self.G, weight="cost")
+            == 1.6
+            != nx.diameter(self.G, weight="high_cost")
+        )
+
+    def test_diameter_weight_fn(self):
+        assert nx.diameter(self.G, weight=self.weight_fn) == 6
+
+    def test_radius_weight_None(self):
+        assert pytest.approx(nx.radius(self.G, weight=None)) == 2
+
+    def test_radius_weight_attr(self):
+        assert (
+            pytest.approx(nx.radius(self.G, weight="weight"))
+            == pytest.approx(nx.radius(self.G, weight="cost"))
+            == 0.9
+            != nx.radius(self.G, weight="high_cost")
+        )
+
+    def test_radius_weight_fn(self):
+        assert nx.radius(self.G, weight=self.weight_fn) == 4
+
+    def test_periphery_weight_None(self):
+        for v in set(nx.periphery(self.G, weight=None)):
+            assert nx.eccentricity(self.G, v, weight=None) == nx.diameter(
+                self.G, weight=None
+            )
+
+    def test_periphery_weight_attr(self):
+        periphery = set(nx.periphery(self.G, weight="weight"))
+        assert (
+            periphery
+            == set(nx.periphery(self.G, weight="cost"))
+            == set(nx.periphery(self.G, weight="high_cost"))
+        )
+        for v in periphery:
+            assert (
+                nx.eccentricity(self.G, v, weight="high_cost")
+                != nx.eccentricity(self.G, v, weight="weight")
+                == nx.eccentricity(self.G, v, weight="cost")
+                == nx.diameter(self.G, weight="weight")
+                == nx.diameter(self.G, weight="cost")
+                != nx.diameter(self.G, weight="high_cost")
+            )
+            assert nx.eccentricity(self.G, v, weight="high_cost") == nx.diameter(
+                self.G, weight="high_cost"
+            )
+
+    def test_periphery_weight_fn(self):
+        for v in set(nx.periphery(self.G, weight=self.weight_fn)):
+            assert nx.eccentricity(self.G, v, weight=self.weight_fn) == nx.diameter(
+                self.G, weight=self.weight_fn
+            )
+
+    def test_center_weight_None(self):
+        for v in set(nx.center(self.G, weight=None)):
+            assert pytest.approx(nx.eccentricity(self.G, v, weight=None)) == nx.radius(
+                self.G, weight=None
+            )
+
+    def test_center_weight_attr(self):
+        center = set(nx.center(self.G, weight="weight"))
+        assert (
+            center
+            == set(nx.center(self.G, weight="cost"))
+            != set(nx.center(self.G, weight="high_cost"))
+        )
+        for v in center:
+            assert (
+                nx.eccentricity(self.G, v, weight="high_cost")
+                != pytest.approx(nx.eccentricity(self.G, v, weight="weight"))
+                == pytest.approx(nx.eccentricity(self.G, v, weight="cost"))
+                == nx.radius(self.G, weight="weight")
+                == nx.radius(self.G, weight="cost")
+                != nx.radius(self.G, weight="high_cost")
+            )
+            assert nx.eccentricity(self.G, v, weight="high_cost") == nx.radius(
+                self.G, weight="high_cost"
+            )
+
+    def test_center_weight_fn(self):
+        for v in set(nx.center(self.G, weight=self.weight_fn)):
+            assert nx.eccentricity(self.G, v, weight=self.weight_fn) == nx.radius(
+                self.G, weight=self.weight_fn
+            )
+
+    def test_bound_diameter_weight_None(self):
+        assert nx.diameter(self.G, usebounds=True, weight=None) == 3
+
+    def test_bound_diameter_weight_attr(self):
+        assert (
+            nx.diameter(self.G, usebounds=True, weight="high_cost")
+            != nx.diameter(self.G, usebounds=True, weight="weight")
+            == nx.diameter(self.G, usebounds=True, weight="cost")
+            == 1.6
+            != nx.diameter(self.G, usebounds=True, weight="high_cost")
+        )
+        assert nx.diameter(self.G, usebounds=True, weight="high_cost") == nx.diameter(
+            self.G, usebounds=True, weight="high_cost"
+        )
+
+    def test_bound_diameter_weight_fn(self):
+        assert nx.diameter(self.G, usebounds=True, weight=self.weight_fn) == 6
+
+    def test_bound_radius_weight_None(self):
+        assert pytest.approx(nx.radius(self.G, usebounds=True, weight=None)) == 2
+
+    def test_bound_radius_weight_attr(self):
+        assert (
+            nx.radius(self.G, usebounds=True, weight="high_cost")
+            != pytest.approx(nx.radius(self.G, usebounds=True, weight="weight"))
+            == pytest.approx(nx.radius(self.G, usebounds=True, weight="cost"))
+            == 0.9
+            != nx.radius(self.G, usebounds=True, weight="high_cost")
+        )
+        assert nx.radius(self.G, usebounds=True, weight="high_cost") == nx.radius(
+            self.G, usebounds=True, weight="high_cost"
+        )
+
+    def test_bound_radius_weight_fn(self):
+        assert nx.radius(self.G, usebounds=True, weight=self.weight_fn) == 4
+
+    def test_bound_periphery_weight_None(self):
+        result = {1, 3, 4}
+        assert set(nx.periphery(self.G, usebounds=True, weight=None)) == result
+
+    def test_bound_periphery_weight_attr(self):
+        result = {4, 5}
+        assert (
+            set(nx.periphery(self.G, usebounds=True, weight="weight"))
+            == set(nx.periphery(self.G, usebounds=True, weight="cost"))
+            == result
+        )
+
+    def test_bound_periphery_weight_fn(self):
+        result = {1, 3, 4}
+        assert (
+            set(nx.periphery(self.G, usebounds=True, weight=self.weight_fn)) == result
+        )
+
+    def test_bound_center_weight_None(self):
+        result = {0, 2, 5}
+        assert set(nx.center(self.G, usebounds=True, weight=None)) == result
+
+    def test_bound_center_weight_attr(self):
+        result = {0}
+        assert (
+            set(nx.center(self.G, usebounds=True, weight="weight"))
+            == set(nx.center(self.G, usebounds=True, weight="cost"))
+            == result
+        )
+
+    def test_bound_center_weight_fn(self):
+        result = {0, 2, 5}
+        assert set(nx.center(self.G, usebounds=True, weight=self.weight_fn)) == result
+
+
+class TestResistanceDistance:
+    @classmethod
+    def setup_class(cls):
+        global np
+        np = pytest.importorskip("numpy")
+        sp = pytest.importorskip("scipy")
+
+    def setup_method(self):
+        G = nx.Graph()
+        G.add_edge(1, 2, weight=2)
+        G.add_edge(2, 3, weight=4)
+        G.add_edge(3, 4, weight=1)
+        G.add_edge(1, 4, weight=3)
+        self.G = G
+
+    def test_resistance_distance_directed_graph(self):
+        G = nx.DiGraph()
+        with pytest.raises(nx.NetworkXNotImplemented):
+            nx.resistance_distance(G)
+
+    def test_resistance_distance_empty(self):
+        G = nx.Graph()
+        with pytest.raises(nx.NetworkXError):
+            nx.resistance_distance(G)
+
+    def test_resistance_distance_not_connected(self):
+        with pytest.raises(nx.NetworkXError):
+            self.G.add_node(5)
+            nx.resistance_distance(self.G, 1, 5)
+
+    def test_resistance_distance_nodeA_not_in_graph(self):
+        with pytest.raises(nx.NetworkXError):
+            nx.resistance_distance(self.G, 9, 1)
+
+    def test_resistance_distance_nodeB_not_in_graph(self):
+        with pytest.raises(nx.NetworkXError):
+            nx.resistance_distance(self.G, 1, 9)
+
+    def test_resistance_distance(self):
+        rd = nx.resistance_distance(self.G, 1, 3, "weight", True)
+        test_data = 1 / (1 / (2 + 4) + 1 / (1 + 3))
+        assert round(rd, 5) == round(test_data, 5)
+
+    def test_resistance_distance_noinv(self):
+        rd = nx.resistance_distance(self.G, 1, 3, "weight", False)
+        test_data = 1 / (1 / (1 / 2 + 1 / 4) + 1 / (1 / 1 + 1 / 3))
+        assert round(rd, 5) == round(test_data, 5)
+
+    def test_resistance_distance_no_weight(self):
+        rd = nx.resistance_distance(self.G, 1, 3)
+        assert round(rd, 5) == 1
+
+    def test_resistance_distance_neg_weight(self):
+        self.G[2][3]["weight"] = -4
+        rd = nx.resistance_distance(self.G, 1, 3, "weight", True)
+        test_data = 1 / (1 / (2 + -4) + 1 / (1 + 3))
+        assert round(rd, 5) == round(test_data, 5)
+
+    def test_multigraph(self):
+        G = nx.MultiGraph()
+        G.add_edge(1, 2, weight=2)
+        G.add_edge(2, 3, weight=4)
+        G.add_edge(3, 4, weight=1)
+        G.add_edge(1, 4, weight=3)
+        rd = nx.resistance_distance(G, 1, 3, "weight", True)
+        assert np.isclose(rd, 1 / (1 / (2 + 4) + 1 / (1 + 3)))
+
+    def test_resistance_distance_div0(self):
+        with pytest.raises(ZeroDivisionError):
+            self.G[1][2]["weight"] = 0
+            nx.resistance_distance(self.G, 1, 3, "weight")
+
+    def test_resistance_distance_same_node(self):
+        assert nx.resistance_distance(self.G, 1, 1) == 0
+
+    def test_resistance_distance_only_nodeA(self):
+        rd = nx.resistance_distance(self.G, nodeA=1)
+        test_data = {}
+        test_data[1] = 0
+        test_data[2] = 0.75
+        test_data[3] = 1
+        test_data[4] = 0.75
+        assert type(rd) == dict
+        assert sorted(rd.keys()) == sorted(test_data.keys())
+        for key in rd:
+            assert np.isclose(rd[key], test_data[key])
+
+    def test_resistance_distance_only_nodeB(self):
+        rd = nx.resistance_distance(self.G, nodeB=1)
+        test_data = {}
+        test_data[1] = 0
+        test_data[2] = 0.75
+        test_data[3] = 1
+        test_data[4] = 0.75
+        assert type(rd) == dict
+        assert sorted(rd.keys()) == sorted(test_data.keys())
+        for key in rd:
+            assert np.isclose(rd[key], test_data[key])
+
+    def test_resistance_distance_all(self):
+        rd = nx.resistance_distance(self.G)
+        assert type(rd) == dict
+        assert round(rd[1][3], 5) == 1
+
+
+class TestEffectiveGraphResistance:
+    @classmethod
+    def setup_class(cls):
+        global np
+        np = pytest.importorskip("numpy")
+        sp = pytest.importorskip("scipy")
+
+    def setup_method(self):
+        G = nx.Graph()
+        G.add_edge(1, 2, weight=2)
+        G.add_edge(1, 3, weight=1)
+        G.add_edge(2, 3, weight=4)
+        self.G = G
+
+    def test_effective_graph_resistance_directed_graph(self):
+        G = nx.DiGraph()
+        with pytest.raises(nx.NetworkXNotImplemented):
+            nx.effective_graph_resistance(G)
+
+    def test_effective_graph_resistance_empty(self):
+        G = nx.Graph()
+        with pytest.raises(nx.NetworkXError):
+            nx.effective_graph_resistance(G)
+
+    def test_effective_graph_resistance_not_connected(self):
+        G = nx.Graph([(1, 2), (3, 4)])
+        RG = nx.effective_graph_resistance(G)
+        assert np.isinf(RG)
+
+    def test_effective_graph_resistance(self):
+        RG = nx.effective_graph_resistance(self.G, "weight", True)
+        rd12 = 1 / (1 / (1 + 4) + 1 / 2)
+        rd13 = 1 / (1 / (1 + 2) + 1 / 4)
+        rd23 = 1 / (1 / (2 + 4) + 1 / 1)
+        assert np.isclose(RG, rd12 + rd13 + rd23)
+
+    def test_effective_graph_resistance_noinv(self):
+        RG = nx.effective_graph_resistance(self.G, "weight", False)
+        rd12 = 1 / (1 / (1 / 1 + 1 / 4) + 1 / (1 / 2))
+        rd13 = 1 / (1 / (1 / 1 + 1 / 2) + 1 / (1 / 4))
+        rd23 = 1 / (1 / (1 / 2 + 1 / 4) + 1 / (1 / 1))
+        assert np.isclose(RG, rd12 + rd13 + rd23)
+
+    def test_effective_graph_resistance_no_weight(self):
+        RG = nx.effective_graph_resistance(self.G)
+        assert np.isclose(RG, 2)
+
+    def test_effective_graph_resistance_neg_weight(self):
+        self.G[2][3]["weight"] = -4
+        RG = nx.effective_graph_resistance(self.G, "weight", True)
+        rd12 = 1 / (1 / (1 + -4) + 1 / 2)
+        rd13 = 1 / (1 / (1 + 2) + 1 / (-4))
+        rd23 = 1 / (1 / (2 + -4) + 1 / 1)
+        assert np.isclose(RG, rd12 + rd13 + rd23)
+
+    def test_effective_graph_resistance_multigraph(self):
+        G = nx.MultiGraph()
+        G.add_edge(1, 2, weight=2)
+        G.add_edge(1, 3, weight=1)
+        G.add_edge(2, 3, weight=1)
+        G.add_edge(2, 3, weight=3)
+        RG = nx.effective_graph_resistance(G, "weight", True)
+        edge23 = 1 / (1 / 1 + 1 / 3)
+        rd12 = 1 / (1 / (1 + edge23) + 1 / 2)
+        rd13 = 1 / (1 / (1 + 2) + 1 / edge23)
+        rd23 = 1 / (1 / (2 + edge23) + 1 / 1)
+        assert np.isclose(RG, rd12 + rd13 + rd23)
+
+    def test_effective_graph_resistance_div0(self):
+        with pytest.raises(ZeroDivisionError):
+            self.G[1][2]["weight"] = 0
+            nx.effective_graph_resistance(self.G, "weight")
+
+    def test_effective_graph_resistance_complete_graph(self):
+        N = 10
+        G = nx.complete_graph(N)
+        RG = nx.effective_graph_resistance(G)
+        assert np.isclose(RG, N - 1)
+
+    def test_effective_graph_resistance_path_graph(self):
+        N = 10
+        G = nx.path_graph(N)
+        RG = nx.effective_graph_resistance(G)
+        assert np.isclose(RG, (N - 1) * N * (N + 1) // 6)
+
+
+class TestBarycenter:
+    """Test :func:`networkx.algorithms.distance_measures.barycenter`."""
+
+    def barycenter_as_subgraph(self, g, **kwargs):
+        """Return the subgraph induced on the barycenter of g"""
+        b = nx.barycenter(g, **kwargs)
+        assert isinstance(b, list)
+        assert set(b) <= set(g)
+        return g.subgraph(b)
+
+    def test_must_be_connected(self):
+        pytest.raises(nx.NetworkXNoPath, nx.barycenter, nx.empty_graph(5))
+
+    def test_sp_kwarg(self):
+        # Complete graph K_5. Normally it works...
+        K_5 = nx.complete_graph(5)
+        sp = dict(nx.shortest_path_length(K_5))
+        assert nx.barycenter(K_5, sp=sp) == list(K_5)
+
+        # ...but not with the weight argument
+        for u, v, data in K_5.edges.data():
+            data["weight"] = 1
+        pytest.raises(ValueError, nx.barycenter, K_5, sp=sp, weight="weight")
+
+        # ...and a corrupted sp can make it seem like K_5 is disconnected
+        del sp[0][1]
+        pytest.raises(nx.NetworkXNoPath, nx.barycenter, K_5, sp=sp)
+
+    def test_trees(self):
+        """The barycenter of a tree is a single vertex or an edge.
+
+        See [West01]_, p. 78.
+        """
+        prng = Random(0xDEADBEEF)
+        for i in range(50):
+            RT = nx.random_labeled_tree(prng.randint(1, 75), seed=prng)
+            b = self.barycenter_as_subgraph(RT)
+            if len(b) == 2:
+                assert b.size() == 1
+            else:
+                assert len(b) == 1
+                assert b.size() == 0
+
+    def test_this_one_specific_tree(self):
+        """Test the tree pictured at the bottom of [West01]_, p. 78."""
+        g = nx.Graph(
+            {
+                "a": ["b"],
+                "b": ["a", "x"],
+                "x": ["b", "y"],
+                "y": ["x", "z"],
+                "z": ["y", 0, 1, 2, 3, 4],
+                0: ["z"],
+                1: ["z"],
+                2: ["z"],
+                3: ["z"],
+                4: ["z"],
+            }
+        )
+        b = self.barycenter_as_subgraph(g, attr="barycentricity")
+        assert list(b) == ["z"]
+        assert not b.edges
+        expected_barycentricity = {
+            0: 23,
+            1: 23,
+            2: 23,
+            3: 23,
+            4: 23,
+            "a": 35,
+            "b": 27,
+            "x": 21,
+            "y": 17,
+            "z": 15,
+        }
+        for node, barycentricity in expected_barycentricity.items():
+            assert g.nodes[node]["barycentricity"] == barycentricity
+
+        # Doubling weights should do nothing but double the barycentricities
+        for edge in g.edges:
+            g.edges[edge]["weight"] = 2
+        b = self.barycenter_as_subgraph(g, weight="weight", attr="barycentricity2")
+        assert list(b) == ["z"]
+        assert not b.edges
+        for node, barycentricity in expected_barycentricity.items():
+            assert g.nodes[node]["barycentricity2"] == barycentricity * 2
+
+
+class TestKemenyConstant:
+    @classmethod
+    def setup_class(cls):
+        global np
+        np = pytest.importorskip("numpy")
+        sp = pytest.importorskip("scipy")
+
+    def setup_method(self):
+        G = nx.Graph()
+        w12 = 2
+        w13 = 3
+        w23 = 4
+        G.add_edge(1, 2, weight=w12)
+        G.add_edge(1, 3, weight=w13)
+        G.add_edge(2, 3, weight=w23)
+        self.G = G
+
+    def test_kemeny_constant_directed(self):
+        G = nx.DiGraph()
+        G.add_edge(1, 2)
+        G.add_edge(1, 3)
+        G.add_edge(2, 3)
+        with pytest.raises(nx.NetworkXNotImplemented):
+            nx.kemeny_constant(G)
+
+    def test_kemeny_constant_not_connected(self):
+        self.G.add_node(5)
+        with pytest.raises(nx.NetworkXError):
+            nx.kemeny_constant(self.G)
+
+    def test_kemeny_constant_no_nodes(self):
+        G = nx.Graph()
+        with pytest.raises(nx.NetworkXError):
+            nx.kemeny_constant(G)
+
+    def test_kemeny_constant_negative_weight(self):
+        G = nx.Graph()
+        w12 = 2
+        w13 = 3
+        w23 = -10
+        G.add_edge(1, 2, weight=w12)
+        G.add_edge(1, 3, weight=w13)
+        G.add_edge(2, 3, weight=w23)
+        with pytest.raises(nx.NetworkXError):
+            nx.kemeny_constant(G, weight="weight")
+
+    def test_kemeny_constant(self):
+        K = nx.kemeny_constant(self.G, weight="weight")
+        w12 = 2
+        w13 = 3
+        w23 = 4
+        test_data = (
+            3
+            / 2
+            * (w12 + w13)
+            * (w12 + w23)
+            * (w13 + w23)
+            / (
+                w12**2 * (w13 + w23)
+                + w13**2 * (w12 + w23)
+                + w23**2 * (w12 + w13)
+                + 3 * w12 * w13 * w23
+            )
+        )
+        assert np.isclose(K, test_data)
+
+    def test_kemeny_constant_no_weight(self):
+        K = nx.kemeny_constant(self.G)
+        assert np.isclose(K, 4 / 3)
+
+    def test_kemeny_constant_multigraph(self):
+        G = nx.MultiGraph()
+        w12_1 = 2
+        w12_2 = 1
+        w13 = 3
+        w23 = 4
+        G.add_edge(1, 2, weight=w12_1)
+        G.add_edge(1, 2, weight=w12_2)
+        G.add_edge(1, 3, weight=w13)
+        G.add_edge(2, 3, weight=w23)
+        K = nx.kemeny_constant(G, weight="weight")
+        w12 = w12_1 + w12_2
+        test_data = (
+            3
+            / 2
+            * (w12 + w13)
+            * (w12 + w23)
+            * (w13 + w23)
+            / (
+                w12**2 * (w13 + w23)
+                + w13**2 * (w12 + w23)
+                + w23**2 * (w12 + w13)
+                + 3 * w12 * w13 * w23
+            )
+        )
+        assert np.isclose(K, test_data)
+
+    def test_kemeny_constant_weight0(self):
+        G = nx.Graph()
+        w12 = 0
+        w13 = 3
+        w23 = 4
+        G.add_edge(1, 2, weight=w12)
+        G.add_edge(1, 3, weight=w13)
+        G.add_edge(2, 3, weight=w23)
+        K = nx.kemeny_constant(G, weight="weight")
+        test_data = (
+            3
+            / 2
+            * (w12 + w13)
+            * (w12 + w23)
+            * (w13 + w23)
+            / (
+                w12**2 * (w13 + w23)
+                + w13**2 * (w12 + w23)
+                + w23**2 * (w12 + w13)
+                + 3 * w12 * w13 * w23
+            )
+        )
+        assert np.isclose(K, test_data)
+
+    def test_kemeny_constant_selfloop(self):
+        G = nx.Graph()
+        w11 = 1
+        w12 = 2
+        w13 = 3
+        w23 = 4
+        G.add_edge(1, 1, weight=w11)
+        G.add_edge(1, 2, weight=w12)
+        G.add_edge(1, 3, weight=w13)
+        G.add_edge(2, 3, weight=w23)
+        K = nx.kemeny_constant(G, weight="weight")
+        test_data = (
+            (2 * w11 + 3 * w12 + 3 * w13)
+            * (w12 + w23)
+            * (w13 + w23)
+            / (
+                (w12 * w13 + w12 * w23 + w13 * w23)
+                * (w11 + 2 * w12 + 2 * w13 + 2 * w23)
+            )
+        )
+        assert np.isclose(K, test_data)
+
+    def test_kemeny_constant_complete_bipartite_graph(self):
+        # Theorem 1 in https://www.sciencedirect.com/science/article/pii/S0166218X20302912
+        n1 = 5
+        n2 = 4
+        G = nx.complete_bipartite_graph(n1, n2)
+        K = nx.kemeny_constant(G)
+        assert np.isclose(K, n1 + n2 - 3 / 2)
+
+    def test_kemeny_constant_path_graph(self):
+        # Theorem 2 in https://www.sciencedirect.com/science/article/pii/S0166218X20302912
+        n = 10
+        G = nx.path_graph(n)
+        K = nx.kemeny_constant(G)
+        assert np.isclose(K, n**2 / 3 - 2 * n / 3 + 1 / 2)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_regular.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_regular.py
new file mode 100644
index 00000000..545fb6de
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_regular.py
@@ -0,0 +1,85 @@
+import pytest
+
+import networkx as nx
+from networkx import is_strongly_regular
+
+
+@pytest.mark.parametrize(
+    "f", (nx.is_distance_regular, nx.intersection_array, nx.is_strongly_regular)
+)
+@pytest.mark.parametrize("graph_constructor", (nx.DiGraph, nx.MultiGraph))
+def test_raises_on_directed_and_multigraphs(f, graph_constructor):
+    G = graph_constructor([(0, 1), (1, 2)])
+    with pytest.raises(nx.NetworkXNotImplemented):
+        f(G)
+
+
+class TestDistanceRegular:
+    def test_is_distance_regular(self):
+        assert nx.is_distance_regular(nx.icosahedral_graph())
+        assert nx.is_distance_regular(nx.petersen_graph())
+        assert nx.is_distance_regular(nx.cubical_graph())
+        assert nx.is_distance_regular(nx.complete_bipartite_graph(3, 3))
+        assert nx.is_distance_regular(nx.tetrahedral_graph())
+        assert nx.is_distance_regular(nx.dodecahedral_graph())
+        assert nx.is_distance_regular(nx.pappus_graph())
+        assert nx.is_distance_regular(nx.heawood_graph())
+        assert nx.is_distance_regular(nx.cycle_graph(3))
+        # no distance regular
+        assert not nx.is_distance_regular(nx.path_graph(4))
+
+    def test_not_connected(self):
+        G = nx.cycle_graph(4)
+        nx.add_cycle(G, [5, 6, 7])
+        assert not nx.is_distance_regular(G)
+
+    def test_global_parameters(self):
+        b, c = nx.intersection_array(nx.cycle_graph(5))
+        g = nx.global_parameters(b, c)
+        assert list(g) == [(0, 0, 2), (1, 0, 1), (1, 1, 0)]
+        b, c = nx.intersection_array(nx.cycle_graph(3))
+        g = nx.global_parameters(b, c)
+        assert list(g) == [(0, 0, 2), (1, 1, 0)]
+
+    def test_intersection_array(self):
+        b, c = nx.intersection_array(nx.cycle_graph(5))
+        assert b == [2, 1]
+        assert c == [1, 1]
+        b, c = nx.intersection_array(nx.dodecahedral_graph())
+        assert b == [3, 2, 1, 1, 1]
+        assert c == [1, 1, 1, 2, 3]
+        b, c = nx.intersection_array(nx.icosahedral_graph())
+        assert b == [5, 2, 1]
+        assert c == [1, 2, 5]
+
+
+@pytest.mark.parametrize("f", (nx.is_distance_regular, nx.is_strongly_regular))
+def test_empty_graph_raises(f):
+    G = nx.Graph()
+    with pytest.raises(nx.NetworkXPointlessConcept, match="Graph has no nodes"):
+        f(G)
+
+
+class TestStronglyRegular:
+    """Unit tests for the :func:`~networkx.is_strongly_regular`
+    function.
+
+    """
+
+    def test_cycle_graph(self):
+        """Tests that the cycle graph on five vertices is strongly
+        regular.
+
+        """
+        G = nx.cycle_graph(5)
+        assert is_strongly_regular(G)
+
+    def test_petersen_graph(self):
+        """Tests that the Petersen graph is strongly regular."""
+        G = nx.petersen_graph()
+        assert is_strongly_regular(G)
+
+    def test_path_graph(self):
+        """Tests that the path graph is not strongly regular."""
+        G = nx.path_graph(4)
+        assert not is_strongly_regular(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominance.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominance.py
new file mode 100644
index 00000000..9b804c2f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominance.py
@@ -0,0 +1,286 @@
+import pytest
+
+import networkx as nx
+
+
+class TestImmediateDominators:
+    def test_exceptions(self):
+        G = nx.Graph()
+        G.add_node(0)
+        pytest.raises(nx.NetworkXNotImplemented, nx.immediate_dominators, G, 0)
+        G = nx.MultiGraph(G)
+        pytest.raises(nx.NetworkXNotImplemented, nx.immediate_dominators, G, 0)
+        G = nx.DiGraph([[0, 0]])
+        pytest.raises(nx.NetworkXError, nx.immediate_dominators, G, 1)
+
+    def test_singleton(self):
+        G = nx.DiGraph()
+        G.add_node(0)
+        assert nx.immediate_dominators(G, 0) == {0: 0}
+        G.add_edge(0, 0)
+        assert nx.immediate_dominators(G, 0) == {0: 0}
+
+    def test_path(self):
+        n = 5
+        G = nx.path_graph(n, create_using=nx.DiGraph())
+        assert nx.immediate_dominators(G, 0) == {i: max(i - 1, 0) for i in range(n)}
+
+    def test_cycle(self):
+        n = 5
+        G = nx.cycle_graph(n, create_using=nx.DiGraph())
+        assert nx.immediate_dominators(G, 0) == {i: max(i - 1, 0) for i in range(n)}
+
+    def test_unreachable(self):
+        n = 5
+        assert n > 1
+        G = nx.path_graph(n, create_using=nx.DiGraph())
+        assert nx.immediate_dominators(G, n // 2) == {
+            i: max(i - 1, n // 2) for i in range(n // 2, n)
+        }
+
+    def test_irreducible1(self):
+        """
+        Graph taken from figure 2 of "A simple, fast dominance algorithm." (2006).
+        https://hdl.handle.net/1911/96345
+        """
+        edges = [(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)]
+        G = nx.DiGraph(edges)
+        assert nx.immediate_dominators(G, 5) == {i: 5 for i in range(1, 6)}
+
+    def test_irreducible2(self):
+        """
+        Graph taken from figure 4 of "A simple, fast dominance algorithm." (2006).
+        https://hdl.handle.net/1911/96345
+        """
+
+        edges = [(1, 2), (2, 1), (2, 3), (3, 2), (4, 2), (4, 3), (5, 1), (6, 4), (6, 5)]
+        G = nx.DiGraph(edges)
+        result = nx.immediate_dominators(G, 6)
+        assert result == {i: 6 for i in range(1, 7)}
+
+    def test_domrel_png(self):
+        # Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png
+        edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)]
+        G = nx.DiGraph(edges)
+        result = nx.immediate_dominators(G, 1)
+        assert result == {1: 1, 2: 1, 3: 2, 4: 2, 5: 2, 6: 2}
+        # Test postdominance.
+        result = nx.immediate_dominators(G.reverse(copy=False), 6)
+        assert result == {1: 2, 2: 6, 3: 5, 4: 5, 5: 2, 6: 6}
+
+    def test_boost_example(self):
+        # Graph taken from Figure 1 of
+        # http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm
+        edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)]
+        G = nx.DiGraph(edges)
+        result = nx.immediate_dominators(G, 0)
+        assert result == {0: 0, 1: 0, 2: 1, 3: 1, 4: 3, 5: 4, 6: 4, 7: 1}
+        # Test postdominance.
+        result = nx.immediate_dominators(G.reverse(copy=False), 7)
+        assert result == {0: 1, 1: 7, 2: 7, 3: 4, 4: 5, 5: 7, 6: 4, 7: 7}
+
+
+class TestDominanceFrontiers:
+    def test_exceptions(self):
+        G = nx.Graph()
+        G.add_node(0)
+        pytest.raises(nx.NetworkXNotImplemented, nx.dominance_frontiers, G, 0)
+        G = nx.MultiGraph(G)
+        pytest.raises(nx.NetworkXNotImplemented, nx.dominance_frontiers, G, 0)
+        G = nx.DiGraph([[0, 0]])
+        pytest.raises(nx.NetworkXError, nx.dominance_frontiers, G, 1)
+
+    def test_singleton(self):
+        G = nx.DiGraph()
+        G.add_node(0)
+        assert nx.dominance_frontiers(G, 0) == {0: set()}
+        G.add_edge(0, 0)
+        assert nx.dominance_frontiers(G, 0) == {0: set()}
+
+    def test_path(self):
+        n = 5
+        G = nx.path_graph(n, create_using=nx.DiGraph())
+        assert nx.dominance_frontiers(G, 0) == {i: set() for i in range(n)}
+
+    def test_cycle(self):
+        n = 5
+        G = nx.cycle_graph(n, create_using=nx.DiGraph())
+        assert nx.dominance_frontiers(G, 0) == {i: set() for i in range(n)}
+
+    def test_unreachable(self):
+        n = 5
+        assert n > 1
+        G = nx.path_graph(n, create_using=nx.DiGraph())
+        assert nx.dominance_frontiers(G, n // 2) == {i: set() for i in range(n // 2, n)}
+
+    def test_irreducible1(self):
+        """
+        Graph taken from figure 2 of "A simple, fast dominance algorithm." (2006).
+        https://hdl.handle.net/1911/96345
+        """
+        edges = [(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)]
+        G = nx.DiGraph(edges)
+        assert dict(nx.dominance_frontiers(G, 5).items()) == {
+            1: {2},
+            2: {1},
+            3: {2},
+            4: {1},
+            5: set(),
+        }
+
+    def test_irreducible2(self):
+        """
+        Graph taken from figure 4 of "A simple, fast dominance algorithm." (2006).
+        https://hdl.handle.net/1911/96345
+        """
+        edges = [(1, 2), (2, 1), (2, 3), (3, 2), (4, 2), (4, 3), (5, 1), (6, 4), (6, 5)]
+        G = nx.DiGraph(edges)
+        assert nx.dominance_frontiers(G, 6) == {
+            1: {2},
+            2: {1, 3},
+            3: {2},
+            4: {2, 3},
+            5: {1},
+            6: set(),
+        }
+
+    def test_domrel_png(self):
+        # Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png
+        edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)]
+        G = nx.DiGraph(edges)
+        assert nx.dominance_frontiers(G, 1) == {
+            1: set(),
+            2: {2},
+            3: {5},
+            4: {5},
+            5: {2},
+            6: set(),
+        }
+        # Test postdominance.
+        result = nx.dominance_frontiers(G.reverse(copy=False), 6)
+        assert result == {1: set(), 2: {2}, 3: {2}, 4: {2}, 5: {2}, 6: set()}
+
+    def test_boost_example(self):
+        # Graph taken from Figure 1 of
+        # http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm
+        edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)]
+        G = nx.DiGraph(edges)
+        assert nx.dominance_frontiers(G, 0) == {
+            0: set(),
+            1: set(),
+            2: {7},
+            3: {7},
+            4: {4, 7},
+            5: {7},
+            6: {4},
+            7: set(),
+        }
+        # Test postdominance.
+        result = nx.dominance_frontiers(G.reverse(copy=False), 7)
+        expected = {
+            0: set(),
+            1: set(),
+            2: {1},
+            3: {1},
+            4: {1, 4},
+            5: {1},
+            6: {4},
+            7: set(),
+        }
+        assert result == expected
+
+    def test_discard_issue(self):
+        # https://github.com/networkx/networkx/issues/2071
+        g = nx.DiGraph()
+        g.add_edges_from(
+            [
+                ("b0", "b1"),
+                ("b1", "b2"),
+                ("b2", "b3"),
+                ("b3", "b1"),
+                ("b1", "b5"),
+                ("b5", "b6"),
+                ("b5", "b8"),
+                ("b6", "b7"),
+                ("b8", "b7"),
+                ("b7", "b3"),
+                ("b3", "b4"),
+            ]
+        )
+        df = nx.dominance_frontiers(g, "b0")
+        assert df == {
+            "b4": set(),
+            "b5": {"b3"},
+            "b6": {"b7"},
+            "b7": {"b3"},
+            "b0": set(),
+            "b1": {"b1"},
+            "b2": {"b3"},
+            "b3": {"b1"},
+            "b8": {"b7"},
+        }
+
+    def test_loop(self):
+        g = nx.DiGraph()
+        g.add_edges_from([("a", "b"), ("b", "c"), ("b", "a")])
+        df = nx.dominance_frontiers(g, "a")
+        assert df == {"a": set(), "b": set(), "c": set()}
+
+    def test_missing_immediate_doms(self):
+        # see https://github.com/networkx/networkx/issues/2070
+        g = nx.DiGraph()
+        edges = [
+            ("entry_1", "b1"),
+            ("b1", "b2"),
+            ("b2", "b3"),
+            ("b3", "exit"),
+            ("entry_2", "b3"),
+        ]
+
+        # entry_1
+        #   |
+        #   b1
+        #   |
+        #   b2  entry_2
+        #    |  /
+        #    b3
+        #    |
+        #   exit
+
+        g.add_edges_from(edges)
+        # formerly raised KeyError on entry_2 when parsing b3
+        # because entry_2 does not have immediate doms (no path)
+        nx.dominance_frontiers(g, "entry_1")
+
+    def test_loops_larger(self):
+        # from
+        # http://ecee.colorado.edu/~waite/Darmstadt/motion.html
+        g = nx.DiGraph()
+        edges = [
+            ("entry", "exit"),
+            ("entry", "1"),
+            ("1", "2"),
+            ("2", "3"),
+            ("3", "4"),
+            ("4", "5"),
+            ("5", "6"),
+            ("6", "exit"),
+            ("6", "2"),
+            ("5", "3"),
+            ("4", "4"),
+        ]
+
+        g.add_edges_from(edges)
+        df = nx.dominance_frontiers(g, "entry")
+        answer = {
+            "entry": set(),
+            "1": {"exit"},
+            "2": {"exit", "2"},
+            "3": {"exit", "3", "2"},
+            "4": {"exit", "4", "3", "2"},
+            "5": {"exit", "3", "2"},
+            "6": {"exit", "2"},
+            "exit": set(),
+        }
+        for n in df:
+            assert set(df[n]) == set(answer[n])
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominating.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominating.py
new file mode 100644
index 00000000..b945c738
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominating.py
@@ -0,0 +1,46 @@
+import pytest
+
+import networkx as nx
+
+
+def test_dominating_set():
+    G = nx.gnp_random_graph(100, 0.1)
+    D = nx.dominating_set(G)
+    assert nx.is_dominating_set(G, D)
+    D = nx.dominating_set(G, start_with=0)
+    assert nx.is_dominating_set(G, D)
+
+
+def test_complete():
+    """In complete graphs each node is a dominating set.
+    Thus the dominating set has to be of cardinality 1.
+    """
+    K4 = nx.complete_graph(4)
+    assert len(nx.dominating_set(K4)) == 1
+    K5 = nx.complete_graph(5)
+    assert len(nx.dominating_set(K5)) == 1
+
+
+def test_raise_dominating_set():
+    with pytest.raises(nx.NetworkXError):
+        G = nx.path_graph(4)
+        D = nx.dominating_set(G, start_with=10)
+
+
+def test_is_dominating_set():
+    G = nx.path_graph(4)
+    d = {1, 3}
+    assert nx.is_dominating_set(G, d)
+    d = {0, 2}
+    assert nx.is_dominating_set(G, d)
+    d = {1}
+    assert not nx.is_dominating_set(G, d)
+
+
+def test_wikipedia_is_dominating_set():
+    """Example from https://en.wikipedia.org/wiki/Dominating_set"""
+    G = nx.cycle_graph(4)
+    G.add_edges_from([(0, 4), (1, 4), (2, 5)])
+    assert nx.is_dominating_set(G, {4, 3, 5})
+    assert nx.is_dominating_set(G, {0, 2})
+    assert nx.is_dominating_set(G, {1, 2})
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_efficiency.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_efficiency.py
new file mode 100644
index 00000000..9a2e7d04
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_efficiency.py
@@ -0,0 +1,58 @@
+"""Unit tests for the :mod:`networkx.algorithms.efficiency` module."""
+
+import networkx as nx
+
+
+class TestEfficiency:
+    def setup_method(self):
+        # G1 is a disconnected graph
+        self.G1 = nx.Graph()
+        self.G1.add_nodes_from([1, 2, 3])
+        # G2 is a cycle graph
+        self.G2 = nx.cycle_graph(4)
+        # G3 is the triangle graph with one additional edge
+        self.G3 = nx.lollipop_graph(3, 1)
+
+    def test_efficiency_disconnected_nodes(self):
+        """
+        When nodes are disconnected, efficiency is 0
+        """
+        assert nx.efficiency(self.G1, 1, 2) == 0
+
+    def test_local_efficiency_disconnected_graph(self):
+        """
+        In a disconnected graph the efficiency is 0
+        """
+        assert nx.local_efficiency(self.G1) == 0
+
+    def test_efficiency(self):
+        assert nx.efficiency(self.G2, 0, 1) == 1
+        assert nx.efficiency(self.G2, 0, 2) == 1 / 2
+
+    def test_global_efficiency(self):
+        assert nx.global_efficiency(self.G2) == 5 / 6
+
+    def test_global_efficiency_complete_graph(self):
+        """
+        Tests that the average global efficiency of the complete graph is one.
+        """
+        for n in range(2, 10):
+            G = nx.complete_graph(n)
+            assert nx.global_efficiency(G) == 1
+
+    def test_local_efficiency_complete_graph(self):
+        """
+        Test that the local efficiency for a complete graph with at least 3
+        nodes should be one. For a graph with only 2 nodes, the induced
+        subgraph has no edges.
+        """
+        for n in range(3, 10):
+            G = nx.complete_graph(n)
+            assert nx.local_efficiency(G) == 1
+
+    def test_using_ego_graph(self):
+        """
+        Test that the ego graph is used when computing local efficiency.
+        For more information, see GitHub issue #2710.
+        """
+        assert nx.local_efficiency(self.G3) == 7 / 12
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_euler.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_euler.py
new file mode 100644
index 00000000..b5871f09
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_euler.py
@@ -0,0 +1,314 @@
+import collections
+
+import pytest
+
+import networkx as nx
+
+
+@pytest.mark.parametrize("f", (nx.is_eulerian, nx.is_semieulerian))
+def test_empty_graph_raises(f):
+    G = nx.Graph()
+    with pytest.raises(nx.NetworkXPointlessConcept, match="Connectivity is undefined"):
+        f(G)
+
+
+class TestIsEulerian:
+    def test_is_eulerian(self):
+        assert nx.is_eulerian(nx.complete_graph(5))
+        assert nx.is_eulerian(nx.complete_graph(7))
+        assert nx.is_eulerian(nx.hypercube_graph(4))
+        assert nx.is_eulerian(nx.hypercube_graph(6))
+
+        assert not nx.is_eulerian(nx.complete_graph(4))
+        assert not nx.is_eulerian(nx.complete_graph(6))
+        assert not nx.is_eulerian(nx.hypercube_graph(3))
+        assert not nx.is_eulerian(nx.hypercube_graph(5))
+
+        assert not nx.is_eulerian(nx.petersen_graph())
+        assert not nx.is_eulerian(nx.path_graph(4))
+
+    def test_is_eulerian2(self):
+        # not connected
+        G = nx.Graph()
+        G.add_nodes_from([1, 2, 3])
+        assert not nx.is_eulerian(G)
+        # not strongly connected
+        G = nx.DiGraph()
+        G.add_nodes_from([1, 2, 3])
+        assert not nx.is_eulerian(G)
+        G = nx.MultiDiGraph()
+        G.add_edge(1, 2)
+        G.add_edge(2, 3)
+        G.add_edge(2, 3)
+        G.add_edge(3, 1)
+        assert not nx.is_eulerian(G)
+
+
+class TestEulerianCircuit:
+    def test_eulerian_circuit_cycle(self):
+        G = nx.cycle_graph(4)
+
+        edges = list(nx.eulerian_circuit(G, source=0))
+        nodes = [u for u, v in edges]
+        assert nodes == [0, 3, 2, 1]
+        assert edges == [(0, 3), (3, 2), (2, 1), (1, 0)]
+
+        edges = list(nx.eulerian_circuit(G, source=1))
+        nodes = [u for u, v in edges]
+        assert nodes == [1, 2, 3, 0]
+        assert edges == [(1, 2), (2, 3), (3, 0), (0, 1)]
+
+        G = nx.complete_graph(3)
+
+        edges = list(nx.eulerian_circuit(G, source=0))
+        nodes = [u for u, v in edges]
+        assert nodes == [0, 2, 1]
+        assert edges == [(0, 2), (2, 1), (1, 0)]
+
+        edges = list(nx.eulerian_circuit(G, source=1))
+        nodes = [u for u, v in edges]
+        assert nodes == [1, 2, 0]
+        assert edges == [(1, 2), (2, 0), (0, 1)]
+
+    def test_eulerian_circuit_digraph(self):
+        G = nx.DiGraph()
+        nx.add_cycle(G, [0, 1, 2, 3])
+
+        edges = list(nx.eulerian_circuit(G, source=0))
+        nodes = [u for u, v in edges]
+        assert nodes == [0, 1, 2, 3]
+        assert edges == [(0, 1), (1, 2), (2, 3), (3, 0)]
+
+        edges = list(nx.eulerian_circuit(G, source=1))
+        nodes = [u for u, v in edges]
+        assert nodes == [1, 2, 3, 0]
+        assert edges == [(1, 2), (2, 3), (3, 0), (0, 1)]
+
+    def test_multigraph(self):
+        G = nx.MultiGraph()
+        nx.add_cycle(G, [0, 1, 2, 3])
+        G.add_edge(1, 2)
+        G.add_edge(1, 2)
+        edges = list(nx.eulerian_circuit(G, source=0))
+        nodes = [u for u, v in edges]
+        assert nodes == [0, 3, 2, 1, 2, 1]
+        assert edges == [(0, 3), (3, 2), (2, 1), (1, 2), (2, 1), (1, 0)]
+
+    def test_multigraph_with_keys(self):
+        G = nx.MultiGraph()
+        nx.add_cycle(G, [0, 1, 2, 3])
+        G.add_edge(1, 2)
+        G.add_edge(1, 2)
+        edges = list(nx.eulerian_circuit(G, source=0, keys=True))
+        nodes = [u for u, v, k in edges]
+        assert nodes == [0, 3, 2, 1, 2, 1]
+        assert edges[:2] == [(0, 3, 0), (3, 2, 0)]
+        assert collections.Counter(edges[2:5]) == collections.Counter(
+            [(2, 1, 0), (1, 2, 1), (2, 1, 2)]
+        )
+        assert edges[5:] == [(1, 0, 0)]
+
+    def test_not_eulerian(self):
+        with pytest.raises(nx.NetworkXError):
+            f = list(nx.eulerian_circuit(nx.complete_graph(4)))
+
+
+class TestIsSemiEulerian:
+    def test_is_semieulerian(self):
+        # Test graphs with Eulerian paths but no cycles return True.
+        assert nx.is_semieulerian(nx.path_graph(4))
+        G = nx.path_graph(6, create_using=nx.DiGraph)
+        assert nx.is_semieulerian(G)
+
+        # Test graphs with Eulerian cycles return False.
+        assert not nx.is_semieulerian(nx.complete_graph(5))
+        assert not nx.is_semieulerian(nx.complete_graph(7))
+        assert not nx.is_semieulerian(nx.hypercube_graph(4))
+        assert not nx.is_semieulerian(nx.hypercube_graph(6))
+
+
+class TestHasEulerianPath:
+    def test_has_eulerian_path_cyclic(self):
+        # Test graphs with Eulerian cycles return True.
+        assert nx.has_eulerian_path(nx.complete_graph(5))
+        assert nx.has_eulerian_path(nx.complete_graph(7))
+        assert nx.has_eulerian_path(nx.hypercube_graph(4))
+        assert nx.has_eulerian_path(nx.hypercube_graph(6))
+
+    def test_has_eulerian_path_non_cyclic(self):
+        # Test graphs with Eulerian paths but no cycles return True.
+        assert nx.has_eulerian_path(nx.path_graph(4))
+        G = nx.path_graph(6, create_using=nx.DiGraph)
+        assert nx.has_eulerian_path(G)
+
+    def test_has_eulerian_path_directed_graph(self):
+        # Test directed graphs and returns False
+        G = nx.DiGraph()
+        G.add_edges_from([(0, 1), (1, 2), (0, 2)])
+        assert not nx.has_eulerian_path(G)
+
+        # Test directed graphs without isolated node returns True
+        G = nx.DiGraph()
+        G.add_edges_from([(0, 1), (1, 2), (2, 0)])
+        assert nx.has_eulerian_path(G)
+
+        # Test directed graphs with isolated node returns False
+        G.add_node(3)
+        assert not nx.has_eulerian_path(G)
+
+    @pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph()))
+    def test_has_eulerian_path_not_weakly_connected(self, G):
+        G.add_edges_from([(0, 1), (2, 3), (3, 2)])
+        assert not nx.has_eulerian_path(G)
+
+    @pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph()))
+    def test_has_eulerian_path_unbalancedins_more_than_one(self, G):
+        G.add_edges_from([(0, 1), (2, 3)])
+        assert not nx.has_eulerian_path(G)
+
+
+class TestFindPathStart:
+    def testfind_path_start(self):
+        find_path_start = nx.algorithms.euler._find_path_start
+        # Test digraphs return correct starting node.
+        G = nx.path_graph(6, create_using=nx.DiGraph)
+        assert find_path_start(G) == 0
+        edges = [(0, 1), (1, 2), (2, 0), (4, 0)]
+        assert find_path_start(nx.DiGraph(edges)) == 4
+
+        # Test graph with no Eulerian path return None.
+        edges = [(0, 1), (1, 2), (2, 3), (2, 4)]
+        assert find_path_start(nx.DiGraph(edges)) is None
+
+
+class TestEulerianPath:
+    def test_eulerian_path(self):
+        x = [(4, 0), (0, 1), (1, 2), (2, 0)]
+        for e1, e2 in zip(x, nx.eulerian_path(nx.DiGraph(x))):
+            assert e1 == e2
+
+    def test_eulerian_path_straight_link(self):
+        G = nx.DiGraph()
+        result = [(1, 2), (2, 3), (3, 4), (4, 5)]
+        G.add_edges_from(result)
+        assert result == list(nx.eulerian_path(G))
+        assert result == list(nx.eulerian_path(G, source=1))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=3))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=4))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=5))
+
+    def test_eulerian_path_multigraph(self):
+        G = nx.MultiDiGraph()
+        result = [(2, 1), (1, 2), (2, 1), (1, 2), (2, 3), (3, 4), (4, 3)]
+        G.add_edges_from(result)
+        assert result == list(nx.eulerian_path(G))
+        assert result == list(nx.eulerian_path(G, source=2))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=3))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=4))
+
+    def test_eulerian_path_eulerian_circuit(self):
+        G = nx.DiGraph()
+        result = [(1, 2), (2, 3), (3, 4), (4, 1)]
+        result2 = [(2, 3), (3, 4), (4, 1), (1, 2)]
+        result3 = [(3, 4), (4, 1), (1, 2), (2, 3)]
+        G.add_edges_from(result)
+        assert result == list(nx.eulerian_path(G))
+        assert result == list(nx.eulerian_path(G, source=1))
+        assert result2 == list(nx.eulerian_path(G, source=2))
+        assert result3 == list(nx.eulerian_path(G, source=3))
+
+    def test_eulerian_path_undirected(self):
+        G = nx.Graph()
+        result = [(1, 2), (2, 3), (3, 4), (4, 5)]
+        result2 = [(5, 4), (4, 3), (3, 2), (2, 1)]
+        G.add_edges_from(result)
+        assert list(nx.eulerian_path(G)) in (result, result2)
+        assert result == list(nx.eulerian_path(G, source=1))
+        assert result2 == list(nx.eulerian_path(G, source=5))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=3))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=2))
+
+    def test_eulerian_path_multigraph_undirected(self):
+        G = nx.MultiGraph()
+        result = [(2, 1), (1, 2), (2, 1), (1, 2), (2, 3), (3, 4)]
+        G.add_edges_from(result)
+        assert result == list(nx.eulerian_path(G))
+        assert result == list(nx.eulerian_path(G, source=2))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=3))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=1))
+
+    @pytest.mark.parametrize(
+        ("graph_type", "result"),
+        (
+            (nx.MultiGraph, [(0, 1, 0), (1, 0, 1)]),
+            (nx.MultiDiGraph, [(0, 1, 0), (1, 0, 0)]),
+        ),
+    )
+    def test_eulerian_with_keys(self, graph_type, result):
+        G = graph_type([(0, 1), (1, 0)])
+        answer = nx.eulerian_path(G, keys=True)
+        assert list(answer) == result
+
+
+class TestEulerize:
+    def test_disconnected(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.from_edgelist([(0, 1), (2, 3)])
+            nx.eulerize(G)
+
+    def test_null_graph(self):
+        with pytest.raises(nx.NetworkXPointlessConcept):
+            nx.eulerize(nx.Graph())
+
+    def test_null_multigraph(self):
+        with pytest.raises(nx.NetworkXPointlessConcept):
+            nx.eulerize(nx.MultiGraph())
+
+    def test_on_empty_graph(self):
+        with pytest.raises(nx.NetworkXError):
+            nx.eulerize(nx.empty_graph(3))
+
+    def test_on_eulerian(self):
+        G = nx.cycle_graph(3)
+        H = nx.eulerize(G)
+        assert nx.is_isomorphic(G, H)
+
+    def test_on_eulerian_multigraph(self):
+        G = nx.MultiGraph(nx.cycle_graph(3))
+        G.add_edge(0, 1)
+        H = nx.eulerize(G)
+        assert nx.is_eulerian(H)
+
+    def test_on_complete_graph(self):
+        G = nx.complete_graph(4)
+        assert nx.is_eulerian(nx.eulerize(G))
+        assert nx.is_eulerian(nx.eulerize(nx.MultiGraph(G)))
+
+    def test_on_non_eulerian_graph(self):
+        G = nx.cycle_graph(18)
+        G.add_edge(0, 18)
+        G.add_edge(18, 19)
+        G.add_edge(17, 19)
+        G.add_edge(4, 20)
+        G.add_edge(20, 21)
+        G.add_edge(21, 22)
+        G.add_edge(22, 23)
+        G.add_edge(23, 24)
+        G.add_edge(24, 25)
+        G.add_edge(25, 26)
+        G.add_edge(26, 27)
+        G.add_edge(27, 28)
+        G.add_edge(28, 13)
+        assert not nx.is_eulerian(G)
+        G = nx.eulerize(G)
+        assert nx.is_eulerian(G)
+        assert nx.number_of_edges(G) == 39
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py
new file mode 100644
index 00000000..0828069d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py
@@ -0,0 +1,686 @@
+import pytest
+
+import networkx as nx
+from networkx.generators import directed
+
+# Unit tests for the :func:`~networkx.weisfeiler_lehman_graph_hash` function
+
+
+def test_empty_graph_hash():
+    """
+    empty graphs should give hashes regardless of other params
+    """
+    G1 = nx.empty_graph()
+    G2 = nx.empty_graph()
+
+    h1 = nx.weisfeiler_lehman_graph_hash(G1)
+    h2 = nx.weisfeiler_lehman_graph_hash(G2)
+    h3 = nx.weisfeiler_lehman_graph_hash(G2, edge_attr="edge_attr1")
+    h4 = nx.weisfeiler_lehman_graph_hash(G2, node_attr="node_attr1")
+    h5 = nx.weisfeiler_lehman_graph_hash(
+        G2, edge_attr="edge_attr1", node_attr="node_attr1"
+    )
+    h6 = nx.weisfeiler_lehman_graph_hash(G2, iterations=10)
+
+    assert h1 == h2
+    assert h1 == h3
+    assert h1 == h4
+    assert h1 == h5
+    assert h1 == h6
+
+
+def test_directed():
+    """
+    A directed graph with no bi-directional edges should yield different a graph hash
+    to the same graph taken as undirected if there are no hash collisions.
+    """
+    r = 10
+    for i in range(r):
+        G_directed = nx.gn_graph(10 + r, seed=100 + i)
+        G_undirected = nx.to_undirected(G_directed)
+
+        h_directed = nx.weisfeiler_lehman_graph_hash(G_directed)
+        h_undirected = nx.weisfeiler_lehman_graph_hash(G_undirected)
+
+        assert h_directed != h_undirected
+
+
+def test_reversed():
+    """
+    A directed graph with no bi-directional edges should yield different a graph hash
+    to the same graph taken with edge directions reversed if there are no hash collisions.
+    Here we test a cycle graph which is the minimal counterexample
+    """
+    G = nx.cycle_graph(5, create_using=nx.DiGraph)
+    nx.set_node_attributes(G, {n: str(n) for n in G.nodes()}, name="label")
+
+    G_reversed = G.reverse()
+
+    h = nx.weisfeiler_lehman_graph_hash(G, node_attr="label")
+    h_reversed = nx.weisfeiler_lehman_graph_hash(G_reversed, node_attr="label")
+
+    assert h != h_reversed
+
+
+def test_isomorphic():
+    """
+    graph hashes should be invariant to node-relabeling (when the output is reindexed
+    by the same mapping)
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G1 = nx.erdos_renyi_graph(n, p * i, seed=200 + i)
+        G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+        g1_hash = nx.weisfeiler_lehman_graph_hash(G1)
+        g2_hash = nx.weisfeiler_lehman_graph_hash(G2)
+
+        assert g1_hash == g2_hash
+
+
+def test_isomorphic_edge_attr():
+    """
+    Isomorphic graphs with differing edge attributes should yield different graph
+    hashes if the 'edge_attr' argument is supplied and populated in the graph,
+    and there are no hash collisions.
+    The output should still be invariant to node-relabeling
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G1 = nx.erdos_renyi_graph(n, p * i, seed=300 + i)
+
+        for a, b in G1.edges:
+            G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
+            G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
+
+        g1_hash_with_edge_attr1 = nx.weisfeiler_lehman_graph_hash(
+            G1, edge_attr="edge_attr1"
+        )
+        g1_hash_with_edge_attr2 = nx.weisfeiler_lehman_graph_hash(
+            G1, edge_attr="edge_attr2"
+        )
+        g1_hash_no_edge_attr = nx.weisfeiler_lehman_graph_hash(G1, edge_attr=None)
+
+        assert g1_hash_with_edge_attr1 != g1_hash_no_edge_attr
+        assert g1_hash_with_edge_attr2 != g1_hash_no_edge_attr
+        assert g1_hash_with_edge_attr1 != g1_hash_with_edge_attr2
+
+        G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+        g2_hash_with_edge_attr1 = nx.weisfeiler_lehman_graph_hash(
+            G2, edge_attr="edge_attr1"
+        )
+        g2_hash_with_edge_attr2 = nx.weisfeiler_lehman_graph_hash(
+            G2, edge_attr="edge_attr2"
+        )
+
+        assert g1_hash_with_edge_attr1 == g2_hash_with_edge_attr1
+        assert g1_hash_with_edge_attr2 == g2_hash_with_edge_attr2
+
+
+def test_missing_edge_attr():
+    """
+    If the 'edge_attr' argument is supplied but is missing from an edge in the graph,
+    we should raise a KeyError
+    """
+    G = nx.Graph()
+    G.add_edges_from([(1, 2, {"edge_attr1": "a"}), (1, 3, {})])
+    pytest.raises(KeyError, nx.weisfeiler_lehman_graph_hash, G, edge_attr="edge_attr1")
+
+
+def test_isomorphic_node_attr():
+    """
+    Isomorphic graphs with differing node attributes should yield different graph
+    hashes if the 'node_attr' argument is supplied and populated in the graph, and
+    there are no hash collisions.
+    The output should still be invariant to node-relabeling
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G1 = nx.erdos_renyi_graph(n, p * i, seed=400 + i)
+
+        for u in G1.nodes():
+            G1.nodes[u]["node_attr1"] = f"{u}-1"
+            G1.nodes[u]["node_attr2"] = f"{u}-2"
+
+        g1_hash_with_node_attr1 = nx.weisfeiler_lehman_graph_hash(
+            G1, node_attr="node_attr1"
+        )
+        g1_hash_with_node_attr2 = nx.weisfeiler_lehman_graph_hash(
+            G1, node_attr="node_attr2"
+        )
+        g1_hash_no_node_attr = nx.weisfeiler_lehman_graph_hash(G1, node_attr=None)
+
+        assert g1_hash_with_node_attr1 != g1_hash_no_node_attr
+        assert g1_hash_with_node_attr2 != g1_hash_no_node_attr
+        assert g1_hash_with_node_attr1 != g1_hash_with_node_attr2
+
+        G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+        g2_hash_with_node_attr1 = nx.weisfeiler_lehman_graph_hash(
+            G2, node_attr="node_attr1"
+        )
+        g2_hash_with_node_attr2 = nx.weisfeiler_lehman_graph_hash(
+            G2, node_attr="node_attr2"
+        )
+
+        assert g1_hash_with_node_attr1 == g2_hash_with_node_attr1
+        assert g1_hash_with_node_attr2 == g2_hash_with_node_attr2
+
+
+def test_missing_node_attr():
+    """
+    If the 'node_attr' argument is supplied but is missing from a node in the graph,
+    we should raise a KeyError
+    """
+    G = nx.Graph()
+    G.add_nodes_from([(1, {"node_attr1": "a"}), (2, {})])
+    G.add_edges_from([(1, 2), (2, 3), (3, 1), (1, 4)])
+    pytest.raises(KeyError, nx.weisfeiler_lehman_graph_hash, G, node_attr="node_attr1")
+
+
+def test_isomorphic_edge_attr_and_node_attr():
+    """
+    Isomorphic graphs with differing node attributes should yield different graph
+    hashes if the 'node_attr' and 'edge_attr' argument is supplied and populated in
+    the graph, and there are no hash collisions.
+    The output should still be invariant to node-relabeling
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i)
+
+        for u in G1.nodes():
+            G1.nodes[u]["node_attr1"] = f"{u}-1"
+            G1.nodes[u]["node_attr2"] = f"{u}-2"
+
+        for a, b in G1.edges:
+            G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
+            G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
+
+        g1_hash_edge1_node1 = nx.weisfeiler_lehman_graph_hash(
+            G1, edge_attr="edge_attr1", node_attr="node_attr1"
+        )
+        g1_hash_edge2_node2 = nx.weisfeiler_lehman_graph_hash(
+            G1, edge_attr="edge_attr2", node_attr="node_attr2"
+        )
+        g1_hash_edge1_node2 = nx.weisfeiler_lehman_graph_hash(
+            G1, edge_attr="edge_attr1", node_attr="node_attr2"
+        )
+        g1_hash_no_attr = nx.weisfeiler_lehman_graph_hash(G1)
+
+        assert g1_hash_edge1_node1 != g1_hash_no_attr
+        assert g1_hash_edge2_node2 != g1_hash_no_attr
+        assert g1_hash_edge1_node1 != g1_hash_edge2_node2
+        assert g1_hash_edge1_node2 != g1_hash_edge2_node2
+        assert g1_hash_edge1_node2 != g1_hash_edge1_node1
+
+        G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+        g2_hash_edge1_node1 = nx.weisfeiler_lehman_graph_hash(
+            G2, edge_attr="edge_attr1", node_attr="node_attr1"
+        )
+        g2_hash_edge2_node2 = nx.weisfeiler_lehman_graph_hash(
+            G2, edge_attr="edge_attr2", node_attr="node_attr2"
+        )
+
+        assert g1_hash_edge1_node1 == g2_hash_edge1_node1
+        assert g1_hash_edge2_node2 == g2_hash_edge2_node2
+
+
+def test_digest_size():
+    """
+    The hash string lengths should be as expected for a variety of graphs and
+    digest sizes
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G = nx.erdos_renyi_graph(n, p * i, seed=1000 + i)
+
+        h16 = nx.weisfeiler_lehman_graph_hash(G)
+        h32 = nx.weisfeiler_lehman_graph_hash(G, digest_size=32)
+
+        assert h16 != h32
+        assert len(h16) == 16 * 2
+        assert len(h32) == 32 * 2
+
+
+# Unit tests for the :func:`~networkx.weisfeiler_lehman_hash_subgraphs` function
+
+
+def is_subiteration(a, b):
+    """
+    returns True if that each hash sequence in 'a' is a prefix for
+    the corresponding sequence indexed by the same node in 'b'.
+    """
+    return all(b[node][: len(hashes)] == hashes for node, hashes in a.items())
+
+
+def hexdigest_sizes_correct(a, digest_size):
+    """
+    returns True if all hex digest sizes are the expected length in a node:subgraph-hashes
+    dictionary. Hex digest string length == 2 * bytes digest length since each pair of hex
+    digits encodes 1 byte (https://docs.python.org/3/library/hashlib.html)
+    """
+    hexdigest_size = digest_size * 2
+    list_digest_sizes_correct = lambda l: all(len(x) == hexdigest_size for x in l)
+    return all(list_digest_sizes_correct(hashes) for hashes in a.values())
+
+
+def test_empty_graph_subgraph_hash():
+    """ "
+    empty graphs should give empty dict subgraph hashes regardless of other params
+    """
+    G = nx.empty_graph()
+
+    subgraph_hashes1 = nx.weisfeiler_lehman_subgraph_hashes(G)
+    subgraph_hashes2 = nx.weisfeiler_lehman_subgraph_hashes(G, edge_attr="edge_attr")
+    subgraph_hashes3 = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="edge_attr")
+    subgraph_hashes4 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=2)
+    subgraph_hashes5 = nx.weisfeiler_lehman_subgraph_hashes(G, digest_size=64)
+
+    assert subgraph_hashes1 == {}
+    assert subgraph_hashes2 == {}
+    assert subgraph_hashes3 == {}
+    assert subgraph_hashes4 == {}
+    assert subgraph_hashes5 == {}
+
+
+def test_directed_subgraph_hash():
+    """
+    A directed graph with no bi-directional edges should yield different subgraph hashes
+    to the same graph taken as undirected, if all hashes don't collide.
+    """
+    r = 10
+    for i in range(r):
+        G_directed = nx.gn_graph(10 + r, seed=100 + i)
+        G_undirected = nx.to_undirected(G_directed)
+
+        directed_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G_directed)
+        undirected_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G_undirected)
+
+        assert directed_subgraph_hashes != undirected_subgraph_hashes
+
+
+def test_reversed_subgraph_hash():
+    """
+    A directed graph with no bi-directional edges should yield different subgraph hashes
+    to the same graph taken with edge directions reversed if there are no hash collisions.
+    Here we test a cycle graph which is the minimal counterexample
+    """
+    G = nx.cycle_graph(5, create_using=nx.DiGraph)
+    nx.set_node_attributes(G, {n: str(n) for n in G.nodes()}, name="label")
+
+    G_reversed = G.reverse()
+
+    h = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="label")
+    h_reversed = nx.weisfeiler_lehman_subgraph_hashes(G_reversed, node_attr="label")
+
+    assert h != h_reversed
+
+
+def test_isomorphic_subgraph_hash():
+    """
+    the subgraph hashes should be invariant to node-relabeling when the output is reindexed
+    by the same mapping and all hashes don't collide.
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G1 = nx.erdos_renyi_graph(n, p * i, seed=200 + i)
+        G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+        g1_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G1)
+        g2_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G2)
+
+        assert g1_subgraph_hashes == {-1 * k: v for k, v in g2_subgraph_hashes.items()}
+
+
+def test_isomorphic_edge_attr_subgraph_hash():
+    """
+    Isomorphic graphs with differing edge attributes should yield different subgraph
+    hashes if the 'edge_attr' argument is supplied and populated in the graph, and
+    all hashes don't collide.
+    The output should still be invariant to node-relabeling
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G1 = nx.erdos_renyi_graph(n, p * i, seed=300 + i)
+
+        for a, b in G1.edges:
+            G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
+            G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
+
+        g1_hash_with_edge_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
+            G1, edge_attr="edge_attr1"
+        )
+        g1_hash_with_edge_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
+            G1, edge_attr="edge_attr2"
+        )
+        g1_hash_no_edge_attr = nx.weisfeiler_lehman_subgraph_hashes(G1, edge_attr=None)
+
+        assert g1_hash_with_edge_attr1 != g1_hash_no_edge_attr
+        assert g1_hash_with_edge_attr2 != g1_hash_no_edge_attr
+        assert g1_hash_with_edge_attr1 != g1_hash_with_edge_attr2
+
+        G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+        g2_hash_with_edge_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
+            G2, edge_attr="edge_attr1"
+        )
+        g2_hash_with_edge_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
+            G2, edge_attr="edge_attr2"
+        )
+
+        assert g1_hash_with_edge_attr1 == {
+            -1 * k: v for k, v in g2_hash_with_edge_attr1.items()
+        }
+        assert g1_hash_with_edge_attr2 == {
+            -1 * k: v for k, v in g2_hash_with_edge_attr2.items()
+        }
+
+
+def test_missing_edge_attr_subgraph_hash():
+    """
+    If the 'edge_attr' argument is supplied but is missing from an edge in the graph,
+    we should raise a KeyError
+    """
+    G = nx.Graph()
+    G.add_edges_from([(1, 2, {"edge_attr1": "a"}), (1, 3, {})])
+    pytest.raises(
+        KeyError, nx.weisfeiler_lehman_subgraph_hashes, G, edge_attr="edge_attr1"
+    )
+
+
+def test_isomorphic_node_attr_subgraph_hash():
+    """
+    Isomorphic graphs with differing node attributes should yield different subgraph
+    hashes if the 'node_attr' argument is supplied and populated in the graph, and
+    all hashes don't collide.
+    The output should still be invariant to node-relabeling
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G1 = nx.erdos_renyi_graph(n, p * i, seed=400 + i)
+
+        for u in G1.nodes():
+            G1.nodes[u]["node_attr1"] = f"{u}-1"
+            G1.nodes[u]["node_attr2"] = f"{u}-2"
+
+        g1_hash_with_node_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
+            G1, node_attr="node_attr1"
+        )
+        g1_hash_with_node_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
+            G1, node_attr="node_attr2"
+        )
+        g1_hash_no_node_attr = nx.weisfeiler_lehman_subgraph_hashes(G1, node_attr=None)
+
+        assert g1_hash_with_node_attr1 != g1_hash_no_node_attr
+        assert g1_hash_with_node_attr2 != g1_hash_no_node_attr
+        assert g1_hash_with_node_attr1 != g1_hash_with_node_attr2
+
+        G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+        g2_hash_with_node_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
+            G2, node_attr="node_attr1"
+        )
+        g2_hash_with_node_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
+            G2, node_attr="node_attr2"
+        )
+
+        assert g1_hash_with_node_attr1 == {
+            -1 * k: v for k, v in g2_hash_with_node_attr1.items()
+        }
+        assert g1_hash_with_node_attr2 == {
+            -1 * k: v for k, v in g2_hash_with_node_attr2.items()
+        }
+
+
+def test_missing_node_attr_subgraph_hash():
+    """
+    If the 'node_attr' argument is supplied but is missing from a node in the graph,
+    we should raise a KeyError
+    """
+    G = nx.Graph()
+    G.add_nodes_from([(1, {"node_attr1": "a"}), (2, {})])
+    G.add_edges_from([(1, 2), (2, 3), (3, 1), (1, 4)])
+    pytest.raises(
+        KeyError, nx.weisfeiler_lehman_subgraph_hashes, G, node_attr="node_attr1"
+    )
+
+
+def test_isomorphic_edge_attr_and_node_attr_subgraph_hash():
+    """
+    Isomorphic graphs with differing node attributes should yield different subgraph
+    hashes if the 'node_attr' and 'edge_attr' argument is supplied and populated in
+    the graph, and all hashes don't collide
+    The output should still be invariant to node-relabeling
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i)
+
+        for u in G1.nodes():
+            G1.nodes[u]["node_attr1"] = f"{u}-1"
+            G1.nodes[u]["node_attr2"] = f"{u}-2"
+
+        for a, b in G1.edges:
+            G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
+            G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
+
+        g1_hash_edge1_node1 = nx.weisfeiler_lehman_subgraph_hashes(
+            G1, edge_attr="edge_attr1", node_attr="node_attr1"
+        )
+        g1_hash_edge2_node2 = nx.weisfeiler_lehman_subgraph_hashes(
+            G1, edge_attr="edge_attr2", node_attr="node_attr2"
+        )
+        g1_hash_edge1_node2 = nx.weisfeiler_lehman_subgraph_hashes(
+            G1, edge_attr="edge_attr1", node_attr="node_attr2"
+        )
+        g1_hash_no_attr = nx.weisfeiler_lehman_subgraph_hashes(G1)
+
+        assert g1_hash_edge1_node1 != g1_hash_no_attr
+        assert g1_hash_edge2_node2 != g1_hash_no_attr
+        assert g1_hash_edge1_node1 != g1_hash_edge2_node2
+        assert g1_hash_edge1_node2 != g1_hash_edge2_node2
+        assert g1_hash_edge1_node2 != g1_hash_edge1_node1
+
+        G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+        g2_hash_edge1_node1 = nx.weisfeiler_lehman_subgraph_hashes(
+            G2, edge_attr="edge_attr1", node_attr="node_attr1"
+        )
+        g2_hash_edge2_node2 = nx.weisfeiler_lehman_subgraph_hashes(
+            G2, edge_attr="edge_attr2", node_attr="node_attr2"
+        )
+
+        assert g1_hash_edge1_node1 == {
+            -1 * k: v for k, v in g2_hash_edge1_node1.items()
+        }
+        assert g1_hash_edge2_node2 == {
+            -1 * k: v for k, v in g2_hash_edge2_node2.items()
+        }
+
+
+def test_iteration_depth():
+    """
+    All nodes should have the correct number of subgraph hashes in the output when
+    using degree as initial node labels
+    Subsequent iteration depths for the same graph should be additive for each node
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G = nx.erdos_renyi_graph(n, p * i, seed=600 + i)
+
+        depth3 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=3)
+        depth4 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=4)
+        depth5 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=5)
+
+        assert all(len(hashes) == 3 for hashes in depth3.values())
+        assert all(len(hashes) == 4 for hashes in depth4.values())
+        assert all(len(hashes) == 5 for hashes in depth5.values())
+
+        assert is_subiteration(depth3, depth4)
+        assert is_subiteration(depth4, depth5)
+        assert is_subiteration(depth3, depth5)
+
+
+def test_iteration_depth_edge_attr():
+    """
+    All nodes should have the correct number of subgraph hashes in the output when
+    setting initial node labels empty and using an edge attribute when aggregating
+    neighborhoods.
+    Subsequent iteration depths for the same graph should be additive for each node
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G = nx.erdos_renyi_graph(n, p * i, seed=700 + i)
+
+        for a, b in G.edges:
+            G[a][b]["edge_attr1"] = f"{a}-{b}-1"
+
+        depth3 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, edge_attr="edge_attr1", iterations=3
+        )
+        depth4 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, edge_attr="edge_attr1", iterations=4
+        )
+        depth5 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, edge_attr="edge_attr1", iterations=5
+        )
+
+        assert all(len(hashes) == 3 for hashes in depth3.values())
+        assert all(len(hashes) == 4 for hashes in depth4.values())
+        assert all(len(hashes) == 5 for hashes in depth5.values())
+
+        assert is_subiteration(depth3, depth4)
+        assert is_subiteration(depth4, depth5)
+        assert is_subiteration(depth3, depth5)
+
+
+def test_iteration_depth_node_attr():
+    """
+    All nodes should have the correct number of subgraph hashes in the output when
+    setting initial node labels to an attribute.
+    Subsequent iteration depths for the same graph should be additive for each node
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G = nx.erdos_renyi_graph(n, p * i, seed=800 + i)
+
+        for u in G.nodes():
+            G.nodes[u]["node_attr1"] = f"{u}-1"
+
+        depth3 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, node_attr="node_attr1", iterations=3
+        )
+        depth4 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, node_attr="node_attr1", iterations=4
+        )
+        depth5 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, node_attr="node_attr1", iterations=5
+        )
+
+        assert all(len(hashes) == 3 for hashes in depth3.values())
+        assert all(len(hashes) == 4 for hashes in depth4.values())
+        assert all(len(hashes) == 5 for hashes in depth5.values())
+
+        assert is_subiteration(depth3, depth4)
+        assert is_subiteration(depth4, depth5)
+        assert is_subiteration(depth3, depth5)
+
+
+def test_iteration_depth_node_edge_attr():
+    """
+    All nodes should have the correct number of subgraph hashes in the output when
+    setting initial node labels to an attribute and also using an edge attribute when
+    aggregating neighborhoods.
+    Subsequent iteration depths for the same graph should be additive for each node
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G = nx.erdos_renyi_graph(n, p * i, seed=900 + i)
+
+        for u in G.nodes():
+            G.nodes[u]["node_attr1"] = f"{u}-1"
+
+        for a, b in G.edges:
+            G[a][b]["edge_attr1"] = f"{a}-{b}-1"
+
+        depth3 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=3
+        )
+        depth4 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=4
+        )
+        depth5 = nx.weisfeiler_lehman_subgraph_hashes(
+            G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=5
+        )
+
+        assert all(len(hashes) == 3 for hashes in depth3.values())
+        assert all(len(hashes) == 4 for hashes in depth4.values())
+        assert all(len(hashes) == 5 for hashes in depth5.values())
+
+        assert is_subiteration(depth3, depth4)
+        assert is_subiteration(depth4, depth5)
+        assert is_subiteration(depth3, depth5)
+
+
+def test_digest_size_subgraph_hash():
+    """
+    The hash string lengths should be as expected for a variety of graphs and
+    digest sizes
+    """
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(1, r + 1):
+        G = nx.erdos_renyi_graph(n, p * i, seed=1000 + i)
+
+        digest_size16_hashes = nx.weisfeiler_lehman_subgraph_hashes(G)
+        digest_size32_hashes = nx.weisfeiler_lehman_subgraph_hashes(G, digest_size=32)
+
+        assert digest_size16_hashes != digest_size32_hashes
+
+        assert hexdigest_sizes_correct(digest_size16_hashes, 16)
+        assert hexdigest_sizes_correct(digest_size32_hashes, 32)
+
+
+def test_initial_node_labels_subgraph_hash():
+    """
+    Including the hashed initial label prepends an extra hash to the lists
+    """
+    G = nx.path_graph(5)
+    nx.set_node_attributes(G, {i: int(0 < i < 4) for i in G}, "label")
+    # initial node labels:
+    # 0--1--1--1--0
+
+    without_initial_label = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="label")
+    assert all(len(v) == 3 for v in without_initial_label.values())
+    # 3 different 1 hop nhds
+    assert len({v[0] for v in without_initial_label.values()}) == 3
+
+    with_initial_label = nx.weisfeiler_lehman_subgraph_hashes(
+        G, node_attr="label", include_initial_labels=True
+    )
+    assert all(len(v) == 4 for v in with_initial_label.values())
+    # 2 different initial labels
+    assert len({v[0] for v in with_initial_label.values()}) == 2
+
+    # check hashes match otherwise
+    for u in G:
+        for a, b in zip(
+            with_initial_label[u][1:], without_initial_label[u], strict=True
+        ):
+            assert a == b
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graphical.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graphical.py
new file mode 100644
index 00000000..99f766f7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graphical.py
@@ -0,0 +1,163 @@
+import pytest
+
+import networkx as nx
+
+
+def test_valid_degree_sequence1():
+    n = 100
+    p = 0.3
+    for i in range(10):
+        G = nx.erdos_renyi_graph(n, p)
+        deg = (d for n, d in G.degree())
+        assert nx.is_graphical(deg, method="eg")
+        assert nx.is_graphical(deg, method="hh")
+
+
+def test_valid_degree_sequence2():
+    n = 100
+    for i in range(10):
+        G = nx.barabasi_albert_graph(n, 1)
+        deg = (d for n, d in G.degree())
+        assert nx.is_graphical(deg, method="eg")
+        assert nx.is_graphical(deg, method="hh")
+
+
+def test_string_input():
+    pytest.raises(nx.NetworkXException, nx.is_graphical, [], "foo")
+    pytest.raises(nx.NetworkXException, nx.is_graphical, ["red"], "hh")
+    pytest.raises(nx.NetworkXException, nx.is_graphical, ["red"], "eg")
+
+
+def test_non_integer_input():
+    pytest.raises(nx.NetworkXException, nx.is_graphical, [72.5], "eg")
+    pytest.raises(nx.NetworkXException, nx.is_graphical, [72.5], "hh")
+
+
+def test_negative_input():
+    assert not nx.is_graphical([-1], "hh")
+    assert not nx.is_graphical([-1], "eg")
+
+
+class TestAtlas:
+    @classmethod
+    def setup_class(cls):
+        global atlas
+        from networkx.generators import atlas
+
+        cls.GAG = atlas.graph_atlas_g()
+
+    def test_atlas(self):
+        for graph in self.GAG:
+            deg = (d for n, d in graph.degree())
+            assert nx.is_graphical(deg, method="eg")
+            assert nx.is_graphical(deg, method="hh")
+
+
+def test_small_graph_true():
+    z = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+    assert nx.is_graphical(z, method="hh")
+    assert nx.is_graphical(z, method="eg")
+    z = [10, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2]
+    assert nx.is_graphical(z, method="hh")
+    assert nx.is_graphical(z, method="eg")
+    z = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
+    assert nx.is_graphical(z, method="hh")
+    assert nx.is_graphical(z, method="eg")
+
+
+def test_small_graph_false():
+    z = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+    assert not nx.is_graphical(z, method="hh")
+    assert not nx.is_graphical(z, method="eg")
+    z = [6, 5, 4, 4, 2, 1, 1, 1]
+    assert not nx.is_graphical(z, method="hh")
+    assert not nx.is_graphical(z, method="eg")
+    z = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
+    assert not nx.is_graphical(z, method="hh")
+    assert not nx.is_graphical(z, method="eg")
+
+
+def test_directed_degree_sequence():
+    # Test a range of valid directed degree sequences
+    n, r = 100, 10
+    p = 1.0 / r
+    for i in range(r):
+        G = nx.erdos_renyi_graph(n, p * (i + 1), None, True)
+        din = (d for n, d in G.in_degree())
+        dout = (d for n, d in G.out_degree())
+        assert nx.is_digraphical(din, dout)
+
+
+def test_small_directed_sequences():
+    dout = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+    din = [3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1]
+    assert nx.is_digraphical(din, dout)
+    # Test nongraphical directed sequence
+    dout = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+    din = [103, 102, 102, 102, 102, 102, 102, 102, 102, 102]
+    assert not nx.is_digraphical(din, dout)
+    # Test digraphical small sequence
+    dout = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
+    din = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1]
+    assert nx.is_digraphical(din, dout)
+    # Test nonmatching sum
+    din = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1]
+    assert not nx.is_digraphical(din, dout)
+    # Test for negative integer in sequence
+    din = [2, 2, 2, -2, 2, 2, 2, 2, 1, 1, 4]
+    assert not nx.is_digraphical(din, dout)
+    # Test for noninteger
+    din = dout = [1, 1, 1.1, 1]
+    assert not nx.is_digraphical(din, dout)
+    din = dout = [1, 1, "rer", 1]
+    assert not nx.is_digraphical(din, dout)
+
+
+def test_multi_sequence():
+    # Test nongraphical multi sequence
+    seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1]
+    assert not nx.is_multigraphical(seq)
+    # Test small graphical multi sequence
+    seq = [6, 5, 4, 4, 2, 1, 1, 1]
+    assert nx.is_multigraphical(seq)
+    # Test for negative integer in sequence
+    seq = [6, 5, 4, -4, 2, 1, 1, 1]
+    assert not nx.is_multigraphical(seq)
+    # Test for sequence with odd sum
+    seq = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
+    assert not nx.is_multigraphical(seq)
+    # Test for noninteger
+    seq = [1, 1, 1.1, 1]
+    assert not nx.is_multigraphical(seq)
+    seq = [1, 1, "rer", 1]
+    assert not nx.is_multigraphical(seq)
+
+
+def test_pseudo_sequence():
+    # Test small valid pseudo sequence
+    seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1]
+    assert nx.is_pseudographical(seq)
+    # Test for sequence with odd sum
+    seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+    assert not nx.is_pseudographical(seq)
+    # Test for negative integer in sequence
+    seq = [1000, 3, 3, 3, 3, 2, 2, -2, 1, 1]
+    assert not nx.is_pseudographical(seq)
+    # Test for noninteger
+    seq = [1, 1, 1.1, 1]
+    assert not nx.is_pseudographical(seq)
+    seq = [1, 1, "rer", 1]
+    assert not nx.is_pseudographical(seq)
+
+
+def test_numpy_degree_sequence():
+    np = pytest.importorskip("numpy")
+    ds = np.array([1, 2, 2, 2, 1], dtype=np.int64)
+    assert nx.is_graphical(ds, "eg")
+    assert nx.is_graphical(ds, "hh")
+    ds = np.array([1, 2, 2, 2, 1], dtype=np.float64)
+    assert nx.is_graphical(ds, "eg")
+    assert nx.is_graphical(ds, "hh")
+    ds = np.array([1.1, 2, 2, 2, 1], dtype=np.float64)
+    pytest.raises(nx.NetworkXException, nx.is_graphical, ds, "eg")
+    pytest.raises(nx.NetworkXException, nx.is_graphical, ds, "hh")
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_hierarchy.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_hierarchy.py
new file mode 100644
index 00000000..eaa6a67b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_hierarchy.py
@@ -0,0 +1,46 @@
+import pytest
+
+import networkx as nx
+
+
+def test_hierarchy_undirected():
+    G = nx.cycle_graph(5)
+    pytest.raises(nx.NetworkXError, nx.flow_hierarchy, G)
+
+
+def test_hierarchy_cycle():
+    G = nx.cycle_graph(5, create_using=nx.DiGraph())
+    assert nx.flow_hierarchy(G) == 0.0
+
+
+def test_hierarchy_tree():
+    G = nx.full_rary_tree(2, 16, create_using=nx.DiGraph())
+    assert nx.flow_hierarchy(G) == 1.0
+
+
+def test_hierarchy_1():
+    G = nx.DiGraph()
+    G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 1), (3, 4), (0, 4)])
+    assert nx.flow_hierarchy(G) == 0.5
+
+
+def test_hierarchy_weight():
+    G = nx.DiGraph()
+    G.add_edges_from(
+        [
+            (0, 1, {"weight": 0.3}),
+            (1, 2, {"weight": 0.1}),
+            (2, 3, {"weight": 0.1}),
+            (3, 1, {"weight": 0.1}),
+            (3, 4, {"weight": 0.3}),
+            (0, 4, {"weight": 0.3}),
+        ]
+    )
+    assert nx.flow_hierarchy(G, weight="weight") == 0.75
+
+
+@pytest.mark.parametrize("n", (0, 1, 3))
+def test_hierarchy_empty_graph(n):
+    G = nx.empty_graph(n, create_using=nx.DiGraph)
+    with pytest.raises(nx.NetworkXError, match=".*not applicable to empty graphs"):
+        nx.flow_hierarchy(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_hybrid.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_hybrid.py
new file mode 100644
index 00000000..6af00164
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_hybrid.py
@@ -0,0 +1,24 @@
+import networkx as nx
+
+
+def test_2d_grid_graph():
+    # FC article claims 2d grid graph of size n is (3,3)-connected
+    # and (5,9)-connected, but I don't think it is (5,9)-connected
+    G = nx.grid_2d_graph(8, 8, periodic=True)
+    assert nx.is_kl_connected(G, 3, 3)
+    assert not nx.is_kl_connected(G, 5, 9)
+    (H, graphOK) = nx.kl_connected_subgraph(G, 5, 9, same_as_graph=True)
+    assert not graphOK
+
+
+def test_small_graph():
+    G = nx.Graph()
+    G.add_edge(1, 2)
+    G.add_edge(1, 3)
+    G.add_edge(2, 3)
+    assert nx.is_kl_connected(G, 2, 2)
+    H = nx.kl_connected_subgraph(G, 2, 2)
+    (H, graphOK) = nx.kl_connected_subgraph(
+        G, 2, 2, low_memory=True, same_as_graph=True
+    )
+    assert graphOK
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_isolate.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_isolate.py
new file mode 100644
index 00000000..d29b306d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_isolate.py
@@ -0,0 +1,26 @@
+"""Unit tests for the :mod:`networkx.algorithms.isolates` module."""
+
+import networkx as nx
+
+
+def test_is_isolate():
+    G = nx.Graph()
+    G.add_edge(0, 1)
+    G.add_node(2)
+    assert not nx.is_isolate(G, 0)
+    assert not nx.is_isolate(G, 1)
+    assert nx.is_isolate(G, 2)
+
+
+def test_isolates():
+    G = nx.Graph()
+    G.add_edge(0, 1)
+    G.add_nodes_from([2, 3])
+    assert sorted(nx.isolates(G)) == [2, 3]
+
+
+def test_number_of_isolates():
+    G = nx.Graph()
+    G.add_edge(0, 1)
+    G.add_nodes_from([2, 3])
+    assert nx.number_of_isolates(G) == 2
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_link_prediction.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_link_prediction.py
new file mode 100644
index 00000000..0878496b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_link_prediction.py
@@ -0,0 +1,586 @@
+import math
+from functools import partial
+
+import pytest
+
+import networkx as nx
+
+
+def _test_func(G, ebunch, expected, predict_func, **kwargs):
+    result = predict_func(G, ebunch, **kwargs)
+    exp_dict = {tuple(sorted([u, v])): score for u, v, score in expected}
+    res_dict = {tuple(sorted([u, v])): score for u, v, score in result}
+
+    assert len(exp_dict) == len(res_dict)
+    for p in exp_dict:
+        assert exp_dict[p] == pytest.approx(res_dict[p], abs=1e-7)
+
+
+class TestResourceAllocationIndex:
+    @classmethod
+    def setup_class(cls):
+        cls.func = staticmethod(nx.resource_allocation_index)
+        cls.test = partial(_test_func, predict_func=cls.func)
+
+    def test_K5(self):
+        G = nx.complete_graph(5)
+        self.test(G, [(0, 1)], [(0, 1, 0.75)])
+
+    def test_P3(self):
+        G = nx.path_graph(3)
+        self.test(G, [(0, 2)], [(0, 2, 0.5)])
+
+    def test_S4(self):
+        G = nx.star_graph(4)
+        self.test(G, [(1, 2)], [(1, 2, 0.25)])
+
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        assert pytest.raises(
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
+        )
+
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
+    def test_no_common_neighbor(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1])
+        self.test(G, [(0, 1)], [(0, 1, 0)])
+
+    def test_equal_nodes(self):
+        G = nx.complete_graph(4)
+        self.test(G, [(0, 0)], [(0, 0, 1)])
+
+    def test_all_nonexistent_edges(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        self.test(G, None, [(0, 3, 0.5), (1, 2, 0.5), (1, 3, 0)])
+
+
+class TestJaccardCoefficient:
+    @classmethod
+    def setup_class(cls):
+        cls.func = staticmethod(nx.jaccard_coefficient)
+        cls.test = partial(_test_func, predict_func=cls.func)
+
+    def test_K5(self):
+        G = nx.complete_graph(5)
+        self.test(G, [(0, 1)], [(0, 1, 0.6)])
+
+    def test_P4(self):
+        G = nx.path_graph(4)
+        self.test(G, [(0, 2)], [(0, 2, 0.5)])
+
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        assert pytest.raises(
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
+        )
+
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
+    def test_no_common_neighbor(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (2, 3)])
+        self.test(G, [(0, 2)], [(0, 2, 0)])
+
+    def test_isolated_nodes(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1])
+        self.test(G, [(0, 1)], [(0, 1, 0)])
+
+    def test_all_nonexistent_edges(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        self.test(G, None, [(0, 3, 0.5), (1, 2, 0.5), (1, 3, 0)])
+
+
+class TestAdamicAdarIndex:
+    @classmethod
+    def setup_class(cls):
+        cls.func = staticmethod(nx.adamic_adar_index)
+        cls.test = partial(_test_func, predict_func=cls.func)
+
+    def test_K5(self):
+        G = nx.complete_graph(5)
+        self.test(G, [(0, 1)], [(0, 1, 3 / math.log(4))])
+
+    def test_P3(self):
+        G = nx.path_graph(3)
+        self.test(G, [(0, 2)], [(0, 2, 1 / math.log(2))])
+
+    def test_S4(self):
+        G = nx.star_graph(4)
+        self.test(G, [(1, 2)], [(1, 2, 1 / math.log(4))])
+
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        assert pytest.raises(
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
+        )
+
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
+    def test_no_common_neighbor(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1])
+        self.test(G, [(0, 1)], [(0, 1, 0)])
+
+    def test_equal_nodes(self):
+        G = nx.complete_graph(4)
+        self.test(G, [(0, 0)], [(0, 0, 3 / math.log(3))])
+
+    def test_all_nonexistent_edges(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        self.test(
+            G, None, [(0, 3, 1 / math.log(2)), (1, 2, 1 / math.log(2)), (1, 3, 0)]
+        )
+
+
+class TestCommonNeighborCentrality:
+    @classmethod
+    def setup_class(cls):
+        cls.func = staticmethod(nx.common_neighbor_centrality)
+        cls.test = partial(_test_func, predict_func=cls.func)
+
+    def test_K5(self):
+        G = nx.complete_graph(5)
+        self.test(G, [(0, 1)], [(0, 1, 3.0)], alpha=1)
+        self.test(G, [(0, 1)], [(0, 1, 5.0)], alpha=0)
+
+    def test_P3(self):
+        G = nx.path_graph(3)
+        self.test(G, [(0, 2)], [(0, 2, 1.25)], alpha=0.5)
+
+    def test_S4(self):
+        G = nx.star_graph(4)
+        self.test(G, [(1, 2)], [(1, 2, 1.75)], alpha=0.5)
+
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        assert pytest.raises(
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
+        )
+
+    def test_node_u_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(1, 3), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 1)])
+
+    def test_node_v_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
+    def test_no_common_neighbor(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1])
+        self.test(G, [(0, 1)], [(0, 1, 0)])
+
+    def test_equal_nodes(self):
+        G = nx.complete_graph(4)
+        assert pytest.raises(nx.NetworkXAlgorithmError, self.test, G, [(0, 0)], [])
+
+    def test_equal_nodes_with_alpha_one_raises_error(self):
+        G = nx.complete_graph(4)
+        assert pytest.raises(
+            nx.NetworkXAlgorithmError, self.test, G, [(0, 0)], [], alpha=1.0
+        )
+
+    def test_all_nonexistent_edges(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        self.test(G, None, [(0, 3, 1.5), (1, 2, 1.5), (1, 3, 2 / 3)], alpha=0.5)
+
+
+class TestPreferentialAttachment:
+    @classmethod
+    def setup_class(cls):
+        cls.func = staticmethod(nx.preferential_attachment)
+        cls.test = partial(_test_func, predict_func=cls.func)
+
+    def test_K5(self):
+        G = nx.complete_graph(5)
+        self.test(G, [(0, 1)], [(0, 1, 16)])
+
+    def test_P3(self):
+        G = nx.path_graph(3)
+        self.test(G, [(0, 1)], [(0, 1, 2)])
+
+    def test_S4(self):
+        G = nx.star_graph(4)
+        self.test(G, [(0, 2)], [(0, 2, 4)])
+
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        assert pytest.raises(
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
+        )
+
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
+    def test_zero_degrees(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1])
+        self.test(G, [(0, 1)], [(0, 1, 0)])
+
+    def test_all_nonexistent_edges(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        self.test(G, None, [(0, 3, 2), (1, 2, 2), (1, 3, 1)])
+
+
+class TestCNSoundarajanHopcroft:
+    @classmethod
+    def setup_class(cls):
+        cls.func = staticmethod(nx.cn_soundarajan_hopcroft)
+        cls.test = partial(_test_func, predict_func=cls.func, community="community")
+
+    def test_K5(self):
+        G = nx.complete_graph(5)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 1
+        self.test(G, [(0, 1)], [(0, 1, 5)])
+
+    def test_P3(self):
+        G = nx.path_graph(3)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        self.test(G, [(0, 2)], [(0, 2, 1)])
+
+    def test_S4(self):
+        G = nx.star_graph(4)
+        G.nodes[0]["community"] = 1
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 1
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 0
+        self.test(G, [(1, 2)], [(1, 2, 2)])
+
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        G = graph_type([(0, 1), (1, 2)])
+        G.add_nodes_from([0, 1, 2], community=0)
+        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
+
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
+    def test_no_common_neighbor(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        self.test(G, [(0, 1)], [(0, 1, 0)])
+
+    def test_equal_nodes(self):
+        G = nx.complete_graph(3)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        self.test(G, [(0, 0)], [(0, 0, 4)])
+
+    def test_different_community(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 1
+        self.test(G, [(0, 3)], [(0, 3, 2)])
+
+    def test_no_community_information(self):
+        G = nx.complete_graph(5)
+        assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 1)]))
+
+    def test_insufficient_community_information(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 3)]))
+
+    def test_sufficient_community_information(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)])
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 0
+        self.test(G, [(1, 4)], [(1, 4, 4)])
+
+    def test_custom_community_attribute_name(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
+        G.nodes[0]["cmty"] = 0
+        G.nodes[1]["cmty"] = 0
+        G.nodes[2]["cmty"] = 0
+        G.nodes[3]["cmty"] = 1
+        self.test(G, [(0, 3)], [(0, 3, 2)], community="cmty")
+
+    def test_all_nonexistent_edges(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        self.test(G, None, [(0, 3, 2), (1, 2, 1), (1, 3, 0)])
+
+
+class TestRAIndexSoundarajanHopcroft:
+    @classmethod
+    def setup_class(cls):
+        cls.func = staticmethod(nx.ra_index_soundarajan_hopcroft)
+        cls.test = partial(_test_func, predict_func=cls.func, community="community")
+
+    def test_K5(self):
+        G = nx.complete_graph(5)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 1
+        self.test(G, [(0, 1)], [(0, 1, 0.5)])
+
+    def test_P3(self):
+        G = nx.path_graph(3)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        self.test(G, [(0, 2)], [(0, 2, 0)])
+
+    def test_S4(self):
+        G = nx.star_graph(4)
+        G.nodes[0]["community"] = 1
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 1
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 0
+        self.test(G, [(1, 2)], [(1, 2, 0.25)])
+
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        G = graph_type([(0, 1), (1, 2)])
+        G.add_nodes_from([0, 1, 2], community=0)
+        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
+
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
+    def test_no_common_neighbor(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        self.test(G, [(0, 1)], [(0, 1, 0)])
+
+    def test_equal_nodes(self):
+        G = nx.complete_graph(3)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        self.test(G, [(0, 0)], [(0, 0, 1)])
+
+    def test_different_community(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 1
+        self.test(G, [(0, 3)], [(0, 3, 0)])
+
+    def test_no_community_information(self):
+        G = nx.complete_graph(5)
+        assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 1)]))
+
+    def test_insufficient_community_information(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 3)]))
+
+    def test_sufficient_community_information(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)])
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 0
+        self.test(G, [(1, 4)], [(1, 4, 1)])
+
+    def test_custom_community_attribute_name(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
+        G.nodes[0]["cmty"] = 0
+        G.nodes[1]["cmty"] = 0
+        G.nodes[2]["cmty"] = 0
+        G.nodes[3]["cmty"] = 1
+        self.test(G, [(0, 3)], [(0, 3, 0)], community="cmty")
+
+    def test_all_nonexistent_edges(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        self.test(G, None, [(0, 3, 0.5), (1, 2, 0), (1, 3, 0)])
+
+
+class TestWithinInterCluster:
+    @classmethod
+    def setup_class(cls):
+        cls.delta = 0.001
+        cls.func = staticmethod(nx.within_inter_cluster)
+        cls.test = partial(
+            _test_func, predict_func=cls.func, delta=cls.delta, community="community"
+        )
+
+    def test_K5(self):
+        G = nx.complete_graph(5)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 1
+        self.test(G, [(0, 1)], [(0, 1, 2 / (1 + self.delta))])
+
+    def test_P3(self):
+        G = nx.path_graph(3)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        self.test(G, [(0, 2)], [(0, 2, 0)])
+
+    def test_S4(self):
+        G = nx.star_graph(4)
+        G.nodes[0]["community"] = 1
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 1
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 0
+        self.test(G, [(1, 2)], [(1, 2, 1 / self.delta)])
+
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        G = graph_type([(0, 1), (1, 2)])
+        G.add_nodes_from([0, 1, 2], community=0)
+        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
+
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
+    def test_no_common_neighbor(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        self.test(G, [(0, 1)], [(0, 1, 0)])
+
+    def test_equal_nodes(self):
+        G = nx.complete_graph(3)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        self.test(G, [(0, 0)], [(0, 0, 2 / self.delta)])
+
+    def test_different_community(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 1
+        self.test(G, [(0, 3)], [(0, 3, 0)])
+
+    def test_no_inter_cluster_common_neighbor(self):
+        G = nx.complete_graph(4)
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        self.test(G, [(0, 3)], [(0, 3, 2 / self.delta)])
+
+    def test_no_community_information(self):
+        G = nx.complete_graph(5)
+        assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 1)]))
+
+    def test_insufficient_community_information(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 3)]))
+
+    def test_sufficient_community_information(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)])
+        G.nodes[1]["community"] = 0
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        G.nodes[4]["community"] = 0
+        self.test(G, [(1, 4)], [(1, 4, 2 / self.delta)])
+
+    def test_invalid_delta(self):
+        G = nx.complete_graph(3)
+        G.add_nodes_from([0, 1, 2], community=0)
+        assert pytest.raises(nx.NetworkXAlgorithmError, self.func, G, [(0, 1)], 0)
+        assert pytest.raises(nx.NetworkXAlgorithmError, self.func, G, [(0, 1)], -0.5)
+
+    def test_custom_community_attribute_name(self):
+        G = nx.complete_graph(4)
+        G.nodes[0]["cmty"] = 0
+        G.nodes[1]["cmty"] = 0
+        G.nodes[2]["cmty"] = 0
+        G.nodes[3]["cmty"] = 0
+        self.test(G, [(0, 3)], [(0, 3, 2 / self.delta)], community="cmty")
+
+    def test_all_nonexistent_edges(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        self.test(G, None, [(0, 3, 1 / self.delta), (1, 2, 0), (1, 3, 0)])
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py
new file mode 100644
index 00000000..66d75220
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py
@@ -0,0 +1,427 @@
+from itertools import chain, combinations, product
+
+import pytest
+
+import networkx as nx
+
+tree_all_pairs_lca = nx.tree_all_pairs_lowest_common_ancestor
+all_pairs_lca = nx.all_pairs_lowest_common_ancestor
+
+
+def get_pair(dictionary, n1, n2):
+    if (n1, n2) in dictionary:
+        return dictionary[n1, n2]
+    else:
+        return dictionary[n2, n1]
+
+
+class TestTreeLCA:
+    @classmethod
+    def setup_class(cls):
+        cls.DG = nx.DiGraph()
+        edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
+        cls.DG.add_edges_from(edges)
+        cls.ans = dict(tree_all_pairs_lca(cls.DG, 0))
+        gold = {(n, n): n for n in cls.DG}
+        gold.update({(0, i): 0 for i in range(1, 7)})
+        gold.update(
+            {
+                (1, 2): 0,
+                (1, 3): 1,
+                (1, 4): 1,
+                (1, 5): 0,
+                (1, 6): 0,
+                (2, 3): 0,
+                (2, 4): 0,
+                (2, 5): 2,
+                (2, 6): 2,
+                (3, 4): 1,
+                (3, 5): 0,
+                (3, 6): 0,
+                (4, 5): 0,
+                (4, 6): 0,
+                (5, 6): 2,
+            }
+        )
+
+        cls.gold = gold
+
+    @staticmethod
+    def assert_has_same_pairs(d1, d2):
+        for a, b in ((min(pair), max(pair)) for pair in chain(d1, d2)):
+            assert get_pair(d1, a, b) == get_pair(d2, a, b)
+
+    def test_tree_all_pairs_lca_default_root(self):
+        assert dict(tree_all_pairs_lca(self.DG)) == self.ans
+
+    def test_tree_all_pairs_lca_return_subset(self):
+        test_pairs = [(0, 1), (0, 1), (1, 0)]
+        ans = dict(tree_all_pairs_lca(self.DG, 0, test_pairs))
+        assert (0, 1) in ans and (1, 0) in ans
+        assert len(ans) == 2
+
+    def test_tree_all_pairs_lca(self):
+        all_pairs = chain(combinations(self.DG, 2), ((node, node) for node in self.DG))
+
+        ans = dict(tree_all_pairs_lca(self.DG, 0, all_pairs))
+        self.assert_has_same_pairs(ans, self.ans)
+
+    def test_tree_all_pairs_gold_example(self):
+        ans = dict(tree_all_pairs_lca(self.DG))
+        self.assert_has_same_pairs(self.gold, ans)
+
+    def test_tree_all_pairs_lca_invalid_input(self):
+        empty_digraph = tree_all_pairs_lca(nx.DiGraph())
+        pytest.raises(nx.NetworkXPointlessConcept, list, empty_digraph)
+
+        bad_pairs_digraph = tree_all_pairs_lca(self.DG, pairs=[(-1, -2)])
+        pytest.raises(nx.NodeNotFound, list, bad_pairs_digraph)
+
+    def test_tree_all_pairs_lca_subtrees(self):
+        ans = dict(tree_all_pairs_lca(self.DG, 1))
+        gold = {
+            pair: lca
+            for (pair, lca) in self.gold.items()
+            if all(n in (1, 3, 4) for n in pair)
+        }
+        self.assert_has_same_pairs(gold, ans)
+
+    def test_tree_all_pairs_lca_disconnected_nodes(self):
+        G = nx.DiGraph()
+        G.add_node(1)
+        assert {(1, 1): 1} == dict(tree_all_pairs_lca(G))
+
+        G.add_node(0)
+        assert {(1, 1): 1} == dict(tree_all_pairs_lca(G, 1))
+        assert {(0, 0): 0} == dict(tree_all_pairs_lca(G, 0))
+
+        pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G))
+
+    def test_tree_all_pairs_lca_error_if_input_not_tree(self):
+        # Cycle
+        G = nx.DiGraph([(1, 2), (2, 1)])
+        pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G))
+        # DAG
+        G = nx.DiGraph([(0, 2), (1, 2)])
+        pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G))
+
+    def test_tree_all_pairs_lca_generator(self):
+        pairs = iter([(0, 1), (0, 1), (1, 0)])
+        some_pairs = dict(tree_all_pairs_lca(self.DG, 0, pairs))
+        assert (0, 1) in some_pairs and (1, 0) in some_pairs
+        assert len(some_pairs) == 2
+
+    def test_tree_all_pairs_lca_nonexisting_pairs_exception(self):
+        lca = tree_all_pairs_lca(self.DG, 0, [(-1, -1)])
+        pytest.raises(nx.NodeNotFound, list, lca)
+        # check if node is None
+        lca = tree_all_pairs_lca(self.DG, None, [(-1, -1)])
+        pytest.raises(nx.NodeNotFound, list, lca)
+
+    def test_tree_all_pairs_lca_routine_bails_on_DAGs(self):
+        G = nx.DiGraph([(3, 4), (5, 4)])
+        pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G))
+
+    def test_tree_all_pairs_lca_not_implemented(self):
+        NNI = nx.NetworkXNotImplemented
+        G = nx.Graph([(0, 1)])
+        with pytest.raises(NNI):
+            next(tree_all_pairs_lca(G))
+        with pytest.raises(NNI):
+            next(all_pairs_lca(G))
+        pytest.raises(NNI, nx.lowest_common_ancestor, G, 0, 1)
+        G = nx.MultiGraph([(0, 1)])
+        with pytest.raises(NNI):
+            next(tree_all_pairs_lca(G))
+        with pytest.raises(NNI):
+            next(all_pairs_lca(G))
+        pytest.raises(NNI, nx.lowest_common_ancestor, G, 0, 1)
+
+    def test_tree_all_pairs_lca_trees_without_LCAs(self):
+        G = nx.DiGraph()
+        G.add_node(3)
+        ans = list(tree_all_pairs_lca(G))
+        assert ans == [((3, 3), 3)]
+
+
+class TestMultiTreeLCA(TestTreeLCA):
+    @classmethod
+    def setup_class(cls):
+        cls.DG = nx.MultiDiGraph()
+        edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
+        cls.DG.add_edges_from(edges)
+        cls.ans = dict(tree_all_pairs_lca(cls.DG, 0))
+        # add multiedges
+        cls.DG.add_edges_from(edges)
+
+        gold = {(n, n): n for n in cls.DG}
+        gold.update({(0, i): 0 for i in range(1, 7)})
+        gold.update(
+            {
+                (1, 2): 0,
+                (1, 3): 1,
+                (1, 4): 1,
+                (1, 5): 0,
+                (1, 6): 0,
+                (2, 3): 0,
+                (2, 4): 0,
+                (2, 5): 2,
+                (2, 6): 2,
+                (3, 4): 1,
+                (3, 5): 0,
+                (3, 6): 0,
+                (4, 5): 0,
+                (4, 6): 0,
+                (5, 6): 2,
+            }
+        )
+
+        cls.gold = gold
+
+
+class TestDAGLCA:
+    @classmethod
+    def setup_class(cls):
+        cls.DG = nx.DiGraph()
+        nx.add_path(cls.DG, (0, 1, 2, 3))
+        nx.add_path(cls.DG, (0, 4, 3))
+        nx.add_path(cls.DG, (0, 5, 6, 8, 3))
+        nx.add_path(cls.DG, (5, 7, 8))
+        cls.DG.add_edge(6, 2)
+        cls.DG.add_edge(7, 2)
+
+        cls.root_distance = nx.shortest_path_length(cls.DG, source=0)
+
+        cls.gold = {
+            (1, 1): 1,
+            (1, 2): 1,
+            (1, 3): 1,
+            (1, 4): 0,
+            (1, 5): 0,
+            (1, 6): 0,
+            (1, 7): 0,
+            (1, 8): 0,
+            (2, 2): 2,
+            (2, 3): 2,
+            (2, 4): 0,
+            (2, 5): 5,
+            (2, 6): 6,
+            (2, 7): 7,
+            (2, 8): 7,
+            (3, 3): 3,
+            (3, 4): 4,
+            (3, 5): 5,
+            (3, 6): 6,
+            (3, 7): 7,
+            (3, 8): 8,
+            (4, 4): 4,
+            (4, 5): 0,
+            (4, 6): 0,
+            (4, 7): 0,
+            (4, 8): 0,
+            (5, 5): 5,
+            (5, 6): 5,
+            (5, 7): 5,
+            (5, 8): 5,
+            (6, 6): 6,
+            (6, 7): 5,
+            (6, 8): 6,
+            (7, 7): 7,
+            (7, 8): 7,
+            (8, 8): 8,
+        }
+        cls.gold.update(((0, n), 0) for n in cls.DG)
+
+    def assert_lca_dicts_same(self, d1, d2, G=None):
+        """Checks if d1 and d2 contain the same pairs and
+        have a node at the same distance from root for each.
+        If G is None use self.DG."""
+        if G is None:
+            G = self.DG
+            root_distance = self.root_distance
+        else:
+            roots = [n for n, deg in G.in_degree if deg == 0]
+            assert len(roots) == 1
+            root_distance = nx.shortest_path_length(G, source=roots[0])
+
+        for a, b in ((min(pair), max(pair)) for pair in chain(d1, d2)):
+            assert (
+                root_distance[get_pair(d1, a, b)] == root_distance[get_pair(d2, a, b)]
+            )
+
+    def test_all_pairs_lca_gold_example(self):
+        self.assert_lca_dicts_same(dict(all_pairs_lca(self.DG)), self.gold)
+
+    def test_all_pairs_lca_all_pairs_given(self):
+        all_pairs = list(product(self.DG.nodes(), self.DG.nodes()))
+        ans = all_pairs_lca(self.DG, pairs=all_pairs)
+        self.assert_lca_dicts_same(dict(ans), self.gold)
+
+    def test_all_pairs_lca_generator(self):
+        all_pairs = product(self.DG.nodes(), self.DG.nodes())
+        ans = all_pairs_lca(self.DG, pairs=all_pairs)
+        self.assert_lca_dicts_same(dict(ans), self.gold)
+
+    def test_all_pairs_lca_input_graph_with_two_roots(self):
+        G = self.DG.copy()
+        G.add_edge(9, 10)
+        G.add_edge(9, 4)
+        gold = self.gold.copy()
+        gold[9, 9] = 9
+        gold[9, 10] = 9
+        gold[9, 4] = 9
+        gold[9, 3] = 9
+        gold[10, 4] = 9
+        gold[10, 3] = 9
+        gold[10, 10] = 10
+
+        testing = dict(all_pairs_lca(G))
+
+        G.add_edge(-1, 9)
+        G.add_edge(-1, 0)
+        self.assert_lca_dicts_same(testing, gold, G)
+
+    def test_all_pairs_lca_nonexisting_pairs_exception(self):
+        pytest.raises(nx.NodeNotFound, all_pairs_lca, self.DG, [(-1, -1)])
+
+    def test_all_pairs_lca_pairs_without_lca(self):
+        G = self.DG.copy()
+        G.add_node(-1)
+        gen = all_pairs_lca(G, [(-1, -1), (-1, 0)])
+        assert dict(gen) == {(-1, -1): -1}
+
+    def test_all_pairs_lca_null_graph(self):
+        pytest.raises(nx.NetworkXPointlessConcept, all_pairs_lca, nx.DiGraph())
+
+    def test_all_pairs_lca_non_dags(self):
+        pytest.raises(nx.NetworkXError, all_pairs_lca, nx.DiGraph([(3, 4), (4, 3)]))
+
+    def test_all_pairs_lca_nonempty_graph_without_lca(self):
+        G = nx.DiGraph()
+        G.add_node(3)
+        ans = list(all_pairs_lca(G))
+        assert ans == [((3, 3), 3)]
+
+    def test_all_pairs_lca_bug_gh4942(self):
+        G = nx.DiGraph([(0, 2), (1, 2), (2, 3)])
+        ans = list(all_pairs_lca(G))
+        assert len(ans) == 9
+
+    def test_all_pairs_lca_default_kwarg(self):
+        G = nx.DiGraph([(0, 1), (2, 1)])
+        sentinel = object()
+        assert nx.lowest_common_ancestor(G, 0, 2, default=sentinel) is sentinel
+
+    def test_all_pairs_lca_identity(self):
+        G = nx.DiGraph()
+        G.add_node(3)
+        assert nx.lowest_common_ancestor(G, 3, 3) == 3
+
+    def test_all_pairs_lca_issue_4574(self):
+        G = nx.DiGraph()
+        G.add_nodes_from(range(17))
+        G.add_edges_from(
+            [
+                (2, 0),
+                (1, 2),
+                (3, 2),
+                (5, 2),
+                (8, 2),
+                (11, 2),
+                (4, 5),
+                (6, 5),
+                (7, 8),
+                (10, 8),
+                (13, 11),
+                (14, 11),
+                (15, 11),
+                (9, 10),
+                (12, 13),
+                (16, 15),
+            ]
+        )
+
+        assert nx.lowest_common_ancestor(G, 7, 9) == None
+
+    def test_all_pairs_lca_one_pair_gh4942(self):
+        G = nx.DiGraph()
+        # Note: order edge addition is critical to the test
+        G.add_edge(0, 1)
+        G.add_edge(2, 0)
+        G.add_edge(2, 3)
+        G.add_edge(4, 0)
+        G.add_edge(5, 2)
+
+        assert nx.lowest_common_ancestor(G, 1, 3) == 2
+
+
+class TestMultiDiGraph_DAGLCA(TestDAGLCA):
+    @classmethod
+    def setup_class(cls):
+        cls.DG = nx.MultiDiGraph()
+        nx.add_path(cls.DG, (0, 1, 2, 3))
+        # add multiedges
+        nx.add_path(cls.DG, (0, 1, 2, 3))
+        nx.add_path(cls.DG, (0, 4, 3))
+        nx.add_path(cls.DG, (0, 5, 6, 8, 3))
+        nx.add_path(cls.DG, (5, 7, 8))
+        cls.DG.add_edge(6, 2)
+        cls.DG.add_edge(7, 2)
+
+        cls.root_distance = nx.shortest_path_length(cls.DG, source=0)
+
+        cls.gold = {
+            (1, 1): 1,
+            (1, 2): 1,
+            (1, 3): 1,
+            (1, 4): 0,
+            (1, 5): 0,
+            (1, 6): 0,
+            (1, 7): 0,
+            (1, 8): 0,
+            (2, 2): 2,
+            (2, 3): 2,
+            (2, 4): 0,
+            (2, 5): 5,
+            (2, 6): 6,
+            (2, 7): 7,
+            (2, 8): 7,
+            (3, 3): 3,
+            (3, 4): 4,
+            (3, 5): 5,
+            (3, 6): 6,
+            (3, 7): 7,
+            (3, 8): 8,
+            (4, 4): 4,
+            (4, 5): 0,
+            (4, 6): 0,
+            (4, 7): 0,
+            (4, 8): 0,
+            (5, 5): 5,
+            (5, 6): 5,
+            (5, 7): 5,
+            (5, 8): 5,
+            (6, 6): 6,
+            (6, 7): 5,
+            (6, 8): 6,
+            (7, 7): 7,
+            (7, 8): 7,
+            (8, 8): 8,
+        }
+        cls.gold.update(((0, n), 0) for n in cls.DG)
+
+
+def test_all_pairs_lca_self_ancestors():
+    """Self-ancestors should always be the node itself, i.e. lca of (0, 0) is 0.
+    See gh-4458."""
+    # DAG for test - note order of node/edge addition is relevant
+    G = nx.DiGraph()
+    G.add_nodes_from(range(5))
+    G.add_edges_from([(1, 0), (2, 0), (3, 2), (4, 1), (4, 3)])
+
+    ap_lca = nx.all_pairs_lowest_common_ancestor
+    assert all(u == v == a for (u, v), a in ap_lca(G) if u == v)
+    MG = nx.MultiDiGraph(G)
+    assert all(u == v == a for (u, v), a in ap_lca(MG) if u == v)
+    MG.add_edges_from([(1, 0), (2, 0)])
+    assert all(u == v == a for (u, v), a in ap_lca(MG) if u == v)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_matching.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_matching.py
new file mode 100644
index 00000000..37853e38
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_matching.py
@@ -0,0 +1,605 @@
+import math
+from itertools import permutations
+
+from pytest import raises
+
+import networkx as nx
+from networkx.algorithms.matching import matching_dict_to_set
+from networkx.utils import edges_equal
+
+
+class TestMaxWeightMatching:
+    """Unit tests for the
+    :func:`~networkx.algorithms.matching.max_weight_matching` function.
+
+    """
+
+    def test_trivial1(self):
+        """Empty graph"""
+        G = nx.Graph()
+        assert nx.max_weight_matching(G) == set()
+        assert nx.min_weight_matching(G) == set()
+
+    def test_selfloop(self):
+        G = nx.Graph()
+        G.add_edge(0, 0, weight=100)
+        assert nx.max_weight_matching(G) == set()
+        assert nx.min_weight_matching(G) == set()
+
+    def test_single_edge(self):
+        G = nx.Graph()
+        G.add_edge(0, 1)
+        assert edges_equal(
+            nx.max_weight_matching(G), matching_dict_to_set({0: 1, 1: 0})
+        )
+        assert edges_equal(
+            nx.min_weight_matching(G), matching_dict_to_set({0: 1, 1: 0})
+        )
+
+    def test_two_path(self):
+        G = nx.Graph()
+        G.add_edge("one", "two", weight=10)
+        G.add_edge("two", "three", weight=11)
+        assert edges_equal(
+            nx.max_weight_matching(G),
+            matching_dict_to_set({"three": "two", "two": "three"}),
+        )
+        assert edges_equal(
+            nx.min_weight_matching(G),
+            matching_dict_to_set({"one": "two", "two": "one"}),
+        )
+
+    def test_path(self):
+        G = nx.Graph()
+        G.add_edge(1, 2, weight=5)
+        G.add_edge(2, 3, weight=11)
+        G.add_edge(3, 4, weight=5)
+        assert edges_equal(
+            nx.max_weight_matching(G), matching_dict_to_set({2: 3, 3: 2})
+        )
+        assert edges_equal(
+            nx.max_weight_matching(G, 1), matching_dict_to_set({1: 2, 2: 1, 3: 4, 4: 3})
+        )
+        assert edges_equal(
+            nx.min_weight_matching(G), matching_dict_to_set({1: 2, 3: 4})
+        )
+        assert edges_equal(
+            nx.min_weight_matching(G, 1), matching_dict_to_set({1: 2, 3: 4})
+        )
+
+    def test_square(self):
+        G = nx.Graph()
+        G.add_edge(1, 4, weight=2)
+        G.add_edge(2, 3, weight=2)
+        G.add_edge(1, 2, weight=1)
+        G.add_edge(3, 4, weight=4)
+        assert edges_equal(
+            nx.max_weight_matching(G), matching_dict_to_set({1: 2, 3: 4})
+        )
+        assert edges_equal(
+            nx.min_weight_matching(G), matching_dict_to_set({1: 4, 2: 3})
+        )
+
+    def test_edge_attribute_name(self):
+        G = nx.Graph()
+        G.add_edge("one", "two", weight=10, abcd=11)
+        G.add_edge("two", "three", weight=11, abcd=10)
+        assert edges_equal(
+            nx.max_weight_matching(G, weight="abcd"),
+            matching_dict_to_set({"one": "two", "two": "one"}),
+        )
+        assert edges_equal(
+            nx.min_weight_matching(G, weight="abcd"),
+            matching_dict_to_set({"three": "two"}),
+        )
+
+    def test_floating_point_weights(self):
+        G = nx.Graph()
+        G.add_edge(1, 2, weight=math.pi)
+        G.add_edge(2, 3, weight=math.exp(1))
+        G.add_edge(1, 3, weight=3.0)
+        G.add_edge(1, 4, weight=math.sqrt(2.0))
+        assert edges_equal(
+            nx.max_weight_matching(G), matching_dict_to_set({1: 4, 2: 3, 3: 2, 4: 1})
+        )
+        assert edges_equal(
+            nx.min_weight_matching(G), matching_dict_to_set({1: 4, 2: 3, 3: 2, 4: 1})
+        )
+
+    def test_negative_weights(self):
+        G = nx.Graph()
+        G.add_edge(1, 2, weight=2)
+        G.add_edge(1, 3, weight=-2)
+        G.add_edge(2, 3, weight=1)
+        G.add_edge(2, 4, weight=-1)
+        G.add_edge(3, 4, weight=-6)
+        assert edges_equal(
+            nx.max_weight_matching(G), matching_dict_to_set({1: 2, 2: 1})
+        )
+        assert edges_equal(
+            nx.max_weight_matching(G, maxcardinality=True),
+            matching_dict_to_set({1: 3, 2: 4, 3: 1, 4: 2}),
+        )
+        assert edges_equal(
+            nx.min_weight_matching(G), matching_dict_to_set({1: 2, 3: 4})
+        )
+
+    def test_s_blossom(self):
+        """Create S-blossom and use it for augmentation:"""
+        G = nx.Graph()
+        G.add_weighted_edges_from([(1, 2, 8), (1, 3, 9), (2, 3, 10), (3, 4, 7)])
+        answer = matching_dict_to_set({1: 2, 2: 1, 3: 4, 4: 3})
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+        G.add_weighted_edges_from([(1, 6, 5), (4, 5, 6)])
+        answer = matching_dict_to_set({1: 6, 2: 3, 3: 2, 4: 5, 5: 4, 6: 1})
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_s_t_blossom(self):
+        """Create S-blossom, relabel as T-blossom, use for augmentation:"""
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [(1, 2, 9), (1, 3, 8), (2, 3, 10), (1, 4, 5), (4, 5, 4), (1, 6, 3)]
+        )
+        answer = matching_dict_to_set({1: 6, 2: 3, 3: 2, 4: 5, 5: 4, 6: 1})
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+        G.add_edge(4, 5, weight=3)
+        G.add_edge(1, 6, weight=4)
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+        G.remove_edge(1, 6)
+        G.add_edge(3, 6, weight=4)
+        answer = matching_dict_to_set({1: 2, 2: 1, 3: 6, 4: 5, 5: 4, 6: 3})
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_nested_s_blossom(self):
+        """Create nested S-blossom, use for augmentation:"""
+
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 9),
+                (1, 3, 9),
+                (2, 3, 10),
+                (2, 4, 8),
+                (3, 5, 8),
+                (4, 5, 10),
+                (5, 6, 6),
+            ]
+        )
+        dict_format = {1: 3, 2: 4, 3: 1, 4: 2, 5: 6, 6: 5}
+        expected = {frozenset(e) for e in matching_dict_to_set(dict_format)}
+        answer = {frozenset(e) for e in nx.max_weight_matching(G)}
+        assert answer == expected
+        answer = {frozenset(e) for e in nx.min_weight_matching(G)}
+        assert answer == expected
+
+    def test_nested_s_blossom_relabel(self):
+        """Create S-blossom, relabel as S, include in nested S-blossom:"""
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 10),
+                (1, 7, 10),
+                (2, 3, 12),
+                (3, 4, 20),
+                (3, 5, 20),
+                (4, 5, 25),
+                (5, 6, 10),
+                (6, 7, 10),
+                (7, 8, 8),
+            ]
+        )
+        answer = matching_dict_to_set({1: 2, 2: 1, 3: 4, 4: 3, 5: 6, 6: 5, 7: 8, 8: 7})
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_nested_s_blossom_expand(self):
+        """Create nested S-blossom, augment, expand recursively:"""
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 8),
+                (1, 3, 8),
+                (2, 3, 10),
+                (2, 4, 12),
+                (3, 5, 12),
+                (4, 5, 14),
+                (4, 6, 12),
+                (5, 7, 12),
+                (6, 7, 14),
+                (7, 8, 12),
+            ]
+        )
+        answer = matching_dict_to_set({1: 2, 2: 1, 3: 5, 4: 6, 5: 3, 6: 4, 7: 8, 8: 7})
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_s_blossom_relabel_expand(self):
+        """Create S-blossom, relabel as T, expand:"""
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 23),
+                (1, 5, 22),
+                (1, 6, 15),
+                (2, 3, 25),
+                (3, 4, 22),
+                (4, 5, 25),
+                (4, 8, 14),
+                (5, 7, 13),
+            ]
+        )
+        answer = matching_dict_to_set({1: 6, 2: 3, 3: 2, 4: 8, 5: 7, 6: 1, 7: 5, 8: 4})
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_nested_s_blossom_relabel_expand(self):
+        """Create nested S-blossom, relabel as T, expand:"""
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 19),
+                (1, 3, 20),
+                (1, 8, 8),
+                (2, 3, 25),
+                (2, 4, 18),
+                (3, 5, 18),
+                (4, 5, 13),
+                (4, 7, 7),
+                (5, 6, 7),
+            ]
+        )
+        answer = matching_dict_to_set({1: 8, 2: 3, 3: 2, 4: 7, 5: 6, 6: 5, 7: 4, 8: 1})
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_nasty_blossom1(self):
+        """Create blossom, relabel as T in more than one way, expand,
+        augment:
+        """
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 45),
+                (1, 5, 45),
+                (2, 3, 50),
+                (3, 4, 45),
+                (4, 5, 50),
+                (1, 6, 30),
+                (3, 9, 35),
+                (4, 8, 35),
+                (5, 7, 26),
+                (9, 10, 5),
+            ]
+        )
+        ansdict = {1: 6, 2: 3, 3: 2, 4: 8, 5: 7, 6: 1, 7: 5, 8: 4, 9: 10, 10: 9}
+        answer = matching_dict_to_set(ansdict)
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_nasty_blossom2(self):
+        """Again but slightly different:"""
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 45),
+                (1, 5, 45),
+                (2, 3, 50),
+                (3, 4, 45),
+                (4, 5, 50),
+                (1, 6, 30),
+                (3, 9, 35),
+                (4, 8, 26),
+                (5, 7, 40),
+                (9, 10, 5),
+            ]
+        )
+        ans = {1: 6, 2: 3, 3: 2, 4: 8, 5: 7, 6: 1, 7: 5, 8: 4, 9: 10, 10: 9}
+        answer = matching_dict_to_set(ans)
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_nasty_blossom_least_slack(self):
+        """Create blossom, relabel as T, expand such that a new
+        least-slack S-to-free dge is produced, augment:
+        """
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 45),
+                (1, 5, 45),
+                (2, 3, 50),
+                (3, 4, 45),
+                (4, 5, 50),
+                (1, 6, 30),
+                (3, 9, 35),
+                (4, 8, 28),
+                (5, 7, 26),
+                (9, 10, 5),
+            ]
+        )
+        ans = {1: 6, 2: 3, 3: 2, 4: 8, 5: 7, 6: 1, 7: 5, 8: 4, 9: 10, 10: 9}
+        answer = matching_dict_to_set(ans)
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_nasty_blossom_augmenting(self):
+        """Create nested blossom, relabel as T in more than one way"""
+        # expand outer blossom such that inner blossom ends up on an
+        # augmenting path:
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 45),
+                (1, 7, 45),
+                (2, 3, 50),
+                (3, 4, 45),
+                (4, 5, 95),
+                (4, 6, 94),
+                (5, 6, 94),
+                (6, 7, 50),
+                (1, 8, 30),
+                (3, 11, 35),
+                (5, 9, 36),
+                (7, 10, 26),
+                (11, 12, 5),
+            ]
+        )
+        ans = {
+            1: 8,
+            2: 3,
+            3: 2,
+            4: 6,
+            5: 9,
+            6: 4,
+            7: 10,
+            8: 1,
+            9: 5,
+            10: 7,
+            11: 12,
+            12: 11,
+        }
+        answer = matching_dict_to_set(ans)
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_nasty_blossom_expand_recursively(self):
+        """Create nested S-blossom, relabel as S, expand recursively:"""
+        G = nx.Graph()
+        G.add_weighted_edges_from(
+            [
+                (1, 2, 40),
+                (1, 3, 40),
+                (2, 3, 60),
+                (2, 4, 55),
+                (3, 5, 55),
+                (4, 5, 50),
+                (1, 8, 15),
+                (5, 7, 30),
+                (7, 6, 10),
+                (8, 10, 10),
+                (4, 9, 30),
+            ]
+        )
+        ans = {1: 2, 2: 1, 3: 5, 4: 9, 5: 3, 6: 7, 7: 6, 8: 10, 9: 4, 10: 8}
+        answer = matching_dict_to_set(ans)
+        assert edges_equal(nx.max_weight_matching(G), answer)
+        assert edges_equal(nx.min_weight_matching(G), answer)
+
+    def test_wrong_graph_type(self):
+        error = nx.NetworkXNotImplemented
+        raises(error, nx.max_weight_matching, nx.MultiGraph())
+        raises(error, nx.max_weight_matching, nx.MultiDiGraph())
+        raises(error, nx.max_weight_matching, nx.DiGraph())
+        raises(error, nx.min_weight_matching, nx.DiGraph())
+
+
+class TestIsMatching:
+    """Unit tests for the
+    :func:`~networkx.algorithms.matching.is_matching` function.
+
+    """
+
+    def test_dict(self):
+        G = nx.path_graph(4)
+        assert nx.is_matching(G, {0: 1, 1: 0, 2: 3, 3: 2})
+
+    def test_empty_matching(self):
+        G = nx.path_graph(4)
+        assert nx.is_matching(G, set())
+
+    def test_single_edge(self):
+        G = nx.path_graph(4)
+        assert nx.is_matching(G, {(1, 2)})
+
+    def test_edge_order(self):
+        G = nx.path_graph(4)
+        assert nx.is_matching(G, {(0, 1), (2, 3)})
+        assert nx.is_matching(G, {(1, 0), (2, 3)})
+        assert nx.is_matching(G, {(0, 1), (3, 2)})
+        assert nx.is_matching(G, {(1, 0), (3, 2)})
+
+    def test_valid_matching(self):
+        G = nx.path_graph(4)
+        assert nx.is_matching(G, {(0, 1), (2, 3)})
+
+    def test_invalid_input(self):
+        error = nx.NetworkXError
+        G = nx.path_graph(4)
+        # edge to node not in G
+        raises(error, nx.is_matching, G, {(0, 5), (2, 3)})
+        # edge not a 2-tuple
+        raises(error, nx.is_matching, G, {(0, 1, 2), (2, 3)})
+        raises(error, nx.is_matching, G, {(0,), (2, 3)})
+
+    def test_selfloops(self):
+        error = nx.NetworkXError
+        G = nx.path_graph(4)
+        # selfloop for node not in G
+        raises(error, nx.is_matching, G, {(5, 5), (2, 3)})
+        # selfloop edge not in G
+        assert not nx.is_matching(G, {(0, 0), (1, 2), (2, 3)})
+        # selfloop edge in G
+        G.add_edge(0, 0)
+        assert not nx.is_matching(G, {(0, 0), (1, 2)})
+
+    def test_invalid_matching(self):
+        G = nx.path_graph(4)
+        assert not nx.is_matching(G, {(0, 1), (1, 2), (2, 3)})
+
+    def test_invalid_edge(self):
+        G = nx.path_graph(4)
+        assert not nx.is_matching(G, {(0, 3), (1, 2)})
+        raises(nx.NetworkXError, nx.is_matching, G, {(0, 55)})
+
+        G = nx.DiGraph(G.edges)
+        assert nx.is_matching(G, {(0, 1)})
+        assert not nx.is_matching(G, {(1, 0)})
+
+
+class TestIsMaximalMatching:
+    """Unit tests for the
+    :func:`~networkx.algorithms.matching.is_maximal_matching` function.
+
+    """
+
+    def test_dict(self):
+        G = nx.path_graph(4)
+        assert nx.is_maximal_matching(G, {0: 1, 1: 0, 2: 3, 3: 2})
+
+    def test_invalid_input(self):
+        error = nx.NetworkXError
+        G = nx.path_graph(4)
+        # edge to node not in G
+        raises(error, nx.is_maximal_matching, G, {(0, 5)})
+        raises(error, nx.is_maximal_matching, G, {(5, 0)})
+        # edge not a 2-tuple
+        raises(error, nx.is_maximal_matching, G, {(0, 1, 2), (2, 3)})
+        raises(error, nx.is_maximal_matching, G, {(0,), (2, 3)})
+
+    def test_valid(self):
+        G = nx.path_graph(4)
+        assert nx.is_maximal_matching(G, {(0, 1), (2, 3)})
+
+    def test_not_matching(self):
+        G = nx.path_graph(4)
+        assert not nx.is_maximal_matching(G, {(0, 1), (1, 2), (2, 3)})
+        assert not nx.is_maximal_matching(G, {(0, 3)})
+        G.add_edge(0, 0)
+        assert not nx.is_maximal_matching(G, {(0, 0)})
+
+    def test_not_maximal(self):
+        G = nx.path_graph(4)
+        assert not nx.is_maximal_matching(G, {(0, 1)})
+
+
+class TestIsPerfectMatching:
+    """Unit tests for the
+    :func:`~networkx.algorithms.matching.is_perfect_matching` function.
+
+    """
+
+    def test_dict(self):
+        G = nx.path_graph(4)
+        assert nx.is_perfect_matching(G, {0: 1, 1: 0, 2: 3, 3: 2})
+
+    def test_valid(self):
+        G = nx.path_graph(4)
+        assert nx.is_perfect_matching(G, {(0, 1), (2, 3)})
+
+    def test_valid_not_path(self):
+        G = nx.cycle_graph(4)
+        G.add_edge(0, 4)
+        G.add_edge(1, 4)
+        G.add_edge(5, 2)
+
+        assert nx.is_perfect_matching(G, {(1, 4), (0, 3), (5, 2)})
+
+    def test_invalid_input(self):
+        error = nx.NetworkXError
+        G = nx.path_graph(4)
+        # edge to node not in G
+        raises(error, nx.is_perfect_matching, G, {(0, 5)})
+        raises(error, nx.is_perfect_matching, G, {(5, 0)})
+        # edge not a 2-tuple
+        raises(error, nx.is_perfect_matching, G, {(0, 1, 2), (2, 3)})
+        raises(error, nx.is_perfect_matching, G, {(0,), (2, 3)})
+
+    def test_selfloops(self):
+        error = nx.NetworkXError
+        G = nx.path_graph(4)
+        # selfloop for node not in G
+        raises(error, nx.is_perfect_matching, G, {(5, 5), (2, 3)})
+        # selfloop edge not in G
+        assert not nx.is_perfect_matching(G, {(0, 0), (1, 2), (2, 3)})
+        # selfloop edge in G
+        G.add_edge(0, 0)
+        assert not nx.is_perfect_matching(G, {(0, 0), (1, 2)})
+
+    def test_not_matching(self):
+        G = nx.path_graph(4)
+        assert not nx.is_perfect_matching(G, {(0, 3)})
+        assert not nx.is_perfect_matching(G, {(0, 1), (1, 2), (2, 3)})
+
+    def test_maximal_but_not_perfect(self):
+        G = nx.cycle_graph(4)
+        G.add_edge(0, 4)
+        G.add_edge(1, 4)
+
+        assert not nx.is_perfect_matching(G, {(1, 4), (0, 3)})
+
+
+class TestMaximalMatching:
+    """Unit tests for the
+    :func:`~networkx.algorithms.matching.maximal_matching`.
+
+    """
+
+    def test_valid_matching(self):
+        edges = [(1, 2), (1, 5), (2, 3), (2, 5), (3, 4), (3, 6), (5, 6)]
+        G = nx.Graph(edges)
+        matching = nx.maximal_matching(G)
+        assert nx.is_maximal_matching(G, matching)
+
+    def test_single_edge_matching(self):
+        # In the star graph, any maximal matching has just one edge.
+        G = nx.star_graph(5)
+        matching = nx.maximal_matching(G)
+        assert 1 == len(matching)
+        assert nx.is_maximal_matching(G, matching)
+
+    def test_self_loops(self):
+        # Create the path graph with two self-loops.
+        G = nx.path_graph(3)
+        G.add_edges_from([(0, 0), (1, 1)])
+        matching = nx.maximal_matching(G)
+        assert len(matching) == 1
+        # The matching should never include self-loops.
+        assert not any(u == v for u, v in matching)
+        assert nx.is_maximal_matching(G, matching)
+
+    def test_ordering(self):
+        """Tests that a maximal matching is computed correctly
+        regardless of the order in which nodes are added to the graph.
+
+        """
+        for nodes in permutations(range(3)):
+            G = nx.Graph()
+            G.add_nodes_from(nodes)
+            G.add_edges_from([(0, 1), (0, 2)])
+            matching = nx.maximal_matching(G)
+            assert len(matching) == 1
+            assert nx.is_maximal_matching(G, matching)
+
+    def test_wrong_graph_type(self):
+        error = nx.NetworkXNotImplemented
+        raises(error, nx.maximal_matching, nx.MultiGraph())
+        raises(error, nx.maximal_matching, nx.MultiDiGraph())
+        raises(error, nx.maximal_matching, nx.DiGraph())
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_max_weight_clique.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_max_weight_clique.py
new file mode 100644
index 00000000..6cd8584e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_max_weight_clique.py
@@ -0,0 +1,179 @@
+"""Maximum weight clique test suite."""
+
+import pytest
+
+import networkx as nx
+
+
+class TestMaximumWeightClique:
+    def test_basic_cases(self):
+        def check_basic_case(graph_func, expected_weight, weight_accessor):
+            graph = graph_func()
+            clique, weight = nx.algorithms.max_weight_clique(graph, weight_accessor)
+            assert verify_clique(
+                graph, clique, weight, expected_weight, weight_accessor
+            )
+
+        for graph_func, (expected_weight, expected_size) in TEST_CASES.items():
+            check_basic_case(graph_func, expected_weight, "weight")
+            check_basic_case(graph_func, expected_size, None)
+
+    def test_key_error(self):
+        graph = two_node_graph()
+        with pytest.raises(KeyError):
+            nx.algorithms.max_weight_clique(graph, "nonexistent-key")
+
+    def test_error_on_non_integer_weight(self):
+        graph = two_node_graph()
+        graph.nodes[2]["weight"] = 1.5
+        with pytest.raises(ValueError):
+            nx.algorithms.max_weight_clique(graph)
+
+    def test_unaffected_by_self_loops(self):
+        graph = two_node_graph()
+        graph.add_edge(1, 1)
+        graph.add_edge(2, 2)
+        clique, weight = nx.algorithms.max_weight_clique(graph, "weight")
+        assert verify_clique(graph, clique, weight, 30, "weight")
+        graph = three_node_independent_set()
+        graph.add_edge(1, 1)
+        clique, weight = nx.algorithms.max_weight_clique(graph, "weight")
+        assert verify_clique(graph, clique, weight, 20, "weight")
+
+    def test_30_node_prob(self):
+        G = nx.Graph()
+        G.add_nodes_from(range(1, 31))
+        for i in range(1, 31):
+            G.nodes[i]["weight"] = i + 1
+        # fmt: off
+        G.add_edges_from(
+            [
+                (1, 12), (1, 13), (1, 15), (1, 16), (1, 18), (1, 19), (1, 20),
+                (1, 23), (1, 26), (1, 28), (1, 29), (1, 30), (2, 3), (2, 4),
+                (2, 5), (2, 8), (2, 9), (2, 10), (2, 14), (2, 17), (2, 18),
+                (2, 21), (2, 22), (2, 23), (2, 27), (3, 9), (3, 15), (3, 21),
+                (3, 22), (3, 23), (3, 24), (3, 27), (3, 28), (3, 29), (4, 5),
+                (4, 6), (4, 8), (4, 21), (4, 22), (4, 23), (4, 26), (4, 28),
+                (4, 30), (5, 6), (5, 8), (5, 9), (5, 13), (5, 14), (5, 15),
+                (5, 16), (5, 20), (5, 21), (5, 22), (5, 25), (5, 28), (5, 29),
+                (6, 7), (6, 8), (6, 13), (6, 17), (6, 18), (6, 19), (6, 24),
+                (6, 26), (6, 27), (6, 28), (6, 29), (7, 12), (7, 14), (7, 15),
+                (7, 16), (7, 17), (7, 20), (7, 25), (7, 27), (7, 29), (7, 30),
+                (8, 10), (8, 15), (8, 16), (8, 18), (8, 20), (8, 22), (8, 24),
+                (8, 26), (8, 27), (8, 28), (8, 30), (9, 11), (9, 12), (9, 13),
+                (9, 14), (9, 15), (9, 16), (9, 19), (9, 20), (9, 21), (9, 24),
+                (9, 30), (10, 12), (10, 15), (10, 18), (10, 19), (10, 20),
+                (10, 22), (10, 23), (10, 24), (10, 26), (10, 27), (10, 29),
+                (10, 30), (11, 13), (11, 15), (11, 16), (11, 17), (11, 18),
+                (11, 19), (11, 20), (11, 22), (11, 29), (11, 30), (12, 14),
+                (12, 17), (12, 18), (12, 19), (12, 20), (12, 21), (12, 23),
+                (12, 25), (12, 26), (12, 30), (13, 20), (13, 22), (13, 23),
+                (13, 24), (13, 30), (14, 16), (14, 20), (14, 21), (14, 22),
+                (14, 23), (14, 25), (14, 26), (14, 27), (14, 29), (14, 30),
+                (15, 17), (15, 18), (15, 20), (15, 21), (15, 26), (15, 27),
+                (15, 28), (16, 17), (16, 18), (16, 19), (16, 20), (16, 21),
+                (16, 29), (16, 30), (17, 18), (17, 21), (17, 22), (17, 25),
+                (17, 27), (17, 28), (17, 30), (18, 19), (18, 20), (18, 21),
+                (18, 22), (18, 23), (18, 24), (19, 20), (19, 22), (19, 23),
+                (19, 24), (19, 25), (19, 27), (19, 30), (20, 21), (20, 23),
+                (20, 24), (20, 26), (20, 28), (20, 29), (21, 23), (21, 26),
+                (21, 27), (21, 29), (22, 24), (22, 25), (22, 26), (22, 29),
+                (23, 25), (23, 30), (24, 25), (24, 26), (25, 27), (25, 29),
+                (26, 27), (26, 28), (26, 30), (28, 29), (29, 30),
+            ]
+        )
+        # fmt: on
+        clique, weight = nx.algorithms.max_weight_clique(G)
+        assert verify_clique(G, clique, weight, 111, "weight")
+
+
+#  ############################  Utility functions ############################
+def verify_clique(
+    graph, clique, reported_clique_weight, expected_clique_weight, weight_accessor
+):
+    for node1 in clique:
+        for node2 in clique:
+            if node1 == node2:
+                continue
+            if not graph.has_edge(node1, node2):
+                return False
+
+    if weight_accessor is None:
+        clique_weight = len(clique)
+    else:
+        clique_weight = sum(graph.nodes[v]["weight"] for v in clique)
+
+    if clique_weight != expected_clique_weight:
+        return False
+    if clique_weight != reported_clique_weight:
+        return False
+
+    return True
+
+
+#  ############################  Graph Generation ############################
+
+
+def empty_graph():
+    return nx.Graph()
+
+
+def one_node_graph():
+    graph = nx.Graph()
+    graph.add_nodes_from([1])
+    graph.nodes[1]["weight"] = 10
+    return graph
+
+
+def two_node_graph():
+    graph = nx.Graph()
+    graph.add_nodes_from([1, 2])
+    graph.add_edges_from([(1, 2)])
+    graph.nodes[1]["weight"] = 10
+    graph.nodes[2]["weight"] = 20
+    return graph
+
+
+def three_node_clique():
+    graph = nx.Graph()
+    graph.add_nodes_from([1, 2, 3])
+    graph.add_edges_from([(1, 2), (1, 3), (2, 3)])
+    graph.nodes[1]["weight"] = 10
+    graph.nodes[2]["weight"] = 20
+    graph.nodes[3]["weight"] = 5
+    return graph
+
+
+def three_node_independent_set():
+    graph = nx.Graph()
+    graph.add_nodes_from([1, 2, 3])
+    graph.nodes[1]["weight"] = 10
+    graph.nodes[2]["weight"] = 20
+    graph.nodes[3]["weight"] = 5
+    return graph
+
+
+def disconnected():
+    graph = nx.Graph()
+    graph.add_edges_from([(1, 2), (2, 3), (4, 5), (5, 6)])
+    graph.nodes[1]["weight"] = 10
+    graph.nodes[2]["weight"] = 20
+    graph.nodes[3]["weight"] = 5
+    graph.nodes[4]["weight"] = 100
+    graph.nodes[5]["weight"] = 200
+    graph.nodes[6]["weight"] = 50
+    return graph
+
+
+# --------------------------------------------------------------------------
+# Basic tests for all strategies
+# For each basic graph function, specify expected weight of max weight clique
+# and expected size of maximum clique
+TEST_CASES = {
+    empty_graph: (0, 0),
+    one_node_graph: (10, 1),
+    two_node_graph: (30, 2),
+    three_node_clique: (35, 3),
+    three_node_independent_set: (20, 1),
+    disconnected: (300, 2),
+}
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_mis.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_mis.py
new file mode 100644
index 00000000..02be02d4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_mis.py
@@ -0,0 +1,62 @@
+"""
+Tests for maximal (not maximum) independent sets.
+
+"""
+
+import random
+
+import pytest
+
+import networkx as nx
+
+
+def test_random_seed():
+    G = nx.empty_graph(5)
+    assert nx.maximal_independent_set(G, seed=1) == [1, 0, 3, 2, 4]
+
+
+@pytest.mark.parametrize("graph", [nx.complete_graph(5), nx.complete_graph(55)])
+def test_K5(graph):
+    """Maximal independent set for complete graphs"""
+    assert all(nx.maximal_independent_set(graph, [n]) == [n] for n in graph)
+
+
+def test_exceptions():
+    """Bad input should raise exception."""
+    G = nx.florentine_families_graph()
+    pytest.raises(nx.NetworkXUnfeasible, nx.maximal_independent_set, G, ["Smith"])
+    pytest.raises(
+        nx.NetworkXUnfeasible, nx.maximal_independent_set, G, ["Salviati", "Pazzi"]
+    )
+    # MaximalIndependentSet is not implemented for directed graphs
+    pytest.raises(nx.NetworkXNotImplemented, nx.maximal_independent_set, nx.DiGraph(G))
+
+
+def test_florentine_family():
+    G = nx.florentine_families_graph()
+    indep = nx.maximal_independent_set(G, ["Medici", "Bischeri"])
+    assert set(indep) == {
+        "Medici",
+        "Bischeri",
+        "Castellani",
+        "Pazzi",
+        "Ginori",
+        "Lamberteschi",
+    }
+
+
+def test_bipartite():
+    G = nx.complete_bipartite_graph(12, 34)
+    indep = nx.maximal_independent_set(G, [4, 5, 9, 10])
+    assert sorted(indep) == list(range(12))
+
+
+def test_random_graphs():
+    """Generate 5 random graphs of different types and sizes and
+    make sure that all sets are independent and maximal."""
+    for i in range(0, 50, 10):
+        G = nx.erdos_renyi_graph(i * 10 + 1, random.random())
+        IS = nx.maximal_independent_set(G)
+        assert G.subgraph(IS).number_of_edges() == 0
+        nbrs_of_MIS = set.union(*(set(G.neighbors(v)) for v in IS))
+        assert all(v in nbrs_of_MIS for v in set(G.nodes()).difference(IS))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_moral.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_moral.py
new file mode 100644
index 00000000..fc98c972
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_moral.py
@@ -0,0 +1,15 @@
+import networkx as nx
+from networkx.algorithms.moral import moral_graph
+
+
+def test_get_moral_graph():
+    graph = nx.DiGraph()
+    graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7])
+    graph.add_edges_from([(1, 2), (3, 2), (4, 1), (4, 5), (6, 5), (7, 5)])
+    H = moral_graph(graph)
+    assert not H.is_directed()
+    assert H.has_edge(1, 3)
+    assert H.has_edge(4, 6)
+    assert H.has_edge(6, 7)
+    assert H.has_edge(4, 7)
+    assert not H.has_edge(1, 5)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_node_classification.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_node_classification.py
new file mode 100644
index 00000000..2e1fc79d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_node_classification.py
@@ -0,0 +1,140 @@
+import pytest
+
+pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+import networkx as nx
+from networkx.algorithms import node_classification
+
+
+class TestHarmonicFunction:
+    def test_path_graph(self):
+        G = nx.path_graph(4)
+        label_name = "label"
+        G.nodes[0][label_name] = "A"
+        G.nodes[3][label_name] = "B"
+        predicted = node_classification.harmonic_function(G, label_name=label_name)
+        assert predicted[0] == "A"
+        assert predicted[1] == "A"
+        assert predicted[2] == "B"
+        assert predicted[3] == "B"
+
+    def test_no_labels(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.path_graph(4)
+            node_classification.harmonic_function(G)
+
+    def test_no_nodes(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.Graph()
+            node_classification.harmonic_function(G)
+
+    def test_no_edges(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.Graph()
+            G.add_node(1)
+            G.add_node(2)
+            node_classification.harmonic_function(G)
+
+    def test_digraph(self):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            G = nx.DiGraph()
+            G.add_edge(0, 1)
+            G.add_edge(1, 2)
+            G.add_edge(2, 3)
+            label_name = "label"
+            G.nodes[0][label_name] = "A"
+            G.nodes[3][label_name] = "B"
+            node_classification.harmonic_function(G)
+
+    def test_one_labeled_node(self):
+        G = nx.path_graph(4)
+        label_name = "label"
+        G.nodes[0][label_name] = "A"
+        predicted = node_classification.harmonic_function(G, label_name=label_name)
+        assert predicted[0] == "A"
+        assert predicted[1] == "A"
+        assert predicted[2] == "A"
+        assert predicted[3] == "A"
+
+    def test_nodes_all_labeled(self):
+        G = nx.karate_club_graph()
+        label_name = "club"
+        predicted = node_classification.harmonic_function(G, label_name=label_name)
+        for i in range(len(G)):
+            assert predicted[i] == G.nodes[i][label_name]
+
+    def test_labeled_nodes_are_not_changed(self):
+        G = nx.karate_club_graph()
+        label_name = "club"
+        label_removed = {0, 1, 2, 3, 4, 5, 6, 7}
+        for i in label_removed:
+            del G.nodes[i][label_name]
+        predicted = node_classification.harmonic_function(G, label_name=label_name)
+        label_not_removed = set(range(len(G))) - label_removed
+        for i in label_not_removed:
+            assert predicted[i] == G.nodes[i][label_name]
+
+
+class TestLocalAndGlobalConsistency:
+    def test_path_graph(self):
+        G = nx.path_graph(4)
+        label_name = "label"
+        G.nodes[0][label_name] = "A"
+        G.nodes[3][label_name] = "B"
+        predicted = node_classification.local_and_global_consistency(
+            G, label_name=label_name
+        )
+        assert predicted[0] == "A"
+        assert predicted[1] == "A"
+        assert predicted[2] == "B"
+        assert predicted[3] == "B"
+
+    def test_no_labels(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.path_graph(4)
+            node_classification.local_and_global_consistency(G)
+
+    def test_no_nodes(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.Graph()
+            node_classification.local_and_global_consistency(G)
+
+    def test_no_edges(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.Graph()
+            G.add_node(1)
+            G.add_node(2)
+            node_classification.local_and_global_consistency(G)
+
+    def test_digraph(self):
+        with pytest.raises(nx.NetworkXNotImplemented):
+            G = nx.DiGraph()
+            G.add_edge(0, 1)
+            G.add_edge(1, 2)
+            G.add_edge(2, 3)
+            label_name = "label"
+            G.nodes[0][label_name] = "A"
+            G.nodes[3][label_name] = "B"
+            node_classification.harmonic_function(G)
+
+    def test_one_labeled_node(self):
+        G = nx.path_graph(4)
+        label_name = "label"
+        G.nodes[0][label_name] = "A"
+        predicted = node_classification.local_and_global_consistency(
+            G, label_name=label_name
+        )
+        assert predicted[0] == "A"
+        assert predicted[1] == "A"
+        assert predicted[2] == "A"
+        assert predicted[3] == "A"
+
+    def test_nodes_all_labeled(self):
+        G = nx.karate_club_graph()
+        label_name = "club"
+        predicted = node_classification.local_and_global_consistency(
+            G, alpha=0, label_name=label_name
+        )
+        for i in range(len(G)):
+            assert predicted[i] == G.nodes[i][label_name]
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_non_randomness.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_non_randomness.py
new file mode 100644
index 00000000..2f495be2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_non_randomness.py
@@ -0,0 +1,42 @@
+import pytest
+
+import networkx as nx
+
+np = pytest.importorskip("numpy")
+
+
+@pytest.mark.parametrize(
+    "k, weight, expected",
+    [
+        (None, None, 7.21),  # infers 3 communities
+        (2, None, 11.7),
+        (None, "weight", 25.45),
+        (2, "weight", 38.8),
+    ],
+)
+def test_non_randomness(k, weight, expected):
+    G = nx.karate_club_graph()
+    np.testing.assert_almost_equal(
+        nx.non_randomness(G, k, weight)[0], expected, decimal=2
+    )
+
+
+def test_non_connected():
+    G = nx.Graph([(1, 2)])
+    G.add_node(3)
+    with pytest.raises(nx.NetworkXException, match="Non connected"):
+        nx.non_randomness(G)
+
+
+def test_self_loops():
+    G = nx.Graph()
+    G.add_edge(1, 2)
+    G.add_edge(1, 1)
+    with pytest.raises(nx.NetworkXError, match="Graph must not contain self-loops"):
+        nx.non_randomness(G)
+
+
+def test_empty_graph():
+    G = nx.empty_graph(1)
+    with pytest.raises(nx.NetworkXError, match=".*not applicable to empty graphs"):
+        nx.non_randomness(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_planar_drawing.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_planar_drawing.py
new file mode 100644
index 00000000..a5de0e03
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_planar_drawing.py
@@ -0,0 +1,274 @@
+import math
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.planar_drawing import triangulate_embedding
+
+
+def test_graph1():
+    embedding_data = {0: [1, 2, 3], 1: [2, 0], 2: [3, 0, 1], 3: [2, 0]}
+    check_embedding_data(embedding_data)
+
+
+def test_graph2():
+    embedding_data = {
+        0: [8, 6],
+        1: [2, 6, 9],
+        2: [8, 1, 7, 9, 6, 4],
+        3: [9],
+        4: [2],
+        5: [6, 8],
+        6: [9, 1, 0, 5, 2],
+        7: [9, 2],
+        8: [0, 2, 5],
+        9: [1, 6, 2, 7, 3],
+    }
+    check_embedding_data(embedding_data)
+
+
+def test_circle_graph():
+    embedding_data = {
+        0: [1, 9],
+        1: [0, 2],
+        2: [1, 3],
+        3: [2, 4],
+        4: [3, 5],
+        5: [4, 6],
+        6: [5, 7],
+        7: [6, 8],
+        8: [7, 9],
+        9: [8, 0],
+    }
+    check_embedding_data(embedding_data)
+
+
+def test_grid_graph():
+    embedding_data = {
+        (0, 1): [(0, 0), (1, 1), (0, 2)],
+        (1, 2): [(1, 1), (2, 2), (0, 2)],
+        (0, 0): [(0, 1), (1, 0)],
+        (2, 1): [(2, 0), (2, 2), (1, 1)],
+        (1, 1): [(2, 1), (1, 2), (0, 1), (1, 0)],
+        (2, 0): [(1, 0), (2, 1)],
+        (2, 2): [(1, 2), (2, 1)],
+        (1, 0): [(0, 0), (2, 0), (1, 1)],
+        (0, 2): [(1, 2), (0, 1)],
+    }
+    check_embedding_data(embedding_data)
+
+
+def test_one_node_graph():
+    embedding_data = {0: []}
+    check_embedding_data(embedding_data)
+
+
+def test_two_node_graph():
+    embedding_data = {0: [1], 1: [0]}
+    check_embedding_data(embedding_data)
+
+
+def test_three_node_graph():
+    embedding_data = {0: [1, 2], 1: [0, 2], 2: [0, 1]}
+    check_embedding_data(embedding_data)
+
+
+def test_multiple_component_graph1():
+    embedding_data = {0: [], 1: []}
+    check_embedding_data(embedding_data)
+
+
+def test_multiple_component_graph2():
+    embedding_data = {0: [1, 2], 1: [0, 2], 2: [0, 1], 3: [4, 5], 4: [3, 5], 5: [3, 4]}
+    check_embedding_data(embedding_data)
+
+
+def test_invalid_half_edge():
+    with pytest.raises(nx.NetworkXException):
+        embedding_data = {1: [2, 3, 4], 2: [1, 3, 4], 3: [1, 2, 4], 4: [1, 2, 3]}
+        embedding = nx.PlanarEmbedding()
+        embedding.set_data(embedding_data)
+        nx.combinatorial_embedding_to_pos(embedding)
+
+
+def test_triangulate_embedding1():
+    embedding = nx.PlanarEmbedding()
+    embedding.add_node(1)
+    expected_embedding = {1: []}
+    check_triangulation(embedding, expected_embedding)
+
+
+def test_triangulate_embedding2():
+    embedding = nx.PlanarEmbedding()
+    embedding.connect_components(1, 2)
+    expected_embedding = {1: [2], 2: [1]}
+    check_triangulation(embedding, expected_embedding)
+
+
+def check_triangulation(embedding, expected_embedding):
+    res_embedding, _ = triangulate_embedding(embedding, True)
+    assert (
+        res_embedding.get_data() == expected_embedding
+    ), "Expected embedding incorrect"
+    res_embedding, _ = triangulate_embedding(embedding, False)
+    assert (
+        res_embedding.get_data() == expected_embedding
+    ), "Expected embedding incorrect"
+
+
+def check_embedding_data(embedding_data):
+    """Checks that the planar embedding of the input is correct"""
+    embedding = nx.PlanarEmbedding()
+    embedding.set_data(embedding_data)
+    pos_fully = nx.combinatorial_embedding_to_pos(embedding, False)
+    msg = "Planar drawing does not conform to the embedding (fully triangulation)"
+    assert planar_drawing_conforms_to_embedding(embedding, pos_fully), msg
+    check_edge_intersections(embedding, pos_fully)
+    pos_internally = nx.combinatorial_embedding_to_pos(embedding, True)
+    msg = "Planar drawing does not conform to the embedding (internal triangulation)"
+    assert planar_drawing_conforms_to_embedding(embedding, pos_internally), msg
+    check_edge_intersections(embedding, pos_internally)
+
+
+def is_close(a, b, rel_tol=1e-09, abs_tol=0.0):
+    # Check if float numbers are basically equal, for python >=3.5 there is
+    # function for that in the standard library
+    return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
+
+
+def point_in_between(a, b, p):
+    # checks if p is on the line between a and b
+    x1, y1 = a
+    x2, y2 = b
+    px, py = p
+    dist_1_2 = math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
+    dist_1_p = math.sqrt((x1 - px) ** 2 + (y1 - py) ** 2)
+    dist_2_p = math.sqrt((x2 - px) ** 2 + (y2 - py) ** 2)
+    return is_close(dist_1_p + dist_2_p, dist_1_2)
+
+
+def check_edge_intersections(G, pos):
+    """Check all edges in G for intersections.
+
+    Raises an exception if an intersection is found.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+    pos : dict
+        Maps every node to a tuple (x, y) representing its position
+
+    """
+    for a, b in G.edges():
+        for c, d in G.edges():
+            # Check if end points are different
+            if a != c and b != d and b != c and a != d:
+                x1, y1 = pos[a]
+                x2, y2 = pos[b]
+                x3, y3 = pos[c]
+                x4, y4 = pos[d]
+                determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
+                if determinant != 0:  # the lines are not parallel
+                    # calculate intersection point, see:
+                    # https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
+                    px = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (
+                        x3 * y4 - y3 * x4
+                    ) / determinant
+                    py = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (
+                        x3 * y4 - y3 * x4
+                    ) / determinant
+
+                    # Check if intersection lies between the points
+                    if point_in_between(pos[a], pos[b], (px, py)) and point_in_between(
+                        pos[c], pos[d], (px, py)
+                    ):
+                        msg = f"There is an intersection at {px},{py}"
+                        raise nx.NetworkXException(msg)
+
+                #  Check overlap
+                msg = "A node lies on a edge connecting two other nodes"
+                if (
+                    point_in_between(pos[a], pos[b], pos[c])
+                    or point_in_between(pos[a], pos[b], pos[d])
+                    or point_in_between(pos[c], pos[d], pos[a])
+                    or point_in_between(pos[c], pos[d], pos[b])
+                ):
+                    raise nx.NetworkXException(msg)
+    # No edge intersection found
+
+
+class Vector:
+    """Compare vectors by their angle without loss of precision
+
+    All vectors in direction [0, 1] are the smallest.
+    The vectors grow in clockwise direction.
+    """
+
+    __slots__ = ["x", "y", "node", "quadrant"]
+
+    def __init__(self, x, y, node):
+        self.x = x
+        self.y = y
+        self.node = node
+        if self.x >= 0 and self.y > 0:
+            self.quadrant = 1
+        elif self.x > 0 and self.y <= 0:
+            self.quadrant = 2
+        elif self.x <= 0 and self.y < 0:
+            self.quadrant = 3
+        else:
+            self.quadrant = 4
+
+    def __eq__(self, other):
+        return self.quadrant == other.quadrant and self.x * other.y == self.y * other.x
+
+    def __lt__(self, other):
+        if self.quadrant < other.quadrant:
+            return True
+        elif self.quadrant > other.quadrant:
+            return False
+        else:
+            return self.x * other.y < self.y * other.x
+
+    def __ne__(self, other):
+        return self != other
+
+    def __le__(self, other):
+        return not other < self
+
+    def __gt__(self, other):
+        return other < self
+
+    def __ge__(self, other):
+        return not self < other
+
+
+def planar_drawing_conforms_to_embedding(embedding, pos):
+    """Checks if pos conforms to the planar embedding
+
+    Returns true iff the neighbors are actually oriented in the orientation
+    specified of the embedding
+    """
+    for v in embedding:
+        nbr_vectors = []
+        v_pos = pos[v]
+        for nbr in embedding[v]:
+            new_vector = Vector(pos[nbr][0] - v_pos[0], pos[nbr][1] - v_pos[1], nbr)
+            nbr_vectors.append(new_vector)
+        # Sort neighbors according to their phi angle
+        nbr_vectors.sort()
+        for idx, nbr_vector in enumerate(nbr_vectors):
+            cw_vector = nbr_vectors[(idx + 1) % len(nbr_vectors)]
+            ccw_vector = nbr_vectors[idx - 1]
+            if (
+                embedding[v][nbr_vector.node]["cw"] != cw_vector.node
+                or embedding[v][nbr_vector.node]["ccw"] != ccw_vector.node
+            ):
+                return False
+            if cw_vector.node != nbr_vector.node and cw_vector == nbr_vector:
+                # Lines overlap
+                return False
+            if ccw_vector.node != nbr_vector.node and ccw_vector == nbr_vector:
+                # Lines overlap
+                return False
+    return True
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_planarity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_planarity.py
new file mode 100644
index 00000000..99bcff41
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_planarity.py
@@ -0,0 +1,535 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms.planarity import (
+    check_planarity_recursive,
+    get_counterexample,
+    get_counterexample_recursive,
+)
+
+
+class TestLRPlanarity:
+    """Nose Unit tests for the :mod:`networkx.algorithms.planarity` module.
+
+    Tests three things:
+    1. Check that the result is correct
+        (returns planar if and only if the graph is actually planar)
+    2. In case a counter example is returned: Check if it is correct
+    3. In case an embedding is returned: Check if its actually an embedding
+    """
+
+    @staticmethod
+    def check_graph(G, is_planar=None):
+        """Raises an exception if the lr_planarity check returns a wrong result
+
+        Parameters
+        ----------
+        G : NetworkX graph
+        is_planar : bool
+            The expected result of the planarity check.
+            If set to None only counter example or embedding are verified.
+
+        """
+
+        # obtain results of planarity check
+        is_planar_lr, result = nx.check_planarity(G, True)
+        is_planar_lr_rec, result_rec = check_planarity_recursive(G, True)
+
+        if is_planar is not None:
+            # set a message for the assert
+            if is_planar:
+                msg = "Wrong planarity check result. Should be planar."
+            else:
+                msg = "Wrong planarity check result. Should be non-planar."
+
+            # check if the result is as expected
+            assert is_planar == is_planar_lr, msg
+            assert is_planar == is_planar_lr_rec, msg
+
+        if is_planar_lr:
+            # check embedding
+            check_embedding(G, result)
+            check_embedding(G, result_rec)
+        else:
+            # check counter example
+            check_counterexample(G, result)
+            check_counterexample(G, result_rec)
+
+    def test_simple_planar_graph(self):
+        e = [
+            (1, 2),
+            (2, 3),
+            (3, 4),
+            (4, 6),
+            (6, 7),
+            (7, 1),
+            (1, 5),
+            (5, 2),
+            (2, 4),
+            (4, 5),
+            (5, 7),
+        ]
+        self.check_graph(nx.Graph(e), is_planar=True)
+
+    def test_planar_with_selfloop(self):
+        e = [
+            (1, 1),
+            (2, 2),
+            (3, 3),
+            (4, 4),
+            (5, 5),
+            (1, 2),
+            (1, 3),
+            (1, 5),
+            (2, 5),
+            (2, 4),
+            (3, 4),
+            (3, 5),
+            (4, 5),
+        ]
+        self.check_graph(nx.Graph(e), is_planar=True)
+
+    def test_k3_3(self):
+        self.check_graph(nx.complete_bipartite_graph(3, 3), is_planar=False)
+
+    def test_k5(self):
+        self.check_graph(nx.complete_graph(5), is_planar=False)
+
+    def test_multiple_components_planar(self):
+        e = [(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)]
+        self.check_graph(nx.Graph(e), is_planar=True)
+
+    def test_multiple_components_non_planar(self):
+        G = nx.complete_graph(5)
+        # add another planar component to the non planar component
+        # G stays non planar
+        G.add_edges_from([(6, 7), (7, 8), (8, 6)])
+        self.check_graph(G, is_planar=False)
+
+    def test_non_planar_with_selfloop(self):
+        G = nx.complete_graph(5)
+        # add self loops
+        for i in range(5):
+            G.add_edge(i, i)
+        self.check_graph(G, is_planar=False)
+
+    def test_non_planar1(self):
+        # tests a graph that has no subgraph directly isomorph to K5 or K3_3
+        e = [
+            (1, 5),
+            (1, 6),
+            (1, 7),
+            (2, 6),
+            (2, 3),
+            (3, 5),
+            (3, 7),
+            (4, 5),
+            (4, 6),
+            (4, 7),
+        ]
+        self.check_graph(nx.Graph(e), is_planar=False)
+
+    def test_loop(self):
+        # test a graph with a selfloop
+        e = [(1, 2), (2, 2)]
+        G = nx.Graph(e)
+        self.check_graph(G, is_planar=True)
+
+    def test_comp(self):
+        # test multiple component graph
+        e = [(1, 2), (3, 4)]
+        G = nx.Graph(e)
+        G.remove_edge(1, 2)
+        self.check_graph(G, is_planar=True)
+
+    def test_goldner_harary(self):
+        # test goldner-harary graph (a maximal planar graph)
+        e = [
+            (1, 2),
+            (1, 3),
+            (1, 4),
+            (1, 5),
+            (1, 7),
+            (1, 8),
+            (1, 10),
+            (1, 11),
+            (2, 3),
+            (2, 4),
+            (2, 6),
+            (2, 7),
+            (2, 9),
+            (2, 10),
+            (2, 11),
+            (3, 4),
+            (4, 5),
+            (4, 6),
+            (4, 7),
+            (5, 7),
+            (6, 7),
+            (7, 8),
+            (7, 9),
+            (7, 10),
+            (8, 10),
+            (9, 10),
+            (10, 11),
+        ]
+        G = nx.Graph(e)
+        self.check_graph(G, is_planar=True)
+
+    def test_planar_multigraph(self):
+        G = nx.MultiGraph([(1, 2), (1, 2), (1, 2), (1, 2), (2, 3), (3, 1)])
+        self.check_graph(G, is_planar=True)
+
+    def test_non_planar_multigraph(self):
+        G = nx.MultiGraph(nx.complete_graph(5))
+        G.add_edges_from([(1, 2)] * 5)
+        self.check_graph(G, is_planar=False)
+
+    def test_planar_digraph(self):
+        G = nx.DiGraph([(1, 2), (2, 3), (2, 4), (4, 1), (4, 2), (1, 4), (3, 2)])
+        self.check_graph(G, is_planar=True)
+
+    def test_non_planar_digraph(self):
+        G = nx.DiGraph(nx.complete_graph(5))
+        G.remove_edge(1, 2)
+        G.remove_edge(4, 1)
+        self.check_graph(G, is_planar=False)
+
+    def test_single_component(self):
+        # Test a graph with only a single node
+        G = nx.Graph()
+        G.add_node(1)
+        self.check_graph(G, is_planar=True)
+
+    def test_graph1(self):
+        G = nx.Graph(
+            [
+                (3, 10),
+                (2, 13),
+                (1, 13),
+                (7, 11),
+                (0, 8),
+                (8, 13),
+                (0, 2),
+                (0, 7),
+                (0, 10),
+                (1, 7),
+            ]
+        )
+        self.check_graph(G, is_planar=True)
+
+    def test_graph2(self):
+        G = nx.Graph(
+            [
+                (1, 2),
+                (4, 13),
+                (0, 13),
+                (4, 5),
+                (7, 10),
+                (1, 7),
+                (0, 3),
+                (2, 6),
+                (5, 6),
+                (7, 13),
+                (4, 8),
+                (0, 8),
+                (0, 9),
+                (2, 13),
+                (6, 7),
+                (3, 6),
+                (2, 8),
+            ]
+        )
+        self.check_graph(G, is_planar=False)
+
+    def test_graph3(self):
+        G = nx.Graph(
+            [
+                (0, 7),
+                (3, 11),
+                (3, 4),
+                (8, 9),
+                (4, 11),
+                (1, 7),
+                (1, 13),
+                (1, 11),
+                (3, 5),
+                (5, 7),
+                (1, 3),
+                (0, 4),
+                (5, 11),
+                (5, 13),
+            ]
+        )
+        self.check_graph(G, is_planar=False)
+
+    def test_counterexample_planar(self):
+        with pytest.raises(nx.NetworkXException):
+            # Try to get a counterexample of a planar graph
+            G = nx.Graph()
+            G.add_node(1)
+            get_counterexample(G)
+
+    def test_counterexample_planar_recursive(self):
+        with pytest.raises(nx.NetworkXException):
+            # Try to get a counterexample of a planar graph
+            G = nx.Graph()
+            G.add_node(1)
+            get_counterexample_recursive(G)
+
+    def test_edge_removal_from_planar_embedding(self):
+        # PlanarEmbedding.check_structure() must succeed after edge removal
+        edges = ((0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 2), (0, 3))
+        G = nx.Graph(edges)
+        cert, P = nx.check_planarity(G)
+        assert cert is True
+        P.remove_edge(0, 2)
+        self.check_graph(P, is_planar=True)
+        P.add_half_edge_ccw(1, 3, 2)
+        P.add_half_edge_cw(3, 1, 2)
+        self.check_graph(P, is_planar=True)
+        P.remove_edges_from(((0, 3), (1, 3)))
+        self.check_graph(P, is_planar=True)
+
+
+def check_embedding(G, embedding):
+    """Raises an exception if the combinatorial embedding is not correct
+
+    Parameters
+    ----------
+    G : NetworkX graph
+    embedding : a dict mapping nodes to a list of edges
+        This specifies the ordering of the outgoing edges from a node for
+        a combinatorial embedding
+
+    Notes
+    -----
+    Checks the following things:
+        - The type of the embedding is correct
+        - The nodes and edges match the original graph
+        - Every half edge has its matching opposite half edge
+        - No intersections of edges (checked by Euler's formula)
+    """
+
+    if not isinstance(embedding, nx.PlanarEmbedding):
+        raise nx.NetworkXException("Bad embedding. Not of type nx.PlanarEmbedding")
+
+    # Check structure
+    embedding.check_structure()
+
+    # Check that graphs are equivalent
+
+    assert set(G.nodes) == set(
+        embedding.nodes
+    ), "Bad embedding. Nodes don't match the original graph."
+
+    # Check that the edges are equal
+    g_edges = set()
+    for edge in G.edges:
+        if edge[0] != edge[1]:
+            g_edges.add((edge[0], edge[1]))
+            g_edges.add((edge[1], edge[0]))
+    assert g_edges == set(
+        embedding.edges
+    ), "Bad embedding. Edges don't match the original graph."
+
+
+def check_counterexample(G, sub_graph):
+    """Raises an exception if the counterexample is wrong.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+    subdivision_nodes : set
+        A set of nodes inducing a subgraph as a counterexample
+    """
+    # 1. Create the sub graph
+    sub_graph = nx.Graph(sub_graph)
+
+    # 2. Remove self loops
+    for u in sub_graph:
+        if sub_graph.has_edge(u, u):
+            sub_graph.remove_edge(u, u)
+
+    # keep track of nodes we might need to contract
+    contract = list(sub_graph)
+
+    # 3. Contract Edges
+    while len(contract) > 0:
+        contract_node = contract.pop()
+        if contract_node not in sub_graph:
+            # Node was already contracted
+            continue
+        degree = sub_graph.degree[contract_node]
+        # Check if we can remove the node
+        if degree == 2:
+            # Get the two neighbors
+            neighbors = iter(sub_graph[contract_node])
+            u = next(neighbors)
+            v = next(neighbors)
+            # Save nodes for later
+            contract.append(u)
+            contract.append(v)
+            # Contract edge
+            sub_graph.remove_node(contract_node)
+            sub_graph.add_edge(u, v)
+
+    # 4. Check for isomorphism with K5 or K3_3 graphs
+    if len(sub_graph) == 5:
+        if not nx.is_isomorphic(nx.complete_graph(5), sub_graph):
+            raise nx.NetworkXException("Bad counter example.")
+    elif len(sub_graph) == 6:
+        if not nx.is_isomorphic(nx.complete_bipartite_graph(3, 3), sub_graph):
+            raise nx.NetworkXException("Bad counter example.")
+    else:
+        raise nx.NetworkXException("Bad counter example.")
+
+
+class TestPlanarEmbeddingClass:
+    def test_add_half_edge(self):
+        embedding = nx.PlanarEmbedding()
+        embedding.add_half_edge(0, 1)
+        with pytest.raises(
+            nx.NetworkXException, match="Invalid clockwise reference node."
+        ):
+            embedding.add_half_edge(0, 2, cw=3)
+        with pytest.raises(
+            nx.NetworkXException, match="Invalid counterclockwise reference node."
+        ):
+            embedding.add_half_edge(0, 2, ccw=3)
+        with pytest.raises(
+            nx.NetworkXException, match="Only one of cw/ccw can be specified."
+        ):
+            embedding.add_half_edge(0, 2, cw=1, ccw=1)
+        with pytest.raises(
+            nx.NetworkXException,
+            match=(
+                r"Node already has out-half-edge\(s\), either"
+                " cw or ccw reference node required."
+            ),
+        ):
+            embedding.add_half_edge(0, 2)
+        # these should work
+        embedding.add_half_edge(0, 2, cw=1)
+        embedding.add_half_edge(0, 3, ccw=1)
+        assert sorted(embedding.edges(data=True)) == [
+            (0, 1, {"ccw": 2, "cw": 3}),
+            (0, 2, {"cw": 1, "ccw": 3}),
+            (0, 3, {"cw": 2, "ccw": 1}),
+        ]
+
+    def test_get_data(self):
+        embedding = self.get_star_embedding(4)
+        data = embedding.get_data()
+        data_cmp = {0: [3, 2, 1], 1: [0], 2: [0], 3: [0]}
+        assert data == data_cmp
+
+    def test_edge_removal(self):
+        embedding = nx.PlanarEmbedding()
+        embedding.set_data(
+            {
+                1: [2, 5, 7],
+                2: [1, 3, 4, 5],
+                3: [2, 4],
+                4: [3, 6, 5, 2],
+                5: [7, 1, 2, 4],
+                6: [4, 7],
+                7: [6, 1, 5],
+            }
+        )
+        # remove_edges_from() calls remove_edge(), so both are tested here
+        embedding.remove_edges_from(((5, 4), (1, 5)))
+        embedding.check_structure()
+        embedding_expected = nx.PlanarEmbedding()
+        embedding_expected.set_data(
+            {
+                1: [2, 7],
+                2: [1, 3, 4, 5],
+                3: [2, 4],
+                4: [3, 6, 2],
+                5: [7, 2],
+                6: [4, 7],
+                7: [6, 1, 5],
+            }
+        )
+        assert nx.utils.graphs_equal(embedding, embedding_expected)
+
+    def test_missing_edge_orientation(self):
+        embedding = nx.PlanarEmbedding({1: {2: {}}, 2: {1: {}}})
+        with pytest.raises(nx.NetworkXException):
+            # Invalid structure because the orientation of the edge was not set
+            embedding.check_structure()
+
+    def test_invalid_edge_orientation(self):
+        embedding = nx.PlanarEmbedding(
+            {
+                1: {2: {"cw": 2, "ccw": 2}},
+                2: {1: {"cw": 1, "ccw": 1}},
+                1: {3: {}},
+                3: {1: {}},
+            }
+        )
+        with pytest.raises(nx.NetworkXException):
+            embedding.check_structure()
+
+    def test_missing_half_edge(self):
+        embedding = nx.PlanarEmbedding()
+        embedding.add_half_edge(1, 2)
+        with pytest.raises(nx.NetworkXException):
+            # Invalid structure because other half edge is missing
+            embedding.check_structure()
+
+    def test_not_fulfilling_euler_formula(self):
+        embedding = nx.PlanarEmbedding()
+        for i in range(5):
+            ref = None
+            for j in range(5):
+                if i != j:
+                    embedding.add_half_edge(i, j, cw=ref)
+                    ref = j
+        with pytest.raises(nx.NetworkXException):
+            embedding.check_structure()
+
+    def test_missing_reference(self):
+        embedding = nx.PlanarEmbedding()
+        with pytest.raises(nx.NetworkXException, match="Invalid reference node."):
+            embedding.add_half_edge(1, 2, ccw=3)
+
+    def test_connect_components(self):
+        embedding = nx.PlanarEmbedding()
+        embedding.connect_components(1, 2)
+
+    def test_successful_face_traversal(self):
+        embedding = nx.PlanarEmbedding()
+        embedding.add_half_edge(1, 2)
+        embedding.add_half_edge(2, 1)
+        face = embedding.traverse_face(1, 2)
+        assert face == [1, 2]
+
+    def test_unsuccessful_face_traversal(self):
+        embedding = nx.PlanarEmbedding(
+            {1: {2: {"cw": 3, "ccw": 2}}, 2: {1: {"cw": 3, "ccw": 1}}}
+        )
+        with pytest.raises(nx.NetworkXException):
+            embedding.traverse_face(1, 2)
+
+    def test_forbidden_methods(self):
+        embedding = nx.PlanarEmbedding()
+        embedding.add_node(42)  # no exception
+        embedding.add_nodes_from([(23, 24)])  # no exception
+        with pytest.raises(NotImplementedError):
+            embedding.add_edge(1, 3)
+        with pytest.raises(NotImplementedError):
+            embedding.add_edges_from([(0, 2), (1, 4)])
+        with pytest.raises(NotImplementedError):
+            embedding.add_weighted_edges_from([(0, 2, 350), (1, 4, 125)])
+
+    @staticmethod
+    def get_star_embedding(n):
+        embedding = nx.PlanarEmbedding()
+        ref = None
+        for i in range(1, n):
+            embedding.add_half_edge(0, i, cw=ref)
+            ref = i
+            embedding.add_half_edge(i, 0)
+        return embedding
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_polynomials.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_polynomials.py
new file mode 100644
index 00000000..a81d6a69
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_polynomials.py
@@ -0,0 +1,57 @@
+"""Unit tests for the :mod:`networkx.algorithms.polynomials` module."""
+
+import pytest
+
+import networkx as nx
+
+sympy = pytest.importorskip("sympy")
+
+
+# Mapping of input graphs to a string representation of their tutte polynomials
+_test_tutte_graphs = {
+    nx.complete_graph(1): "1",
+    nx.complete_graph(4): "x**3 + 3*x**2 + 4*x*y + 2*x + y**3 + 3*y**2 + 2*y",
+    nx.cycle_graph(5): "x**4 + x**3 + x**2 + x + y",
+    nx.diamond_graph(): "x**3 + 2*x**2 + 2*x*y + x + y**2 + y",
+}
+
+_test_chromatic_graphs = {
+    nx.complete_graph(1): "x",
+    nx.complete_graph(4): "x**4 - 6*x**3 + 11*x**2 - 6*x",
+    nx.cycle_graph(5): "x**5 - 5*x**4 + 10*x**3 - 10*x**2 + 4*x",
+    nx.diamond_graph(): "x**4 - 5*x**3 + 8*x**2 - 4*x",
+    nx.path_graph(5): "x**5 - 4*x**4 + 6*x**3 - 4*x**2 + x",
+}
+
+
+@pytest.mark.parametrize(("G", "expected"), _test_tutte_graphs.items())
+def test_tutte_polynomial(G, expected):
+    assert nx.tutte_polynomial(G).equals(expected)
+
+
+@pytest.mark.parametrize("G", _test_tutte_graphs.keys())
+def test_tutte_polynomial_disjoint(G):
+    """Tutte polynomial factors into the Tutte polynomials of its components.
+    Verify this property with the disjoint union of two copies of the input graph.
+    """
+    t_g = nx.tutte_polynomial(G)
+    H = nx.disjoint_union(G, G)
+    t_h = nx.tutte_polynomial(H)
+    assert sympy.simplify(t_g * t_g).equals(t_h)
+
+
+@pytest.mark.parametrize(("G", "expected"), _test_chromatic_graphs.items())
+def test_chromatic_polynomial(G, expected):
+    assert nx.chromatic_polynomial(G).equals(expected)
+
+
+@pytest.mark.parametrize("G", _test_chromatic_graphs.keys())
+def test_chromatic_polynomial_disjoint(G):
+    """Chromatic polynomial factors into the Chromatic polynomials of its
+    components. Verify this property with the disjoint union of two copies of
+    the input graph.
+    """
+    x_g = nx.chromatic_polynomial(G)
+    H = nx.disjoint_union(G, G)
+    x_h = nx.chromatic_polynomial(H)
+    assert sympy.simplify(x_g * x_g).equals(x_h)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_reciprocity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_reciprocity.py
new file mode 100644
index 00000000..e713bc43
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_reciprocity.py
@@ -0,0 +1,37 @@
+import pytest
+
+import networkx as nx
+
+
+class TestReciprocity:
+    # test overall reciprocity by passing whole graph
+    def test_reciprocity_digraph(self):
+        DG = nx.DiGraph([(1, 2), (2, 1)])
+        reciprocity = nx.reciprocity(DG)
+        assert reciprocity == 1.0
+
+    # test empty graph's overall reciprocity which will throw an error
+    def test_overall_reciprocity_empty_graph(self):
+        with pytest.raises(nx.NetworkXError):
+            DG = nx.DiGraph()
+            nx.overall_reciprocity(DG)
+
+    # test for reciprocity for a list of nodes
+    def test_reciprocity_graph_nodes(self):
+        DG = nx.DiGraph([(1, 2), (2, 3), (3, 2)])
+        reciprocity = nx.reciprocity(DG, [1, 2])
+        expected_reciprocity = {1: 0.0, 2: 0.6666666666666666}
+        assert reciprocity == expected_reciprocity
+
+    # test for reciprocity for a single node
+    def test_reciprocity_graph_node(self):
+        DG = nx.DiGraph([(1, 2), (2, 3), (3, 2)])
+        reciprocity = nx.reciprocity(DG, 2)
+        assert reciprocity == 0.6666666666666666
+
+    # test for reciprocity for an isolated node
+    def test_reciprocity_graph_isolated_nodes(self):
+        with pytest.raises(nx.NetworkXError):
+            DG = nx.DiGraph([(1, 2)])
+            DG.add_node(4)
+            nx.reciprocity(DG, 4)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_regular.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_regular.py
new file mode 100644
index 00000000..a8b4c3a3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_regular.py
@@ -0,0 +1,92 @@
+import pytest
+
+import networkx
+import networkx as nx
+import networkx.algorithms.regular as reg
+import networkx.generators as gen
+
+
+class TestKFactor:
+    def test_k_factor_trivial(self):
+        g = gen.cycle_graph(4)
+        f = reg.k_factor(g, 2)
+        assert g.edges == f.edges
+
+    def test_k_factor1(self):
+        g = gen.grid_2d_graph(4, 4)
+        g_kf = reg.k_factor(g, 2)
+        for edge in g_kf.edges():
+            assert g.has_edge(edge[0], edge[1])
+        for _, degree in g_kf.degree():
+            assert degree == 2
+
+    def test_k_factor2(self):
+        g = gen.complete_graph(6)
+        g_kf = reg.k_factor(g, 3)
+        for edge in g_kf.edges():
+            assert g.has_edge(edge[0], edge[1])
+        for _, degree in g_kf.degree():
+            assert degree == 3
+
+    def test_k_factor3(self):
+        g = gen.grid_2d_graph(4, 4)
+        with pytest.raises(nx.NetworkXUnfeasible):
+            reg.k_factor(g, 3)
+
+    def test_k_factor4(self):
+        g = gen.lattice.hexagonal_lattice_graph(4, 4)
+        # Perfect matching doesn't exist for 4,4 hexagonal lattice graph
+        with pytest.raises(nx.NetworkXUnfeasible):
+            reg.k_factor(g, 2)
+
+    def test_k_factor5(self):
+        g = gen.complete_graph(6)
+        # small k to exercise SmallKGadget
+        g_kf = reg.k_factor(g, 2)
+        for edge in g_kf.edges():
+            assert g.has_edge(edge[0], edge[1])
+        for _, degree in g_kf.degree():
+            assert degree == 2
+
+
+class TestIsRegular:
+    def test_is_regular1(self):
+        g = gen.cycle_graph(4)
+        assert reg.is_regular(g)
+
+    def test_is_regular2(self):
+        g = gen.complete_graph(5)
+        assert reg.is_regular(g)
+
+    def test_is_regular3(self):
+        g = gen.lollipop_graph(5, 5)
+        assert not reg.is_regular(g)
+
+    def test_is_regular4(self):
+        g = nx.DiGraph()
+        g.add_edges_from([(0, 1), (1, 2), (2, 0)])
+        assert reg.is_regular(g)
+
+
+def test_is_regular_empty_graph_raises():
+    G = nx.Graph()
+    with pytest.raises(nx.NetworkXPointlessConcept, match="Graph has no nodes"):
+        nx.is_regular(G)
+
+
+class TestIsKRegular:
+    def test_is_k_regular1(self):
+        g = gen.cycle_graph(4)
+        assert reg.is_k_regular(g, 2)
+        assert not reg.is_k_regular(g, 3)
+
+    def test_is_k_regular2(self):
+        g = gen.complete_graph(5)
+        assert reg.is_k_regular(g, 4)
+        assert not reg.is_k_regular(g, 3)
+        assert not reg.is_k_regular(g, 6)
+
+    def test_is_k_regular3(self):
+        g = gen.lollipop_graph(5, 5)
+        assert not reg.is_k_regular(g, 5)
+        assert not reg.is_k_regular(g, 6)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_richclub.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_richclub.py
new file mode 100644
index 00000000..1bdb6684
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_richclub.py
@@ -0,0 +1,149 @@
+import pytest
+
+import networkx as nx
+
+
+def test_richclub():
+    G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)])
+    rc = nx.richclub.rich_club_coefficient(G, normalized=False)
+    assert rc == {0: 12.0 / 30, 1: 8.0 / 12}
+
+    # test single value
+    rc0 = nx.richclub.rich_club_coefficient(G, normalized=False)[0]
+    assert rc0 == 12.0 / 30.0
+
+
+def test_richclub_seed():
+    G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)])
+    rcNorm = nx.richclub.rich_club_coefficient(G, Q=2, seed=1)
+    assert rcNorm == {0: 1.0, 1: 1.0}
+
+
+def test_richclub_normalized():
+    G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)])
+    rcNorm = nx.richclub.rich_club_coefficient(G, Q=2, seed=42)
+    assert rcNorm == {0: 1.0, 1: 1.0}
+
+
+def test_richclub2():
+    T = nx.balanced_tree(2, 10)
+    rc = nx.richclub.rich_club_coefficient(T, normalized=False)
+    assert rc == {
+        0: 4092 / (2047 * 2046.0),
+        1: (2044.0 / (1023 * 1022)),
+        2: (2040.0 / (1022 * 1021)),
+    }
+
+
+def test_richclub3():
+    # tests edgecase
+    G = nx.karate_club_graph()
+    rc = nx.rich_club_coefficient(G, normalized=False)
+    assert rc == {
+        0: 156.0 / 1122,
+        1: 154.0 / 1056,
+        2: 110.0 / 462,
+        3: 78.0 / 240,
+        4: 44.0 / 90,
+        5: 22.0 / 42,
+        6: 10.0 / 20,
+        7: 10.0 / 20,
+        8: 10.0 / 20,
+        9: 6.0 / 12,
+        10: 2.0 / 6,
+        11: 2.0 / 6,
+        12: 0.0,
+        13: 0.0,
+        14: 0.0,
+        15: 0.0,
+    }
+
+
+def test_richclub4():
+    G = nx.Graph()
+    G.add_edges_from(
+        [(0, 1), (0, 2), (0, 3), (0, 4), (4, 5), (5, 9), (6, 9), (7, 9), (8, 9)]
+    )
+    rc = nx.rich_club_coefficient(G, normalized=False)
+    assert rc == {0: 18 / 90.0, 1: 6 / 12.0, 2: 0.0, 3: 0.0}
+
+
+def test_richclub_exception():
+    with pytest.raises(nx.NetworkXNotImplemented):
+        G = nx.DiGraph()
+        nx.rich_club_coefficient(G)
+
+
+def test_rich_club_exception2():
+    with pytest.raises(nx.NetworkXNotImplemented):
+        G = nx.MultiGraph()
+        nx.rich_club_coefficient(G)
+
+
+def test_rich_club_selfloop():
+    G = nx.Graph()  # or DiGraph, MultiGraph, MultiDiGraph, etc
+    G.add_edge(1, 1)  # self loop
+    G.add_edge(1, 2)
+    with pytest.raises(
+        Exception,
+        match="rich_club_coefficient is not implemented for " "graphs with self loops.",
+    ):
+        nx.rich_club_coefficient(G)
+
+
+def test_rich_club_leq_3_nodes_unnormalized():
+    # edgeless graphs upto 3 nodes
+    G = nx.Graph()
+    rc = nx.rich_club_coefficient(G, normalized=False)
+    assert rc == {}
+
+    for i in range(3):
+        G.add_node(i)
+        rc = nx.rich_club_coefficient(G, normalized=False)
+        assert rc == {}
+
+    # 2 nodes, single edge
+    G = nx.Graph()
+    G.add_edge(0, 1)
+    rc = nx.rich_club_coefficient(G, normalized=False)
+    assert rc == {0: 1}
+
+    # 3 nodes, single edge
+    G = nx.Graph()
+    G.add_nodes_from([0, 1, 2])
+    G.add_edge(0, 1)
+    rc = nx.rich_club_coefficient(G, normalized=False)
+    assert rc == {0: 1}
+
+    # 3 nodes, 2 edges
+    G.add_edge(1, 2)
+    rc = nx.rich_club_coefficient(G, normalized=False)
+    assert rc == {0: 2 / 3}
+
+    # 3 nodes, 3 edges
+    G.add_edge(0, 2)
+    rc = nx.rich_club_coefficient(G, normalized=False)
+    assert rc == {0: 1, 1: 1}
+
+
+def test_rich_club_leq_3_nodes_normalized():
+    G = nx.Graph()
+    with pytest.raises(
+        nx.exception.NetworkXError,
+        match="Graph has fewer than four nodes",
+    ):
+        rc = nx.rich_club_coefficient(G, normalized=True)
+
+    for i in range(3):
+        G.add_node(i)
+        with pytest.raises(
+            nx.exception.NetworkXError,
+            match="Graph has fewer than four nodes",
+        ):
+            rc = nx.rich_club_coefficient(G, normalized=True)
+
+
+# def test_richclub2_normalized():
+#    T = nx.balanced_tree(2,10)
+#    rcNorm = nx.richclub.rich_club_coefficient(T,Q=2)
+#    assert_true(rcNorm[0] ==1.0 and rcNorm[1] < 0.9 and rcNorm[2] < 0.9)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_similarity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_similarity.py
new file mode 100644
index 00000000..3836ccfe
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_similarity.py
@@ -0,0 +1,946 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms.similarity import (
+    graph_edit_distance,
+    optimal_edit_paths,
+    optimize_graph_edit_distance,
+)
+from networkx.generators.classic import (
+    circular_ladder_graph,
+    cycle_graph,
+    path_graph,
+    wheel_graph,
+)
+
+
+def nmatch(n1, n2):
+    return n1 == n2
+
+
+def ematch(e1, e2):
+    return e1 == e2
+
+
+def getCanonical():
+    G = nx.Graph()
+    G.add_node("A", label="A")
+    G.add_node("B", label="B")
+    G.add_node("C", label="C")
+    G.add_node("D", label="D")
+    G.add_edge("A", "B", label="a-b")
+    G.add_edge("B", "C", label="b-c")
+    G.add_edge("B", "D", label="b-d")
+    return G
+
+
+class TestSimilarity:
+    @classmethod
+    def setup_class(cls):
+        global np
+        np = pytest.importorskip("numpy")
+        pytest.importorskip("scipy")
+
+    def test_graph_edit_distance_roots_and_timeout(self):
+        G0 = nx.star_graph(5)
+        G1 = G0.copy()
+        pytest.raises(ValueError, graph_edit_distance, G0, G1, roots=[2])
+        pytest.raises(ValueError, graph_edit_distance, G0, G1, roots=[2, 3, 4])
+        pytest.raises(nx.NodeNotFound, graph_edit_distance, G0, G1, roots=(9, 3))
+        pytest.raises(nx.NodeNotFound, graph_edit_distance, G0, G1, roots=(3, 9))
+        pytest.raises(nx.NodeNotFound, graph_edit_distance, G0, G1, roots=(9, 9))
+        assert graph_edit_distance(G0, G1, roots=(1, 2)) == 0
+        assert graph_edit_distance(G0, G1, roots=(0, 1)) == 8
+        assert graph_edit_distance(G0, G1, roots=(1, 2), timeout=5) == 0
+        assert graph_edit_distance(G0, G1, roots=(0, 1), timeout=5) == 8
+        assert graph_edit_distance(G0, G1, roots=(0, 1), timeout=0.0001) is None
+        # test raise on 0 timeout
+        pytest.raises(nx.NetworkXError, graph_edit_distance, G0, G1, timeout=0)
+
+    def test_graph_edit_distance(self):
+        G0 = nx.Graph()
+        G1 = path_graph(6)
+        G2 = cycle_graph(6)
+        G3 = wheel_graph(7)
+
+        assert graph_edit_distance(G0, G0) == 0
+        assert graph_edit_distance(G0, G1) == 11
+        assert graph_edit_distance(G1, G0) == 11
+        assert graph_edit_distance(G0, G2) == 12
+        assert graph_edit_distance(G2, G0) == 12
+        assert graph_edit_distance(G0, G3) == 19
+        assert graph_edit_distance(G3, G0) == 19
+
+        assert graph_edit_distance(G1, G1) == 0
+        assert graph_edit_distance(G1, G2) == 1
+        assert graph_edit_distance(G2, G1) == 1
+        assert graph_edit_distance(G1, G3) == 8
+        assert graph_edit_distance(G3, G1) == 8
+
+        assert graph_edit_distance(G2, G2) == 0
+        assert graph_edit_distance(G2, G3) == 7
+        assert graph_edit_distance(G3, G2) == 7
+
+        assert graph_edit_distance(G3, G3) == 0
+
+    def test_graph_edit_distance_node_match(self):
+        G1 = cycle_graph(5)
+        G2 = cycle_graph(5)
+        for n, attr in G1.nodes.items():
+            attr["color"] = "red" if n % 2 == 0 else "blue"
+        for n, attr in G2.nodes.items():
+            attr["color"] = "red" if n % 2 == 1 else "blue"
+        assert graph_edit_distance(G1, G2) == 0
+        assert (
+            graph_edit_distance(
+                G1, G2, node_match=lambda n1, n2: n1["color"] == n2["color"]
+            )
+            == 1
+        )
+
+    def test_graph_edit_distance_edge_match(self):
+        G1 = path_graph(6)
+        G2 = path_graph(6)
+        for e, attr in G1.edges.items():
+            attr["color"] = "red" if min(e) % 2 == 0 else "blue"
+        for e, attr in G2.edges.items():
+            attr["color"] = "red" if min(e) // 3 == 0 else "blue"
+        assert graph_edit_distance(G1, G2) == 0
+        assert (
+            graph_edit_distance(
+                G1, G2, edge_match=lambda e1, e2: e1["color"] == e2["color"]
+            )
+            == 2
+        )
+
+    def test_graph_edit_distance_node_cost(self):
+        G1 = path_graph(6)
+        G2 = path_graph(6)
+        for n, attr in G1.nodes.items():
+            attr["color"] = "red" if n % 2 == 0 else "blue"
+        for n, attr in G2.nodes.items():
+            attr["color"] = "red" if n % 2 == 1 else "blue"
+
+        def node_subst_cost(uattr, vattr):
+            if uattr["color"] == vattr["color"]:
+                return 1
+            else:
+                return 10
+
+        def node_del_cost(attr):
+            if attr["color"] == "blue":
+                return 20
+            else:
+                return 50
+
+        def node_ins_cost(attr):
+            if attr["color"] == "blue":
+                return 40
+            else:
+                return 100
+
+        assert (
+            graph_edit_distance(
+                G1,
+                G2,
+                node_subst_cost=node_subst_cost,
+                node_del_cost=node_del_cost,
+                node_ins_cost=node_ins_cost,
+            )
+            == 6
+        )
+
+    def test_graph_edit_distance_edge_cost(self):
+        G1 = path_graph(6)
+        G2 = path_graph(6)
+        for e, attr in G1.edges.items():
+            attr["color"] = "red" if min(e) % 2 == 0 else "blue"
+        for e, attr in G2.edges.items():
+            attr["color"] = "red" if min(e) // 3 == 0 else "blue"
+
+        def edge_subst_cost(gattr, hattr):
+            if gattr["color"] == hattr["color"]:
+                return 0.01
+            else:
+                return 0.1
+
+        def edge_del_cost(attr):
+            if attr["color"] == "blue":
+                return 0.2
+            else:
+                return 0.5
+
+        def edge_ins_cost(attr):
+            if attr["color"] == "blue":
+                return 0.4
+            else:
+                return 1.0
+
+        assert (
+            graph_edit_distance(
+                G1,
+                G2,
+                edge_subst_cost=edge_subst_cost,
+                edge_del_cost=edge_del_cost,
+                edge_ins_cost=edge_ins_cost,
+            )
+            == 0.23
+        )
+
+    def test_graph_edit_distance_upper_bound(self):
+        G1 = circular_ladder_graph(2)
+        G2 = circular_ladder_graph(6)
+        assert graph_edit_distance(G1, G2, upper_bound=5) is None
+        assert graph_edit_distance(G1, G2, upper_bound=24) == 22
+        assert graph_edit_distance(G1, G2) == 22
+
+    def test_optimal_edit_paths(self):
+        G1 = path_graph(3)
+        G2 = cycle_graph(3)
+        paths, cost = optimal_edit_paths(G1, G2)
+        assert cost == 1
+        assert len(paths) == 6
+
+        def canonical(vertex_path, edge_path):
+            return (
+                tuple(sorted(vertex_path)),
+                tuple(sorted(edge_path, key=lambda x: (None in x, x))),
+            )
+
+        expected_paths = [
+            (
+                [(0, 0), (1, 1), (2, 2)],
+                [((0, 1), (0, 1)), ((1, 2), (1, 2)), (None, (0, 2))],
+            ),
+            (
+                [(0, 0), (1, 2), (2, 1)],
+                [((0, 1), (0, 2)), ((1, 2), (1, 2)), (None, (0, 1))],
+            ),
+            (
+                [(0, 1), (1, 0), (2, 2)],
+                [((0, 1), (0, 1)), ((1, 2), (0, 2)), (None, (1, 2))],
+            ),
+            (
+                [(0, 1), (1, 2), (2, 0)],
+                [((0, 1), (1, 2)), ((1, 2), (0, 2)), (None, (0, 1))],
+            ),
+            (
+                [(0, 2), (1, 0), (2, 1)],
+                [((0, 1), (0, 2)), ((1, 2), (0, 1)), (None, (1, 2))],
+            ),
+            (
+                [(0, 2), (1, 1), (2, 0)],
+                [((0, 1), (1, 2)), ((1, 2), (0, 1)), (None, (0, 2))],
+            ),
+        ]
+        assert {canonical(*p) for p in paths} == {canonical(*p) for p in expected_paths}
+
+    def test_optimize_graph_edit_distance(self):
+        G1 = circular_ladder_graph(2)
+        G2 = circular_ladder_graph(6)
+        bestcost = 1000
+        for cost in optimize_graph_edit_distance(G1, G2):
+            assert cost < bestcost
+            bestcost = cost
+        assert bestcost == 22
+
+    # def test_graph_edit_distance_bigger(self):
+    #     G1 = circular_ladder_graph(12)
+    #     G2 = circular_ladder_graph(16)
+    #     assert_equal(graph_edit_distance(G1, G2), 22)
+
+    def test_selfloops(self):
+        G0 = nx.Graph()
+        G1 = nx.Graph()
+        G1.add_edges_from((("A", "A"), ("A", "B")))
+        G2 = nx.Graph()
+        G2.add_edges_from((("A", "B"), ("B", "B")))
+        G3 = nx.Graph()
+        G3.add_edges_from((("A", "A"), ("A", "B"), ("B", "B")))
+
+        assert graph_edit_distance(G0, G0) == 0
+        assert graph_edit_distance(G0, G1) == 4
+        assert graph_edit_distance(G1, G0) == 4
+        assert graph_edit_distance(G0, G2) == 4
+        assert graph_edit_distance(G2, G0) == 4
+        assert graph_edit_distance(G0, G3) == 5
+        assert graph_edit_distance(G3, G0) == 5
+
+        assert graph_edit_distance(G1, G1) == 0
+        assert graph_edit_distance(G1, G2) == 0
+        assert graph_edit_distance(G2, G1) == 0
+        assert graph_edit_distance(G1, G3) == 1
+        assert graph_edit_distance(G3, G1) == 1
+
+        assert graph_edit_distance(G2, G2) == 0
+        assert graph_edit_distance(G2, G3) == 1
+        assert graph_edit_distance(G3, G2) == 1
+
+        assert graph_edit_distance(G3, G3) == 0
+
+    def test_digraph(self):
+        G0 = nx.DiGraph()
+        G1 = nx.DiGraph()
+        G1.add_edges_from((("A", "B"), ("B", "C"), ("C", "D"), ("D", "A")))
+        G2 = nx.DiGraph()
+        G2.add_edges_from((("A", "B"), ("B", "C"), ("C", "D"), ("A", "D")))
+        G3 = nx.DiGraph()
+        G3.add_edges_from((("A", "B"), ("A", "C"), ("B", "D"), ("C", "D")))
+
+        assert graph_edit_distance(G0, G0) == 0
+        assert graph_edit_distance(G0, G1) == 8
+        assert graph_edit_distance(G1, G0) == 8
+        assert graph_edit_distance(G0, G2) == 8
+        assert graph_edit_distance(G2, G0) == 8
+        assert graph_edit_distance(G0, G3) == 8
+        assert graph_edit_distance(G3, G0) == 8
+
+        assert graph_edit_distance(G1, G1) == 0
+        assert graph_edit_distance(G1, G2) == 2
+        assert graph_edit_distance(G2, G1) == 2
+        assert graph_edit_distance(G1, G3) == 4
+        assert graph_edit_distance(G3, G1) == 4
+
+        assert graph_edit_distance(G2, G2) == 0
+        assert graph_edit_distance(G2, G3) == 2
+        assert graph_edit_distance(G3, G2) == 2
+
+        assert graph_edit_distance(G3, G3) == 0
+
+    def test_multigraph(self):
+        G0 = nx.MultiGraph()
+        G1 = nx.MultiGraph()
+        G1.add_edges_from((("A", "B"), ("B", "C"), ("A", "C")))
+        G2 = nx.MultiGraph()
+        G2.add_edges_from((("A", "B"), ("B", "C"), ("B", "C"), ("A", "C")))
+        G3 = nx.MultiGraph()
+        G3.add_edges_from((("A", "B"), ("B", "C"), ("A", "C"), ("A", "C"), ("A", "C")))
+
+        assert graph_edit_distance(G0, G0) == 0
+        assert graph_edit_distance(G0, G1) == 6
+        assert graph_edit_distance(G1, G0) == 6
+        assert graph_edit_distance(G0, G2) == 7
+        assert graph_edit_distance(G2, G0) == 7
+        assert graph_edit_distance(G0, G3) == 8
+        assert graph_edit_distance(G3, G0) == 8
+
+        assert graph_edit_distance(G1, G1) == 0
+        assert graph_edit_distance(G1, G2) == 1
+        assert graph_edit_distance(G2, G1) == 1
+        assert graph_edit_distance(G1, G3) == 2
+        assert graph_edit_distance(G3, G1) == 2
+
+        assert graph_edit_distance(G2, G2) == 0
+        assert graph_edit_distance(G2, G3) == 1
+        assert graph_edit_distance(G3, G2) == 1
+
+        assert graph_edit_distance(G3, G3) == 0
+
+    def test_multidigraph(self):
+        G1 = nx.MultiDiGraph()
+        G1.add_edges_from(
+            (
+                ("hardware", "kernel"),
+                ("kernel", "hardware"),
+                ("kernel", "userspace"),
+                ("userspace", "kernel"),
+            )
+        )
+        G2 = nx.MultiDiGraph()
+        G2.add_edges_from(
+            (
+                ("winter", "spring"),
+                ("spring", "summer"),
+                ("summer", "autumn"),
+                ("autumn", "winter"),
+            )
+        )
+
+        assert graph_edit_distance(G1, G2) == 5
+        assert graph_edit_distance(G2, G1) == 5
+
+    # by https://github.com/jfbeaumont
+    def testCopy(self):
+        G = nx.Graph()
+        G.add_node("A", label="A")
+        G.add_node("B", label="B")
+        G.add_edge("A", "B", label="a-b")
+        assert (
+            graph_edit_distance(G, G.copy(), node_match=nmatch, edge_match=ematch) == 0
+        )
+
+    def testSame(self):
+        G1 = nx.Graph()
+        G1.add_node("A", label="A")
+        G1.add_node("B", label="B")
+        G1.add_edge("A", "B", label="a-b")
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_edge("A", "B", label="a-b")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 0
+
+    def testOneEdgeLabelDiff(self):
+        G1 = nx.Graph()
+        G1.add_node("A", label="A")
+        G1.add_node("B", label="B")
+        G1.add_edge("A", "B", label="a-b")
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_edge("A", "B", label="bad")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1
+
+    def testOneNodeLabelDiff(self):
+        G1 = nx.Graph()
+        G1.add_node("A", label="A")
+        G1.add_node("B", label="B")
+        G1.add_edge("A", "B", label="a-b")
+        G2 = nx.Graph()
+        G2.add_node("A", label="Z")
+        G2.add_node("B", label="B")
+        G2.add_edge("A", "B", label="a-b")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1
+
+    def testOneExtraNode(self):
+        G1 = nx.Graph()
+        G1.add_node("A", label="A")
+        G1.add_node("B", label="B")
+        G1.add_edge("A", "B", label="a-b")
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_edge("A", "B", label="a-b")
+        G2.add_node("C", label="C")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1
+
+    def testOneExtraEdge(self):
+        G1 = nx.Graph()
+        G1.add_node("A", label="A")
+        G1.add_node("B", label="B")
+        G1.add_node("C", label="C")
+        G1.add_node("C", label="C")
+        G1.add_edge("A", "B", label="a-b")
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_node("C", label="C")
+        G2.add_edge("A", "B", label="a-b")
+        G2.add_edge("A", "C", label="a-c")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1
+
+    def testOneExtraNodeAndEdge(self):
+        G1 = nx.Graph()
+        G1.add_node("A", label="A")
+        G1.add_node("B", label="B")
+        G1.add_edge("A", "B", label="a-b")
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_node("C", label="C")
+        G2.add_edge("A", "B", label="a-b")
+        G2.add_edge("A", "C", label="a-c")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 2
+
+    def testGraph1(self):
+        G1 = getCanonical()
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_node("D", label="D")
+        G2.add_node("E", label="E")
+        G2.add_edge("A", "B", label="a-b")
+        G2.add_edge("B", "D", label="b-d")
+        G2.add_edge("D", "E", label="d-e")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 3
+
+    def testGraph2(self):
+        G1 = getCanonical()
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_node("C", label="C")
+        G2.add_node("D", label="D")
+        G2.add_node("E", label="E")
+        G2.add_edge("A", "B", label="a-b")
+        G2.add_edge("B", "C", label="b-c")
+        G2.add_edge("C", "D", label="c-d")
+        G2.add_edge("C", "E", label="c-e")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 4
+
+    def testGraph3(self):
+        G1 = getCanonical()
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_node("C", label="C")
+        G2.add_node("D", label="D")
+        G2.add_node("E", label="E")
+        G2.add_node("F", label="F")
+        G2.add_node("G", label="G")
+        G2.add_edge("A", "C", label="a-c")
+        G2.add_edge("A", "D", label="a-d")
+        G2.add_edge("D", "E", label="d-e")
+        G2.add_edge("D", "F", label="d-f")
+        G2.add_edge("D", "G", label="d-g")
+        G2.add_edge("E", "B", label="e-b")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 12
+
+    def testGraph4(self):
+        G1 = getCanonical()
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_node("C", label="C")
+        G2.add_node("D", label="D")
+        G2.add_edge("A", "B", label="a-b")
+        G2.add_edge("B", "C", label="b-c")
+        G2.add_edge("C", "D", label="c-d")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 2
+
+    def testGraph4_a(self):
+        G1 = getCanonical()
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_node("C", label="C")
+        G2.add_node("D", label="D")
+        G2.add_edge("A", "B", label="a-b")
+        G2.add_edge("B", "C", label="b-c")
+        G2.add_edge("A", "D", label="a-d")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 2
+
+    def testGraph4_b(self):
+        G1 = getCanonical()
+        G2 = nx.Graph()
+        G2.add_node("A", label="A")
+        G2.add_node("B", label="B")
+        G2.add_node("C", label="C")
+        G2.add_node("D", label="D")
+        G2.add_edge("A", "B", label="a-b")
+        G2.add_edge("B", "C", label="b-c")
+        G2.add_edge("B", "D", label="bad")
+        assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1
+
+    # note: nx.simrank_similarity_numpy not included because returns np.array
+    simrank_algs = [
+        nx.simrank_similarity,
+        nx.algorithms.similarity._simrank_similarity_python,
+    ]
+
+    @pytest.mark.parametrize("simrank_similarity", simrank_algs)
+    def test_simrank_no_source_no_target(self, simrank_similarity):
+        G = nx.cycle_graph(5)
+        expected = {
+            0: {
+                0: 1,
+                1: 0.3951219505902448,
+                2: 0.5707317069281646,
+                3: 0.5707317069281646,
+                4: 0.3951219505902449,
+            },
+            1: {
+                0: 0.3951219505902448,
+                1: 1,
+                2: 0.3951219505902449,
+                3: 0.5707317069281646,
+                4: 0.5707317069281646,
+            },
+            2: {
+                0: 0.5707317069281646,
+                1: 0.3951219505902449,
+                2: 1,
+                3: 0.3951219505902449,
+                4: 0.5707317069281646,
+            },
+            3: {
+                0: 0.5707317069281646,
+                1: 0.5707317069281646,
+                2: 0.3951219505902449,
+                3: 1,
+                4: 0.3951219505902449,
+            },
+            4: {
+                0: 0.3951219505902449,
+                1: 0.5707317069281646,
+                2: 0.5707317069281646,
+                3: 0.3951219505902449,
+                4: 1,
+            },
+        }
+        actual = simrank_similarity(G)
+        for k, v in expected.items():
+            assert v == pytest.approx(actual[k], abs=1e-2)
+
+        # For a DiGraph test, use the first graph from the paper cited in
+        # the docs: https://dl.acm.org/doi/pdf/10.1145/775047.775126
+        G = nx.DiGraph()
+        G.add_node(0, label="Univ")
+        G.add_node(1, label="ProfA")
+        G.add_node(2, label="ProfB")
+        G.add_node(3, label="StudentA")
+        G.add_node(4, label="StudentB")
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 4), (4, 2), (3, 0)])
+
+        expected = {
+            0: {0: 1, 1: 0.0, 2: 0.1323363991265798, 3: 0.0, 4: 0.03387811817640443},
+            1: {0: 0.0, 1: 1, 2: 0.4135512472705618, 3: 0.0, 4: 0.10586911930126384},
+            2: {
+                0: 0.1323363991265798,
+                1: 0.4135512472705618,
+                2: 1,
+                3: 0.04234764772050554,
+                4: 0.08822426608438655,
+            },
+            3: {0: 0.0, 1: 0.0, 2: 0.04234764772050554, 3: 1, 4: 0.3308409978164495},
+            4: {
+                0: 0.03387811817640443,
+                1: 0.10586911930126384,
+                2: 0.08822426608438655,
+                3: 0.3308409978164495,
+                4: 1,
+            },
+        }
+        # Use the importance_factor from the paper to get the same numbers.
+        actual = simrank_similarity(G, importance_factor=0.8)
+        for k, v in expected.items():
+            assert v == pytest.approx(actual[k], abs=1e-2)
+
+    @pytest.mark.parametrize("simrank_similarity", simrank_algs)
+    def test_simrank_source_no_target(self, simrank_similarity):
+        G = nx.cycle_graph(5)
+        expected = {
+            0: 1,
+            1: 0.3951219505902448,
+            2: 0.5707317069281646,
+            3: 0.5707317069281646,
+            4: 0.3951219505902449,
+        }
+        actual = simrank_similarity(G, source=0)
+        assert expected == pytest.approx(actual, abs=1e-2)
+
+        # For a DiGraph test, use the first graph from the paper cited in
+        # the docs: https://dl.acm.org/doi/pdf/10.1145/775047.775126
+        G = nx.DiGraph()
+        G.add_node(0, label="Univ")
+        G.add_node(1, label="ProfA")
+        G.add_node(2, label="ProfB")
+        G.add_node(3, label="StudentA")
+        G.add_node(4, label="StudentB")
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 4), (4, 2), (3, 0)])
+
+        expected = {0: 1, 1: 0.0, 2: 0.1323363991265798, 3: 0.0, 4: 0.03387811817640443}
+        # Use the importance_factor from the paper to get the same numbers.
+        actual = simrank_similarity(G, importance_factor=0.8, source=0)
+        assert expected == pytest.approx(actual, abs=1e-2)
+
+    @pytest.mark.parametrize("simrank_similarity", simrank_algs)
+    def test_simrank_noninteger_nodes(self, simrank_similarity):
+        G = nx.cycle_graph(5)
+        G = nx.relabel_nodes(G, dict(enumerate("abcde")))
+        expected = {
+            "a": 1,
+            "b": 0.3951219505902448,
+            "c": 0.5707317069281646,
+            "d": 0.5707317069281646,
+            "e": 0.3951219505902449,
+        }
+        actual = simrank_similarity(G, source="a")
+        assert expected == pytest.approx(actual, abs=1e-2)
+
+        # For a DiGraph test, use the first graph from the paper cited in
+        # the docs: https://dl.acm.org/doi/pdf/10.1145/775047.775126
+        G = nx.DiGraph()
+        G.add_node(0, label="Univ")
+        G.add_node(1, label="ProfA")
+        G.add_node(2, label="ProfB")
+        G.add_node(3, label="StudentA")
+        G.add_node(4, label="StudentB")
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 4), (4, 2), (3, 0)])
+        node_labels = dict(enumerate(nx.get_node_attributes(G, "label").values()))
+        G = nx.relabel_nodes(G, node_labels)
+
+        expected = {
+            "Univ": 1,
+            "ProfA": 0.0,
+            "ProfB": 0.1323363991265798,
+            "StudentA": 0.0,
+            "StudentB": 0.03387811817640443,
+        }
+        # Use the importance_factor from the paper to get the same numbers.
+        actual = simrank_similarity(G, importance_factor=0.8, source="Univ")
+        assert expected == pytest.approx(actual, abs=1e-2)
+
+    @pytest.mark.parametrize("simrank_similarity", simrank_algs)
+    def test_simrank_source_and_target(self, simrank_similarity):
+        G = nx.cycle_graph(5)
+        expected = 1
+        actual = simrank_similarity(G, source=0, target=0)
+        assert expected == pytest.approx(actual, abs=1e-2)
+
+        # For a DiGraph test, use the first graph from the paper cited in
+        # the docs: https://dl.acm.org/doi/pdf/10.1145/775047.775126
+        G = nx.DiGraph()
+        G.add_node(0, label="Univ")
+        G.add_node(1, label="ProfA")
+        G.add_node(2, label="ProfB")
+        G.add_node(3, label="StudentA")
+        G.add_node(4, label="StudentB")
+        G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 4), (4, 2), (3, 0)])
+
+        expected = 0.1323363991265798
+        # Use the importance_factor from the paper to get the same numbers.
+        # Use the pair (0,2) because (0,0) and (0,1) have trivial results.
+        actual = simrank_similarity(G, importance_factor=0.8, source=0, target=2)
+        assert expected == pytest.approx(actual, abs=1e-5)
+
+    @pytest.mark.parametrize("alg", simrank_algs)
+    def test_simrank_max_iterations(self, alg):
+        G = nx.cycle_graph(5)
+        pytest.raises(nx.ExceededMaxIterations, alg, G, max_iterations=10)
+
+    def test_simrank_source_not_found(self):
+        G = nx.cycle_graph(5)
+        with pytest.raises(nx.NodeNotFound, match="Source node 10 not in G"):
+            nx.simrank_similarity(G, source=10)
+
+    def test_simrank_target_not_found(self):
+        G = nx.cycle_graph(5)
+        with pytest.raises(nx.NodeNotFound, match="Target node 10 not in G"):
+            nx.simrank_similarity(G, target=10)
+
+    def test_simrank_between_versions(self):
+        G = nx.cycle_graph(5)
+        # _python tolerance 1e-4
+        expected_python_tol4 = {
+            0: 1,
+            1: 0.394512499239852,
+            2: 0.5703550452791322,
+            3: 0.5703550452791323,
+            4: 0.394512499239852,
+        }
+        # _numpy tolerance 1e-4
+        expected_numpy_tol4 = {
+            0: 1.0,
+            1: 0.3947180735764555,
+            2: 0.570482097206368,
+            3: 0.570482097206368,
+            4: 0.3947180735764555,
+        }
+        actual = nx.simrank_similarity(G, source=0)
+        assert expected_numpy_tol4 == pytest.approx(actual, abs=1e-7)
+        # versions differ at 1e-4 level but equal at 1e-3
+        assert expected_python_tol4 != pytest.approx(actual, abs=1e-4)
+        assert expected_python_tol4 == pytest.approx(actual, abs=1e-3)
+
+        actual = nx.similarity._simrank_similarity_python(G, source=0)
+        assert expected_python_tol4 == pytest.approx(actual, abs=1e-7)
+        # versions differ at 1e-4 level but equal at 1e-3
+        assert expected_numpy_tol4 != pytest.approx(actual, abs=1e-4)
+        assert expected_numpy_tol4 == pytest.approx(actual, abs=1e-3)
+
+    def test_simrank_numpy_no_source_no_target(self):
+        G = nx.cycle_graph(5)
+        expected = np.array(
+            [
+                [
+                    1.0,
+                    0.3947180735764555,
+                    0.570482097206368,
+                    0.570482097206368,
+                    0.3947180735764555,
+                ],
+                [
+                    0.3947180735764555,
+                    1.0,
+                    0.3947180735764555,
+                    0.570482097206368,
+                    0.570482097206368,
+                ],
+                [
+                    0.570482097206368,
+                    0.3947180735764555,
+                    1.0,
+                    0.3947180735764555,
+                    0.570482097206368,
+                ],
+                [
+                    0.570482097206368,
+                    0.570482097206368,
+                    0.3947180735764555,
+                    1.0,
+                    0.3947180735764555,
+                ],
+                [
+                    0.3947180735764555,
+                    0.570482097206368,
+                    0.570482097206368,
+                    0.3947180735764555,
+                    1.0,
+                ],
+            ]
+        )
+        actual = nx.similarity._simrank_similarity_numpy(G)
+        np.testing.assert_allclose(expected, actual, atol=1e-7)
+
+    def test_simrank_numpy_source_no_target(self):
+        G = nx.cycle_graph(5)
+        expected = np.array(
+            [
+                1.0,
+                0.3947180735764555,
+                0.570482097206368,
+                0.570482097206368,
+                0.3947180735764555,
+            ]
+        )
+        actual = nx.similarity._simrank_similarity_numpy(G, source=0)
+        np.testing.assert_allclose(expected, actual, atol=1e-7)
+
+    def test_simrank_numpy_source_and_target(self):
+        G = nx.cycle_graph(5)
+        expected = 1.0
+        actual = nx.similarity._simrank_similarity_numpy(G, source=0, target=0)
+        np.testing.assert_allclose(expected, actual, atol=1e-7)
+
+    def test_panther_similarity_unweighted(self):
+        np.random.seed(42)
+
+        G = nx.Graph()
+        G.add_edge(0, 1)
+        G.add_edge(0, 2)
+        G.add_edge(0, 3)
+        G.add_edge(1, 2)
+        G.add_edge(2, 4)
+        expected = {3: 0.5, 2: 0.5, 1: 0.5, 4: 0.125}
+        sim = nx.panther_similarity(G, 0, path_length=2)
+        assert sim == expected
+
+    def test_panther_similarity_weighted(self):
+        np.random.seed(42)
+
+        G = nx.Graph()
+        G.add_edge("v1", "v2", w=5)
+        G.add_edge("v1", "v3", w=1)
+        G.add_edge("v1", "v4", w=2)
+        G.add_edge("v2", "v3", w=0.1)
+        G.add_edge("v3", "v5", w=1)
+        expected = {"v3": 0.75, "v4": 0.5, "v2": 0.5, "v5": 0.25}
+        sim = nx.panther_similarity(G, "v1", path_length=2, weight="w")
+        assert sim == expected
+
+    def test_panther_similarity_source_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)])
+        with pytest.raises(nx.NodeNotFound, match="Source node 10 not in G"):
+            nx.panther_similarity(G, source=10)
+
+    def test_panther_similarity_isolated(self):
+        G = nx.Graph()
+        G.add_nodes_from(range(5))
+        with pytest.raises(
+            nx.NetworkXUnfeasible,
+            match="Panther similarity is not defined for the isolated source node 1.",
+        ):
+            nx.panther_similarity(G, source=1)
+
+    def test_generate_random_paths_unweighted(self):
+        index_map = {}
+        num_paths = 10
+        path_length = 2
+        G = nx.Graph()
+        G.add_edge(0, 1)
+        G.add_edge(0, 2)
+        G.add_edge(0, 3)
+        G.add_edge(1, 2)
+        G.add_edge(2, 4)
+        paths = nx.generate_random_paths(
+            G, num_paths, path_length=path_length, index_map=index_map, seed=42
+        )
+        expected_paths = [
+            [3, 0, 3],
+            [4, 2, 1],
+            [2, 1, 0],
+            [2, 0, 3],
+            [3, 0, 1],
+            [3, 0, 1],
+            [4, 2, 0],
+            [2, 1, 0],
+            [3, 0, 2],
+            [2, 1, 2],
+        ]
+        expected_map = {
+            0: {0, 2, 3, 4, 5, 6, 7, 8},
+            1: {1, 2, 4, 5, 7, 9},
+            2: {1, 2, 3, 6, 7, 8, 9},
+            3: {0, 3, 4, 5, 8},
+            4: {1, 6},
+        }
+
+        assert expected_paths == list(paths)
+        assert expected_map == index_map
+
+    def test_generate_random_paths_weighted(self):
+        np.random.seed(42)
+
+        index_map = {}
+        num_paths = 10
+        path_length = 6
+        G = nx.Graph()
+        G.add_edge("a", "b", weight=0.6)
+        G.add_edge("a", "c", weight=0.2)
+        G.add_edge("c", "d", weight=0.1)
+        G.add_edge("c", "e", weight=0.7)
+        G.add_edge("c", "f", weight=0.9)
+        G.add_edge("a", "d", weight=0.3)
+        paths = nx.generate_random_paths(
+            G, num_paths, path_length=path_length, index_map=index_map
+        )
+
+        expected_paths = [
+            ["d", "c", "f", "c", "d", "a", "b"],
+            ["e", "c", "f", "c", "f", "c", "e"],
+            ["d", "a", "b", "a", "b", "a", "c"],
+            ["b", "a", "d", "a", "b", "a", "b"],
+            ["d", "a", "b", "a", "b", "a", "d"],
+            ["d", "a", "b", "a", "b", "a", "c"],
+            ["d", "a", "b", "a", "b", "a", "b"],
+            ["f", "c", "f", "c", "f", "c", "e"],
+            ["d", "a", "d", "a", "b", "a", "b"],
+            ["e", "c", "f", "c", "e", "c", "d"],
+        ]
+        expected_map = {
+            "d": {0, 2, 3, 4, 5, 6, 8, 9},
+            "c": {0, 1, 2, 5, 7, 9},
+            "f": {0, 1, 9, 7},
+            "a": {0, 2, 3, 4, 5, 6, 8},
+            "b": {0, 2, 3, 4, 5, 6, 8},
+            "e": {1, 9, 7},
+        }
+
+        assert expected_paths == list(paths)
+        assert expected_map == index_map
+
+    def test_symmetry_with_custom_matching(self):
+        print("G2 is edge (a,b) and G3 is edge (a,a)")
+        print("but node order for G2 is (a,b) while for G3 it is (b,a)")
+
+        a, b = "A", "B"
+        G2 = nx.Graph()
+        G2.add_nodes_from((a, b))
+        G2.add_edges_from([(a, b)])
+        G3 = nx.Graph()
+        G3.add_nodes_from((b, a))
+        G3.add_edges_from([(a, a)])
+        for G in (G2, G3):
+            for n in G:
+                G.nodes[n]["attr"] = n
+            for e in G.edges:
+                G.edges[e]["attr"] = e
+        match = lambda x, y: x == y
+
+        print("Starting G2 to G3 GED calculation")
+        assert nx.graph_edit_distance(G2, G3, node_match=match, edge_match=match) == 1
+
+        print("Starting G3 to G2 GED calculation")
+        assert nx.graph_edit_distance(G3, G2, node_match=match, edge_match=match) == 1
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_simple_paths.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_simple_paths.py
new file mode 100644
index 00000000..7855bbad
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_simple_paths.py
@@ -0,0 +1,803 @@
+import random
+
+import pytest
+
+import networkx as nx
+from networkx import convert_node_labels_to_integers as cnlti
+from networkx.algorithms.simple_paths import (
+    _bidirectional_dijkstra,
+    _bidirectional_shortest_path,
+)
+from networkx.utils import arbitrary_element, pairwise
+
+
+class TestIsSimplePath:
+    """Unit tests for the
+    :func:`networkx.algorithms.simple_paths.is_simple_path` function.
+
+    """
+
+    def test_empty_list(self):
+        """Tests that the empty list is not a valid path, since there
+        should be a one-to-one correspondence between paths as lists of
+        nodes and paths as lists of edges.
+
+        """
+        G = nx.trivial_graph()
+        assert not nx.is_simple_path(G, [])
+
+    def test_trivial_path(self):
+        """Tests that the trivial path, a path of length one, is
+        considered a simple path in a graph.
+
+        """
+        G = nx.trivial_graph()
+        assert nx.is_simple_path(G, [0])
+
+    def test_trivial_nonpath(self):
+        """Tests that a list whose sole element is an object not in the
+        graph is not considered a simple path.
+
+        """
+        G = nx.trivial_graph()
+        assert not nx.is_simple_path(G, ["not a node"])
+
+    def test_simple_path(self):
+        G = nx.path_graph(2)
+        assert nx.is_simple_path(G, [0, 1])
+
+    def test_non_simple_path(self):
+        G = nx.path_graph(2)
+        assert not nx.is_simple_path(G, [0, 1, 0])
+
+    def test_cycle(self):
+        G = nx.cycle_graph(3)
+        assert not nx.is_simple_path(G, [0, 1, 2, 0])
+
+    def test_missing_node(self):
+        G = nx.path_graph(2)
+        assert not nx.is_simple_path(G, [0, 2])
+
+    def test_missing_starting_node(self):
+        G = nx.path_graph(2)
+        assert not nx.is_simple_path(G, [2, 0])
+
+    def test_directed_path(self):
+        G = nx.DiGraph([(0, 1), (1, 2)])
+        assert nx.is_simple_path(G, [0, 1, 2])
+
+    def test_directed_non_path(self):
+        G = nx.DiGraph([(0, 1), (1, 2)])
+        assert not nx.is_simple_path(G, [2, 1, 0])
+
+    def test_directed_cycle(self):
+        G = nx.DiGraph([(0, 1), (1, 2), (2, 0)])
+        assert not nx.is_simple_path(G, [0, 1, 2, 0])
+
+    def test_multigraph(self):
+        G = nx.MultiGraph([(0, 1), (0, 1)])
+        assert nx.is_simple_path(G, [0, 1])
+
+    def test_multidigraph(self):
+        G = nx.MultiDiGraph([(0, 1), (0, 1), (1, 0), (1, 0)])
+        assert nx.is_simple_path(G, [0, 1])
+
+
+# Tests for all_simple_paths
+def test_all_simple_paths():
+    G = nx.path_graph(4)
+    paths = nx.all_simple_paths(G, 0, 3)
+    assert {tuple(p) for p in paths} == {(0, 1, 2, 3)}
+
+
+def test_all_simple_paths_with_two_targets_emits_two_paths():
+    G = nx.path_graph(4)
+    G.add_edge(2, 4)
+    paths = nx.all_simple_paths(G, 0, [3, 4])
+    assert {tuple(p) for p in paths} == {(0, 1, 2, 3), (0, 1, 2, 4)}
+
+
+def test_digraph_all_simple_paths_with_two_targets_emits_two_paths():
+    G = nx.path_graph(4, create_using=nx.DiGraph())
+    G.add_edge(2, 4)
+    paths = nx.all_simple_paths(G, 0, [3, 4])
+    assert {tuple(p) for p in paths} == {(0, 1, 2, 3), (0, 1, 2, 4)}
+
+
+def test_all_simple_paths_with_two_targets_cutoff():
+    G = nx.path_graph(4)
+    G.add_edge(2, 4)
+    paths = nx.all_simple_paths(G, 0, [3, 4], cutoff=3)
+    assert {tuple(p) for p in paths} == {(0, 1, 2, 3), (0, 1, 2, 4)}
+
+
+def test_digraph_all_simple_paths_with_two_targets_cutoff():
+    G = nx.path_graph(4, create_using=nx.DiGraph())
+    G.add_edge(2, 4)
+    paths = nx.all_simple_paths(G, 0, [3, 4], cutoff=3)
+    assert {tuple(p) for p in paths} == {(0, 1, 2, 3), (0, 1, 2, 4)}
+
+
+def test_all_simple_paths_with_two_targets_in_line_emits_two_paths():
+    G = nx.path_graph(4)
+    paths = nx.all_simple_paths(G, 0, [2, 3])
+    assert {tuple(p) for p in paths} == {(0, 1, 2), (0, 1, 2, 3)}
+
+
+def test_all_simple_paths_ignores_cycle():
+    G = nx.cycle_graph(3, create_using=nx.DiGraph())
+    G.add_edge(1, 3)
+    paths = nx.all_simple_paths(G, 0, 3)
+    assert {tuple(p) for p in paths} == {(0, 1, 3)}
+
+
+def test_all_simple_paths_with_two_targets_inside_cycle_emits_two_paths():
+    G = nx.cycle_graph(3, create_using=nx.DiGraph())
+    G.add_edge(1, 3)
+    paths = nx.all_simple_paths(G, 0, [2, 3])
+    assert {tuple(p) for p in paths} == {(0, 1, 2), (0, 1, 3)}
+
+
+def test_all_simple_paths_source_target():
+    G = nx.path_graph(4)
+    assert list(nx.all_simple_paths(G, 1, 1)) == [[1]]
+
+
+def test_all_simple_paths_cutoff():
+    G = nx.complete_graph(4)
+    paths = nx.all_simple_paths(G, 0, 1, cutoff=1)
+    assert {tuple(p) for p in paths} == {(0, 1)}
+    paths = nx.all_simple_paths(G, 0, 1, cutoff=2)
+    assert {tuple(p) for p in paths} == {(0, 1), (0, 2, 1), (0, 3, 1)}
+
+
+def test_all_simple_paths_on_non_trivial_graph():
+    """you may need to draw this graph to make sure it is reasonable"""
+    G = nx.path_graph(5, create_using=nx.DiGraph())
+    G.add_edges_from([(0, 5), (1, 5), (1, 3), (5, 4), (4, 2), (4, 3)])
+    paths = nx.all_simple_paths(G, 1, [2, 3])
+    assert {tuple(p) for p in paths} == {
+        (1, 2),
+        (1, 3, 4, 2),
+        (1, 5, 4, 2),
+        (1, 3),
+        (1, 2, 3),
+        (1, 5, 4, 3),
+        (1, 5, 4, 2, 3),
+    }
+    paths = nx.all_simple_paths(G, 1, [2, 3], cutoff=3)
+    assert {tuple(p) for p in paths} == {
+        (1, 2),
+        (1, 3, 4, 2),
+        (1, 5, 4, 2),
+        (1, 3),
+        (1, 2, 3),
+        (1, 5, 4, 3),
+    }
+    paths = nx.all_simple_paths(G, 1, [2, 3], cutoff=2)
+    assert {tuple(p) for p in paths} == {(1, 2), (1, 3), (1, 2, 3)}
+
+
+def test_all_simple_paths_multigraph():
+    G = nx.MultiGraph([(1, 2), (1, 2)])
+    assert list(nx.all_simple_paths(G, 1, 1)) == [[1]]
+    nx.add_path(G, [3, 1, 10, 2])
+    paths = list(nx.all_simple_paths(G, 1, 2))
+    assert len(paths) == 3
+    assert {tuple(p) for p in paths} == {(1, 2), (1, 2), (1, 10, 2)}
+
+
+def test_all_simple_paths_multigraph_with_cutoff():
+    G = nx.MultiGraph([(1, 2), (1, 2), (1, 10), (10, 2)])
+    paths = list(nx.all_simple_paths(G, 1, 2, cutoff=1))
+    assert len(paths) == 2
+    assert {tuple(p) for p in paths} == {(1, 2), (1, 2)}
+
+    # See GitHub issue #6732.
+    G = nx.MultiGraph([(0, 1), (0, 2)])
+    assert list(nx.all_simple_paths(G, 0, {1, 2}, cutoff=1)) == [[0, 1], [0, 2]]
+
+
+def test_all_simple_paths_directed():
+    G = nx.DiGraph()
+    nx.add_path(G, [1, 2, 3])
+    nx.add_path(G, [3, 2, 1])
+    paths = nx.all_simple_paths(G, 1, 3)
+    assert {tuple(p) for p in paths} == {(1, 2, 3)}
+
+
+def test_all_simple_paths_empty():
+    G = nx.path_graph(4)
+    paths = nx.all_simple_paths(G, 0, 3, cutoff=2)
+    assert list(paths) == []
+
+
+def test_all_simple_paths_corner_cases():
+    assert list(nx.all_simple_paths(nx.empty_graph(2), 0, 0)) == [[0]]
+    assert list(nx.all_simple_paths(nx.empty_graph(2), 0, 1)) == []
+    assert list(nx.all_simple_paths(nx.path_graph(9), 0, 8, 0)) == []
+
+
+def test_all_simple_paths_source_in_targets():
+    # See GitHub issue #6690.
+    G = nx.path_graph(3)
+    assert list(nx.all_simple_paths(G, 0, {0, 1, 2})) == [[0], [0, 1], [0, 1, 2]]
+
+
+def hamiltonian_path(G, source):
+    source = arbitrary_element(G)
+    neighbors = set(G[source]) - {source}
+    n = len(G)
+    for target in neighbors:
+        for path in nx.all_simple_paths(G, source, target):
+            if len(path) == n:
+                yield path
+
+
+def test_hamiltonian_path():
+    from itertools import permutations
+
+    G = nx.complete_graph(4)
+    paths = [list(p) for p in hamiltonian_path(G, 0)]
+    exact = [[0] + list(p) for p in permutations([1, 2, 3], 3)]
+    assert sorted(paths) == sorted(exact)
+
+
+def test_cutoff_zero():
+    G = nx.complete_graph(4)
+    paths = nx.all_simple_paths(G, 0, 3, cutoff=0)
+    assert [list(p) for p in paths] == []
+    paths = nx.all_simple_paths(nx.MultiGraph(G), 0, 3, cutoff=0)
+    assert [list(p) for p in paths] == []
+
+
+def test_source_missing():
+    with pytest.raises(nx.NodeNotFound):
+        G = nx.Graph()
+        nx.add_path(G, [1, 2, 3])
+        list(nx.all_simple_paths(nx.MultiGraph(G), 0, 3))
+
+
+def test_target_missing():
+    with pytest.raises(nx.NodeNotFound):
+        G = nx.Graph()
+        nx.add_path(G, [1, 2, 3])
+        list(nx.all_simple_paths(nx.MultiGraph(G), 1, 4))
+
+
+# Tests for all_simple_edge_paths
+def test_all_simple_edge_paths():
+    G = nx.path_graph(4)
+    paths = nx.all_simple_edge_paths(G, 0, 3)
+    assert {tuple(p) for p in paths} == {((0, 1), (1, 2), (2, 3))}
+
+
+def test_all_simple_edge_paths_empty_path():
+    G = nx.empty_graph(1)
+    assert list(nx.all_simple_edge_paths(G, 0, 0)) == [[]]
+
+
+def test_all_simple_edge_paths_with_two_targets_emits_two_paths():
+    G = nx.path_graph(4)
+    G.add_edge(2, 4)
+    paths = nx.all_simple_edge_paths(G, 0, [3, 4])
+    assert {tuple(p) for p in paths} == {
+        ((0, 1), (1, 2), (2, 3)),
+        ((0, 1), (1, 2), (2, 4)),
+    }
+
+
+def test_digraph_all_simple_edge_paths_with_two_targets_emits_two_paths():
+    G = nx.path_graph(4, create_using=nx.DiGraph())
+    G.add_edge(2, 4)
+    paths = nx.all_simple_edge_paths(G, 0, [3, 4])
+    assert {tuple(p) for p in paths} == {
+        ((0, 1), (1, 2), (2, 3)),
+        ((0, 1), (1, 2), (2, 4)),
+    }
+
+
+def test_all_simple_edge_paths_with_two_targets_cutoff():
+    G = nx.path_graph(4)
+    G.add_edge(2, 4)
+    paths = nx.all_simple_edge_paths(G, 0, [3, 4], cutoff=3)
+    assert {tuple(p) for p in paths} == {
+        ((0, 1), (1, 2), (2, 3)),
+        ((0, 1), (1, 2), (2, 4)),
+    }
+
+
+def test_digraph_all_simple_edge_paths_with_two_targets_cutoff():
+    G = nx.path_graph(4, create_using=nx.DiGraph())
+    G.add_edge(2, 4)
+    paths = nx.all_simple_edge_paths(G, 0, [3, 4], cutoff=3)
+    assert {tuple(p) for p in paths} == {
+        ((0, 1), (1, 2), (2, 3)),
+        ((0, 1), (1, 2), (2, 4)),
+    }
+
+
+def test_all_simple_edge_paths_with_two_targets_in_line_emits_two_paths():
+    G = nx.path_graph(4)
+    paths = nx.all_simple_edge_paths(G, 0, [2, 3])
+    assert {tuple(p) for p in paths} == {((0, 1), (1, 2)), ((0, 1), (1, 2), (2, 3))}
+
+
+def test_all_simple_edge_paths_ignores_cycle():
+    G = nx.cycle_graph(3, create_using=nx.DiGraph())
+    G.add_edge(1, 3)
+    paths = nx.all_simple_edge_paths(G, 0, 3)
+    assert {tuple(p) for p in paths} == {((0, 1), (1, 3))}
+
+
+def test_all_simple_edge_paths_with_two_targets_inside_cycle_emits_two_paths():
+    G = nx.cycle_graph(3, create_using=nx.DiGraph())
+    G.add_edge(1, 3)
+    paths = nx.all_simple_edge_paths(G, 0, [2, 3])
+    assert {tuple(p) for p in paths} == {((0, 1), (1, 2)), ((0, 1), (1, 3))}
+
+
+def test_all_simple_edge_paths_source_target():
+    G = nx.path_graph(4)
+    paths = nx.all_simple_edge_paths(G, 1, 1)
+    assert list(paths) == [[]]
+
+
+def test_all_simple_edge_paths_cutoff():
+    G = nx.complete_graph(4)
+    paths = nx.all_simple_edge_paths(G, 0, 1, cutoff=1)
+    assert {tuple(p) for p in paths} == {((0, 1),)}
+    paths = nx.all_simple_edge_paths(G, 0, 1, cutoff=2)
+    assert {tuple(p) for p in paths} == {((0, 1),), ((0, 2), (2, 1)), ((0, 3), (3, 1))}
+
+
+def test_all_simple_edge_paths_on_non_trivial_graph():
+    """you may need to draw this graph to make sure it is reasonable"""
+    G = nx.path_graph(5, create_using=nx.DiGraph())
+    G.add_edges_from([(0, 5), (1, 5), (1, 3), (5, 4), (4, 2), (4, 3)])
+    paths = nx.all_simple_edge_paths(G, 1, [2, 3])
+    assert {tuple(p) for p in paths} == {
+        ((1, 2),),
+        ((1, 3), (3, 4), (4, 2)),
+        ((1, 5), (5, 4), (4, 2)),
+        ((1, 3),),
+        ((1, 2), (2, 3)),
+        ((1, 5), (5, 4), (4, 3)),
+        ((1, 5), (5, 4), (4, 2), (2, 3)),
+    }
+    paths = nx.all_simple_edge_paths(G, 1, [2, 3], cutoff=3)
+    assert {tuple(p) for p in paths} == {
+        ((1, 2),),
+        ((1, 3), (3, 4), (4, 2)),
+        ((1, 5), (5, 4), (4, 2)),
+        ((1, 3),),
+        ((1, 2), (2, 3)),
+        ((1, 5), (5, 4), (4, 3)),
+    }
+    paths = nx.all_simple_edge_paths(G, 1, [2, 3], cutoff=2)
+    assert {tuple(p) for p in paths} == {((1, 2),), ((1, 3),), ((1, 2), (2, 3))}
+
+
+def test_all_simple_edge_paths_multigraph():
+    G = nx.MultiGraph([(1, 2), (1, 2)])
+    paths = nx.all_simple_edge_paths(G, 1, 1)
+    assert list(paths) == [[]]
+    nx.add_path(G, [3, 1, 10, 2])
+    paths = list(nx.all_simple_edge_paths(G, 1, 2))
+    assert len(paths) == 3
+    assert {tuple(p) for p in paths} == {
+        ((1, 2, 0),),
+        ((1, 2, 1),),
+        ((1, 10, 0), (10, 2, 0)),
+    }
+
+
+def test_all_simple_edge_paths_multigraph_with_cutoff():
+    G = nx.MultiGraph([(1, 2), (1, 2), (1, 10), (10, 2)])
+    paths = list(nx.all_simple_edge_paths(G, 1, 2, cutoff=1))
+    assert len(paths) == 2
+    assert {tuple(p) for p in paths} == {((1, 2, 0),), ((1, 2, 1),)}
+
+
+def test_all_simple_edge_paths_directed():
+    G = nx.DiGraph()
+    nx.add_path(G, [1, 2, 3])
+    nx.add_path(G, [3, 2, 1])
+    paths = nx.all_simple_edge_paths(G, 1, 3)
+    assert {tuple(p) for p in paths} == {((1, 2), (2, 3))}
+
+
+def test_all_simple_edge_paths_empty():
+    G = nx.path_graph(4)
+    paths = nx.all_simple_edge_paths(G, 0, 3, cutoff=2)
+    assert list(paths) == []
+
+
+def test_all_simple_edge_paths_corner_cases():
+    assert list(nx.all_simple_edge_paths(nx.empty_graph(2), 0, 0)) == [[]]
+    assert list(nx.all_simple_edge_paths(nx.empty_graph(2), 0, 1)) == []
+    assert list(nx.all_simple_edge_paths(nx.path_graph(9), 0, 8, 0)) == []
+
+
+def test_all_simple_edge_paths_ignores_self_loop():
+    G = nx.Graph([(0, 0), (0, 1), (1, 1), (1, 2)])
+    assert list(nx.all_simple_edge_paths(G, 0, 2)) == [[(0, 1), (1, 2)]]
+
+
+def hamiltonian_edge_path(G, source):
+    source = arbitrary_element(G)
+    neighbors = set(G[source]) - {source}
+    n = len(G)
+    for target in neighbors:
+        for path in nx.all_simple_edge_paths(G, source, target):
+            if len(path) == n - 1:
+                yield path
+
+
+def test_hamiltonian__edge_path():
+    from itertools import permutations
+
+    G = nx.complete_graph(4)
+    paths = hamiltonian_edge_path(G, 0)
+    exact = [list(pairwise([0] + list(p))) for p in permutations([1, 2, 3], 3)]
+    assert sorted(exact) == sorted(paths)
+
+
+def test_edge_cutoff_zero():
+    G = nx.complete_graph(4)
+    paths = nx.all_simple_edge_paths(G, 0, 3, cutoff=0)
+    assert [list(p) for p in paths] == []
+    paths = nx.all_simple_edge_paths(nx.MultiGraph(G), 0, 3, cutoff=0)
+    assert [list(p) for p in paths] == []
+
+
+def test_edge_source_missing():
+    with pytest.raises(nx.NodeNotFound):
+        G = nx.Graph()
+        nx.add_path(G, [1, 2, 3])
+        list(nx.all_simple_edge_paths(nx.MultiGraph(G), 0, 3))
+
+
+def test_edge_target_missing():
+    with pytest.raises(nx.NodeNotFound):
+        G = nx.Graph()
+        nx.add_path(G, [1, 2, 3])
+        list(nx.all_simple_edge_paths(nx.MultiGraph(G), 1, 4))
+
+
+# Tests for shortest_simple_paths
+def test_shortest_simple_paths():
+    G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted")
+    paths = nx.shortest_simple_paths(G, 1, 12)
+    assert next(paths) == [1, 2, 3, 4, 8, 12]
+    assert next(paths) == [1, 5, 6, 7, 8, 12]
+    assert [len(path) for path in nx.shortest_simple_paths(G, 1, 12)] == sorted(
+        len(path) for path in nx.all_simple_paths(G, 1, 12)
+    )
+
+
+def test_shortest_simple_paths_singleton_path():
+    G = nx.empty_graph(3)
+    assert list(nx.shortest_simple_paths(G, 0, 0)) == [[0]]
+
+
+def test_shortest_simple_paths_directed():
+    G = nx.cycle_graph(7, create_using=nx.DiGraph())
+    paths = nx.shortest_simple_paths(G, 0, 3)
+    assert list(paths) == [[0, 1, 2, 3]]
+
+
+def test_shortest_simple_paths_directed_with_weight_function():
+    def cost(u, v, x):
+        return 1
+
+    G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted")
+    paths = nx.shortest_simple_paths(G, 1, 12)
+    assert next(paths) == [1, 2, 3, 4, 8, 12]
+    assert next(paths) == [1, 5, 6, 7, 8, 12]
+    assert [
+        len(path) for path in nx.shortest_simple_paths(G, 1, 12, weight=cost)
+    ] == sorted(len(path) for path in nx.all_simple_paths(G, 1, 12))
+
+
+def test_shortest_simple_paths_with_weight_function():
+    def cost(u, v, x):
+        return 1
+
+    G = nx.cycle_graph(7, create_using=nx.DiGraph())
+    paths = nx.shortest_simple_paths(G, 0, 3, weight=cost)
+    assert list(paths) == [[0, 1, 2, 3]]
+
+
+def test_shortest_simple_paths_with_none_weight_function():
+    def cost(u, v, x):
+        delta = abs(u - v)
+        # ignore interior edges
+        return 1 if (delta == 1 or delta == 4) else None
+
+    G = nx.complete_graph(5)
+    paths = nx.shortest_simple_paths(G, 0, 2, weight=cost)
+    assert list(paths) == [[0, 1, 2], [0, 4, 3, 2]]
+
+
+def test_Greg_Bernstein():
+    g1 = nx.Graph()
+    g1.add_nodes_from(["N0", "N1", "N2", "N3", "N4"])
+    g1.add_edge("N4", "N1", weight=10.0, capacity=50, name="L5")
+    g1.add_edge("N4", "N0", weight=7.0, capacity=40, name="L4")
+    g1.add_edge("N0", "N1", weight=10.0, capacity=45, name="L1")
+    g1.add_edge("N3", "N0", weight=10.0, capacity=50, name="L0")
+    g1.add_edge("N2", "N3", weight=12.0, capacity=30, name="L2")
+    g1.add_edge("N1", "N2", weight=15.0, capacity=42, name="L3")
+    solution = [["N1", "N0", "N3"], ["N1", "N2", "N3"], ["N1", "N4", "N0", "N3"]]
+    result = list(nx.shortest_simple_paths(g1, "N1", "N3", weight="weight"))
+    assert result == solution
+
+
+def test_weighted_shortest_simple_path():
+    def cost_func(path):
+        return sum(G.adj[u][v]["weight"] for (u, v) in zip(path, path[1:]))
+
+    G = nx.complete_graph(5)
+    weight = {(u, v): random.randint(1, 100) for (u, v) in G.edges()}
+    nx.set_edge_attributes(G, weight, "weight")
+    cost = 0
+    for path in nx.shortest_simple_paths(G, 0, 3, weight="weight"):
+        this_cost = cost_func(path)
+        assert cost <= this_cost
+        cost = this_cost
+
+
+def test_directed_weighted_shortest_simple_path():
+    def cost_func(path):
+        return sum(G.adj[u][v]["weight"] for (u, v) in zip(path, path[1:]))
+
+    G = nx.complete_graph(5)
+    G = G.to_directed()
+    weight = {(u, v): random.randint(1, 100) for (u, v) in G.edges()}
+    nx.set_edge_attributes(G, weight, "weight")
+    cost = 0
+    for path in nx.shortest_simple_paths(G, 0, 3, weight="weight"):
+        this_cost = cost_func(path)
+        assert cost <= this_cost
+        cost = this_cost
+
+
+def test_weighted_shortest_simple_path_issue2427():
+    G = nx.Graph()
+    G.add_edge("IN", "OUT", weight=2)
+    G.add_edge("IN", "A", weight=1)
+    G.add_edge("IN", "B", weight=2)
+    G.add_edge("B", "OUT", weight=2)
+    assert list(nx.shortest_simple_paths(G, "IN", "OUT", weight="weight")) == [
+        ["IN", "OUT"],
+        ["IN", "B", "OUT"],
+    ]
+    G = nx.Graph()
+    G.add_edge("IN", "OUT", weight=10)
+    G.add_edge("IN", "A", weight=1)
+    G.add_edge("IN", "B", weight=1)
+    G.add_edge("B", "OUT", weight=1)
+    assert list(nx.shortest_simple_paths(G, "IN", "OUT", weight="weight")) == [
+        ["IN", "B", "OUT"],
+        ["IN", "OUT"],
+    ]
+
+
+def test_directed_weighted_shortest_simple_path_issue2427():
+    G = nx.DiGraph()
+    G.add_edge("IN", "OUT", weight=2)
+    G.add_edge("IN", "A", weight=1)
+    G.add_edge("IN", "B", weight=2)
+    G.add_edge("B", "OUT", weight=2)
+    assert list(nx.shortest_simple_paths(G, "IN", "OUT", weight="weight")) == [
+        ["IN", "OUT"],
+        ["IN", "B", "OUT"],
+    ]
+    G = nx.DiGraph()
+    G.add_edge("IN", "OUT", weight=10)
+    G.add_edge("IN", "A", weight=1)
+    G.add_edge("IN", "B", weight=1)
+    G.add_edge("B", "OUT", weight=1)
+    assert list(nx.shortest_simple_paths(G, "IN", "OUT", weight="weight")) == [
+        ["IN", "B", "OUT"],
+        ["IN", "OUT"],
+    ]
+
+
+def test_weight_name():
+    G = nx.cycle_graph(7)
+    nx.set_edge_attributes(G, 1, "weight")
+    nx.set_edge_attributes(G, 1, "foo")
+    G.adj[1][2]["foo"] = 7
+    paths = list(nx.shortest_simple_paths(G, 0, 3, weight="foo"))
+    solution = [[0, 6, 5, 4, 3], [0, 1, 2, 3]]
+    assert paths == solution
+
+
+def test_ssp_source_missing():
+    with pytest.raises(nx.NodeNotFound):
+        G = nx.Graph()
+        nx.add_path(G, [1, 2, 3])
+        list(nx.shortest_simple_paths(G, 0, 3))
+
+
+def test_ssp_target_missing():
+    with pytest.raises(nx.NodeNotFound):
+        G = nx.Graph()
+        nx.add_path(G, [1, 2, 3])
+        list(nx.shortest_simple_paths(G, 1, 4))
+
+
+def test_ssp_multigraph():
+    with pytest.raises(nx.NetworkXNotImplemented):
+        G = nx.MultiGraph()
+        nx.add_path(G, [1, 2, 3])
+        list(nx.shortest_simple_paths(G, 1, 4))
+
+
+def test_ssp_source_missing2():
+    with pytest.raises(nx.NetworkXNoPath):
+        G = nx.Graph()
+        nx.add_path(G, [0, 1, 2])
+        nx.add_path(G, [3, 4, 5])
+        list(nx.shortest_simple_paths(G, 0, 3))
+
+
+def test_bidirectional_shortest_path_restricted_cycle():
+    cycle = nx.cycle_graph(7)
+    length, path = _bidirectional_shortest_path(cycle, 0, 3)
+    assert path == [0, 1, 2, 3]
+    length, path = _bidirectional_shortest_path(cycle, 0, 3, ignore_nodes=[1])
+    assert path == [0, 6, 5, 4, 3]
+
+
+def test_bidirectional_shortest_path_restricted_wheel():
+    wheel = nx.wheel_graph(6)
+    length, path = _bidirectional_shortest_path(wheel, 1, 3)
+    assert path in [[1, 0, 3], [1, 2, 3]]
+    length, path = _bidirectional_shortest_path(wheel, 1, 3, ignore_nodes=[0])
+    assert path == [1, 2, 3]
+    length, path = _bidirectional_shortest_path(wheel, 1, 3, ignore_nodes=[0, 2])
+    assert path == [1, 5, 4, 3]
+    length, path = _bidirectional_shortest_path(
+        wheel, 1, 3, ignore_edges=[(1, 0), (5, 0), (2, 3)]
+    )
+    assert path in [[1, 2, 0, 3], [1, 5, 4, 3]]
+
+
+def test_bidirectional_shortest_path_restricted_directed_cycle():
+    directed_cycle = nx.cycle_graph(7, create_using=nx.DiGraph())
+    length, path = _bidirectional_shortest_path(directed_cycle, 0, 3)
+    assert path == [0, 1, 2, 3]
+    pytest.raises(
+        nx.NetworkXNoPath,
+        _bidirectional_shortest_path,
+        directed_cycle,
+        0,
+        3,
+        ignore_nodes=[1],
+    )
+    length, path = _bidirectional_shortest_path(
+        directed_cycle, 0, 3, ignore_edges=[(2, 1)]
+    )
+    assert path == [0, 1, 2, 3]
+    pytest.raises(
+        nx.NetworkXNoPath,
+        _bidirectional_shortest_path,
+        directed_cycle,
+        0,
+        3,
+        ignore_edges=[(1, 2)],
+    )
+
+
+def test_bidirectional_shortest_path_ignore():
+    G = nx.Graph()
+    nx.add_path(G, [1, 2])
+    nx.add_path(G, [1, 3])
+    nx.add_path(G, [1, 4])
+    pytest.raises(
+        nx.NetworkXNoPath, _bidirectional_shortest_path, G, 1, 2, ignore_nodes=[1]
+    )
+    pytest.raises(
+        nx.NetworkXNoPath, _bidirectional_shortest_path, G, 1, 2, ignore_nodes=[2]
+    )
+    G = nx.Graph()
+    nx.add_path(G, [1, 3])
+    nx.add_path(G, [1, 4])
+    nx.add_path(G, [3, 2])
+    pytest.raises(
+        nx.NetworkXNoPath, _bidirectional_shortest_path, G, 1, 2, ignore_nodes=[1, 2]
+    )
+
+
+def validate_path(G, s, t, soln_len, path):
+    assert path[0] == s
+    assert path[-1] == t
+    assert soln_len == sum(
+        G[u][v].get("weight", 1) for u, v in zip(path[:-1], path[1:])
+    )
+
+
+def validate_length_path(G, s, t, soln_len, length, path):
+    assert soln_len == length
+    validate_path(G, s, t, length, path)
+
+
+def test_bidirectional_dijkstra_restricted():
+    XG = nx.DiGraph()
+    XG.add_weighted_edges_from(
+        [
+            ("s", "u", 10),
+            ("s", "x", 5),
+            ("u", "v", 1),
+            ("u", "x", 2),
+            ("v", "y", 1),
+            ("x", "u", 3),
+            ("x", "v", 5),
+            ("x", "y", 2),
+            ("y", "s", 7),
+            ("y", "v", 6),
+        ]
+    )
+
+    XG3 = nx.Graph()
+    XG3.add_weighted_edges_from(
+        [[0, 1, 2], [1, 2, 12], [2, 3, 1], [3, 4, 5], [4, 5, 1], [5, 0, 10]]
+    )
+    validate_length_path(XG, "s", "v", 9, *_bidirectional_dijkstra(XG, "s", "v"))
+    validate_length_path(
+        XG, "s", "v", 10, *_bidirectional_dijkstra(XG, "s", "v", ignore_nodes=["u"])
+    )
+    validate_length_path(
+        XG,
+        "s",
+        "v",
+        11,
+        *_bidirectional_dijkstra(XG, "s", "v", ignore_edges=[("s", "x")]),
+    )
+    pytest.raises(
+        nx.NetworkXNoPath,
+        _bidirectional_dijkstra,
+        XG,
+        "s",
+        "v",
+        ignore_nodes=["u"],
+        ignore_edges=[("s", "x")],
+    )
+    validate_length_path(XG3, 0, 3, 15, *_bidirectional_dijkstra(XG3, 0, 3))
+    validate_length_path(
+        XG3, 0, 3, 16, *_bidirectional_dijkstra(XG3, 0, 3, ignore_nodes=[1])
+    )
+    validate_length_path(
+        XG3, 0, 3, 16, *_bidirectional_dijkstra(XG3, 0, 3, ignore_edges=[(2, 3)])
+    )
+    pytest.raises(
+        nx.NetworkXNoPath,
+        _bidirectional_dijkstra,
+        XG3,
+        0,
+        3,
+        ignore_nodes=[1],
+        ignore_edges=[(5, 4)],
+    )
+
+
+def test_bidirectional_dijkstra_no_path():
+    with pytest.raises(nx.NetworkXNoPath):
+        G = nx.Graph()
+        nx.add_path(G, [1, 2, 3])
+        nx.add_path(G, [4, 5, 6])
+        _bidirectional_dijkstra(G, 1, 6)
+
+
+def test_bidirectional_dijkstra_ignore():
+    G = nx.Graph()
+    nx.add_path(G, [1, 2, 10])
+    nx.add_path(G, [1, 3, 10])
+    pytest.raises(nx.NetworkXNoPath, _bidirectional_dijkstra, G, 1, 2, ignore_nodes=[1])
+    pytest.raises(nx.NetworkXNoPath, _bidirectional_dijkstra, G, 1, 2, ignore_nodes=[2])
+    pytest.raises(
+        nx.NetworkXNoPath, _bidirectional_dijkstra, G, 1, 2, ignore_nodes=[1, 2]
+    )
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_smallworld.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_smallworld.py
new file mode 100644
index 00000000..d115dd99
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_smallworld.py
@@ -0,0 +1,78 @@
+import pytest
+
+pytest.importorskip("numpy")
+
+import random
+
+import networkx as nx
+from networkx import lattice_reference, omega, random_reference, sigma
+
+rng = 42
+
+
+def test_random_reference():
+    G = nx.connected_watts_strogatz_graph(50, 6, 0.1, seed=rng)
+    Gr = random_reference(G, niter=1, seed=rng)
+    C = nx.average_clustering(G)
+    Cr = nx.average_clustering(Gr)
+    assert C > Cr
+
+    with pytest.raises(nx.NetworkXError):
+        next(random_reference(nx.Graph()))
+    with pytest.raises(nx.NetworkXNotImplemented):
+        next(random_reference(nx.DiGraph()))
+
+    H = nx.Graph(((0, 1), (2, 3)))
+    Hl = random_reference(H, niter=1, seed=rng)
+
+
+def test_lattice_reference():
+    G = nx.connected_watts_strogatz_graph(50, 6, 1, seed=rng)
+    Gl = lattice_reference(G, niter=1, seed=rng)
+    L = nx.average_shortest_path_length(G)
+    Ll = nx.average_shortest_path_length(Gl)
+    assert Ll > L
+
+    pytest.raises(nx.NetworkXError, lattice_reference, nx.Graph())
+    pytest.raises(nx.NetworkXNotImplemented, lattice_reference, nx.DiGraph())
+
+    H = nx.Graph(((0, 1), (2, 3)))
+    Hl = lattice_reference(H, niter=1)
+
+
+def test_sigma():
+    Gs = nx.connected_watts_strogatz_graph(50, 6, 0.1, seed=rng)
+    Gr = nx.connected_watts_strogatz_graph(50, 6, 1, seed=rng)
+    sigmas = sigma(Gs, niter=1, nrand=2, seed=rng)
+    sigmar = sigma(Gr, niter=1, nrand=2, seed=rng)
+    assert sigmar < sigmas
+
+
+def test_omega():
+    Gl = nx.connected_watts_strogatz_graph(50, 6, 0, seed=rng)
+    Gr = nx.connected_watts_strogatz_graph(50, 6, 1, seed=rng)
+    Gs = nx.connected_watts_strogatz_graph(50, 6, 0.1, seed=rng)
+    omegal = omega(Gl, niter=1, nrand=1, seed=rng)
+    omegar = omega(Gr, niter=1, nrand=1, seed=rng)
+    omegas = omega(Gs, niter=1, nrand=1, seed=rng)
+    assert omegal < omegas and omegas < omegar
+
+    # Test that omega lies within the [-1, 1] bounds
+    G_barbell = nx.barbell_graph(5, 1)
+    G_karate = nx.karate_club_graph()
+
+    omega_barbell = nx.omega(G_barbell)
+    omega_karate = nx.omega(G_karate, nrand=2)
+
+    omegas = (omegal, omegar, omegas, omega_barbell, omega_karate)
+
+    for o in omegas:
+        assert -1 <= o <= 1
+
+
+@pytest.mark.parametrize("f", (nx.random_reference, nx.lattice_reference))
+def test_graph_no_edges(f):
+    G = nx.Graph()
+    G.add_nodes_from([0, 1, 2, 3])
+    with pytest.raises(nx.NetworkXError, match="Graph has fewer that 2 edges"):
+        f(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_smetric.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_smetric.py
new file mode 100644
index 00000000..528dbc8d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_smetric.py
@@ -0,0 +1,8 @@
+import pytest
+
+import networkx as nx
+
+
+def test_smetric():
+    G = nx.Graph([(1, 2), (2, 3), (2, 4), (1, 4)])
+    assert nx.s_metric(G) == 19.0
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_sparsifiers.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_sparsifiers.py
new file mode 100644
index 00000000..e8604e61
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_sparsifiers.py
@@ -0,0 +1,138 @@
+"""Unit tests for the sparsifier computation functions."""
+
+import pytest
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+_seed = 2
+
+
+def _test_spanner(G, spanner, stretch, weight=None):
+    """Test whether a spanner is valid.
+
+    This function tests whether the given spanner is a subgraph of the
+    given graph G with the same node set. It also tests for all shortest
+    paths whether they adhere to the given stretch.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        The original graph for which the spanner was constructed.
+
+    spanner : NetworkX graph
+        The spanner to be tested.
+
+    stretch : float
+        The proclaimed stretch of the spanner.
+
+    weight : object
+        The edge attribute to use as distance.
+    """
+    # check node set
+    assert set(G.nodes()) == set(spanner.nodes())
+
+    # check edge set and weights
+    for u, v in spanner.edges():
+        assert G.has_edge(u, v)
+        if weight:
+            assert spanner[u][v][weight] == G[u][v][weight]
+
+    # check connectivity and stretch
+    original_length = dict(nx.shortest_path_length(G, weight=weight))
+    spanner_length = dict(nx.shortest_path_length(spanner, weight=weight))
+    for u in G.nodes():
+        for v in G.nodes():
+            if u in original_length and v in original_length[u]:
+                assert spanner_length[u][v] <= stretch * original_length[u][v]
+
+
+@py_random_state(1)
+def _assign_random_weights(G, seed=None):
+    """Assigns random weights to the edges of a graph.
+
+    Parameters
+    ----------
+
+    G : NetworkX graph
+        The original graph for which the spanner was constructed.
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+    """
+    for u, v in G.edges():
+        G[u][v]["weight"] = seed.random()
+
+
+def test_spanner_trivial():
+    """Test a trivial spanner with stretch 1."""
+    G = nx.complete_graph(20)
+    spanner = nx.spanner(G, 1, seed=_seed)
+
+    for u, v in G.edges:
+        assert spanner.has_edge(u, v)
+
+
+def test_spanner_unweighted_complete_graph():
+    """Test spanner construction on a complete unweighted graph."""
+    G = nx.complete_graph(20)
+
+    spanner = nx.spanner(G, 4, seed=_seed)
+    _test_spanner(G, spanner, 4)
+
+    spanner = nx.spanner(G, 10, seed=_seed)
+    _test_spanner(G, spanner, 10)
+
+
+def test_spanner_weighted_complete_graph():
+    """Test spanner construction on a complete weighted graph."""
+    G = nx.complete_graph(20)
+    _assign_random_weights(G, seed=_seed)
+
+    spanner = nx.spanner(G, 4, weight="weight", seed=_seed)
+    _test_spanner(G, spanner, 4, weight="weight")
+
+    spanner = nx.spanner(G, 10, weight="weight", seed=_seed)
+    _test_spanner(G, spanner, 10, weight="weight")
+
+
+def test_spanner_unweighted_gnp_graph():
+    """Test spanner construction on an unweighted gnp graph."""
+    G = nx.gnp_random_graph(20, 0.4, seed=_seed)
+
+    spanner = nx.spanner(G, 4, seed=_seed)
+    _test_spanner(G, spanner, 4)
+
+    spanner = nx.spanner(G, 10, seed=_seed)
+    _test_spanner(G, spanner, 10)
+
+
+def test_spanner_weighted_gnp_graph():
+    """Test spanner construction on an weighted gnp graph."""
+    G = nx.gnp_random_graph(20, 0.4, seed=_seed)
+    _assign_random_weights(G, seed=_seed)
+
+    spanner = nx.spanner(G, 4, weight="weight", seed=_seed)
+    _test_spanner(G, spanner, 4, weight="weight")
+
+    spanner = nx.spanner(G, 10, weight="weight", seed=_seed)
+    _test_spanner(G, spanner, 10, weight="weight")
+
+
+def test_spanner_unweighted_disconnected_graph():
+    """Test spanner construction on a disconnected graph."""
+    G = nx.disjoint_union(nx.complete_graph(10), nx.complete_graph(10))
+
+    spanner = nx.spanner(G, 4, seed=_seed)
+    _test_spanner(G, spanner, 4)
+
+    spanner = nx.spanner(G, 10, seed=_seed)
+    _test_spanner(G, spanner, 10)
+
+
+def test_spanner_invalid_stretch():
+    """Check whether an invalid stretch is caught."""
+    with pytest.raises(ValueError):
+        G = nx.empty_graph()
+        nx.spanner(G, 0)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_structuralholes.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_structuralholes.py
new file mode 100644
index 00000000..1e5952b2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_structuralholes.py
@@ -0,0 +1,137 @@
+"""Unit tests for the :mod:`networkx.algorithms.structuralholes` module."""
+
+import math
+
+import pytest
+
+import networkx as nx
+from networkx.classes.tests import dispatch_interface
+
+
+class TestStructuralHoles:
+    """Unit tests for computing measures of structural holes.
+
+    The expected values for these functions were originally computed using the
+    proprietary software `UCINET`_ and the free software `IGraph`_ , and then
+    computed by hand to make sure that the results are correct.
+
+    .. _UCINET: https://sites.google.com/site/ucinetsoftware/home
+    .. _IGraph: http://igraph.org/
+
+    """
+
+    def setup_method(self):
+        self.D = nx.DiGraph()
+        self.D.add_edges_from([(0, 1), (0, 2), (1, 0), (2, 1)])
+        self.D_weights = {(0, 1): 2, (0, 2): 2, (1, 0): 1, (2, 1): 1}
+        # Example from http://www.analytictech.com/connections/v20(1)/holes.htm
+        self.G = nx.Graph()
+        self.G.add_edges_from(
+            [
+                ("A", "B"),
+                ("A", "F"),
+                ("A", "G"),
+                ("A", "E"),
+                ("E", "G"),
+                ("F", "G"),
+                ("B", "G"),
+                ("B", "D"),
+                ("D", "G"),
+                ("G", "C"),
+            ]
+        )
+        self.G_weights = {
+            ("A", "B"): 2,
+            ("A", "F"): 3,
+            ("A", "G"): 5,
+            ("A", "E"): 2,
+            ("E", "G"): 8,
+            ("F", "G"): 3,
+            ("B", "G"): 4,
+            ("B", "D"): 1,
+            ("D", "G"): 3,
+            ("G", "C"): 10,
+        }
+
+    def test_constraint_directed(self):
+        constraint = nx.constraint(self.D)
+        assert constraint[0] == pytest.approx(1.003, abs=1e-3)
+        assert constraint[1] == pytest.approx(1.003, abs=1e-3)
+        assert constraint[2] == pytest.approx(1.389, abs=1e-3)
+
+    def test_effective_size_directed(self):
+        effective_size = nx.effective_size(self.D)
+        assert effective_size[0] == pytest.approx(1.167, abs=1e-3)
+        assert effective_size[1] == pytest.approx(1.167, abs=1e-3)
+        assert effective_size[2] == pytest.approx(1, abs=1e-3)
+
+    def test_constraint_weighted_directed(self):
+        D = self.D.copy()
+        nx.set_edge_attributes(D, self.D_weights, "weight")
+        constraint = nx.constraint(D, weight="weight")
+        assert constraint[0] == pytest.approx(0.840, abs=1e-3)
+        assert constraint[1] == pytest.approx(1.143, abs=1e-3)
+        assert constraint[2] == pytest.approx(1.378, abs=1e-3)
+
+    def test_effective_size_weighted_directed(self):
+        D = self.D.copy()
+        nx.set_edge_attributes(D, self.D_weights, "weight")
+        effective_size = nx.effective_size(D, weight="weight")
+        assert effective_size[0] == pytest.approx(1.567, abs=1e-3)
+        assert effective_size[1] == pytest.approx(1.083, abs=1e-3)
+        assert effective_size[2] == pytest.approx(1, abs=1e-3)
+
+    def test_constraint_undirected(self):
+        constraint = nx.constraint(self.G)
+        assert constraint["G"] == pytest.approx(0.400, abs=1e-3)
+        assert constraint["A"] == pytest.approx(0.595, abs=1e-3)
+        assert constraint["C"] == pytest.approx(1, abs=1e-3)
+
+    def test_effective_size_undirected_borgatti(self):
+        effective_size = nx.effective_size(self.G)
+        assert effective_size["G"] == pytest.approx(4.67, abs=1e-2)
+        assert effective_size["A"] == pytest.approx(2.50, abs=1e-2)
+        assert effective_size["C"] == pytest.approx(1, abs=1e-2)
+
+    def test_effective_size_undirected(self):
+        G = self.G.copy()
+        nx.set_edge_attributes(G, 1, "weight")
+        effective_size = nx.effective_size(G, weight="weight")
+        assert effective_size["G"] == pytest.approx(4.67, abs=1e-2)
+        assert effective_size["A"] == pytest.approx(2.50, abs=1e-2)
+        assert effective_size["C"] == pytest.approx(1, abs=1e-2)
+
+    def test_constraint_weighted_undirected(self):
+        G = self.G.copy()
+        nx.set_edge_attributes(G, self.G_weights, "weight")
+        constraint = nx.constraint(G, weight="weight")
+        assert constraint["G"] == pytest.approx(0.299, abs=1e-3)
+        assert constraint["A"] == pytest.approx(0.795, abs=1e-3)
+        assert constraint["C"] == pytest.approx(1, abs=1e-3)
+
+    def test_effective_size_weighted_undirected(self):
+        G = self.G.copy()
+        nx.set_edge_attributes(G, self.G_weights, "weight")
+        effective_size = nx.effective_size(G, weight="weight")
+        assert effective_size["G"] == pytest.approx(5.47, abs=1e-2)
+        assert effective_size["A"] == pytest.approx(2.47, abs=1e-2)
+        assert effective_size["C"] == pytest.approx(1, abs=1e-2)
+
+    def test_constraint_isolated(self):
+        G = self.G.copy()
+        G.add_node(1)
+        constraint = nx.constraint(G)
+        assert math.isnan(constraint[1])
+
+    def test_effective_size_isolated(self):
+        G = self.G.copy()
+        G.add_node(1)
+        nx.set_edge_attributes(G, self.G_weights, "weight")
+        effective_size = nx.effective_size(G, weight="weight")
+        assert math.isnan(effective_size[1])
+
+    def test_effective_size_borgatti_isolated(self):
+        G = self.G.copy()
+        G.add_node(1)
+        effective_size = nx.effective_size(G)
+        assert math.isnan(effective_size[1])
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_summarization.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_summarization.py
new file mode 100644
index 00000000..c3bf82fa
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_summarization.py
@@ -0,0 +1,642 @@
+"""
+Unit tests for dedensification and graph summarization
+"""
+
+import pytest
+
+import networkx as nx
+
+
+class TestDirectedDedensification:
+    def build_original_graph(self):
+        original_matrix = [
+            ("1", "BC"),
+            ("2", "ABC"),
+            ("3", ["A", "B", "6"]),
+            ("4", "ABC"),
+            ("5", "AB"),
+            ("6", ["5"]),
+            ("A", ["6"]),
+        ]
+        graph = nx.DiGraph()
+        for source, targets in original_matrix:
+            for target in targets:
+                graph.add_edge(source, target)
+        return graph
+
+    def build_compressed_graph(self):
+        compressed_matrix = [
+            ("1", "BC"),
+            ("2", ["ABC"]),
+            ("3", ["A", "B", "6"]),
+            ("4", ["ABC"]),
+            ("5", "AB"),
+            ("6", ["5"]),
+            ("A", ["6"]),
+            ("ABC", "ABC"),
+        ]
+        compressed_graph = nx.DiGraph()
+        for source, targets in compressed_matrix:
+            for target in targets:
+                compressed_graph.add_edge(source, target)
+        return compressed_graph
+
+    def test_empty(self):
+        """
+        Verify that an empty directed graph results in no compressor nodes
+        """
+        G = nx.DiGraph()
+        compressed_graph, c_nodes = nx.dedensify(G, threshold=2)
+        assert c_nodes == set()
+
+    @staticmethod
+    def densify(G, compressor_nodes, copy=True):
+        """
+        Reconstructs the original graph from a dedensified, directed graph
+
+        Parameters
+        ----------
+        G: dedensified graph
+           A networkx graph
+        compressor_nodes: iterable
+           Iterable of compressor nodes in the dedensified graph
+        inplace: bool, optional (default: False)
+           Indicates if densification should be done inplace
+
+        Returns
+        -------
+        G: graph
+           A densified networkx graph
+        """
+        if copy:
+            G = G.copy()
+        for compressor_node in compressor_nodes:
+            all_neighbors = set(nx.all_neighbors(G, compressor_node))
+            out_neighbors = set(G.neighbors(compressor_node))
+            for out_neighbor in out_neighbors:
+                G.remove_edge(compressor_node, out_neighbor)
+            in_neighbors = all_neighbors - out_neighbors
+            for in_neighbor in in_neighbors:
+                G.remove_edge(in_neighbor, compressor_node)
+                for out_neighbor in out_neighbors:
+                    G.add_edge(in_neighbor, out_neighbor)
+            G.remove_node(compressor_node)
+        return G
+
+    def setup_method(self):
+        self.c_nodes = ("ABC",)
+
+    def test_dedensify_edges(self):
+        """
+        Verifies that dedensify produced the correct edges to/from compressor
+        nodes in a directed graph
+        """
+        G = self.build_original_graph()
+        compressed_G = self.build_compressed_graph()
+        compressed_graph, c_nodes = nx.dedensify(G, threshold=2)
+        for s, t in compressed_graph.edges():
+            o_s = "".join(sorted(s))
+            o_t = "".join(sorted(t))
+            compressed_graph_exists = compressed_graph.has_edge(s, t)
+            verified_compressed_exists = compressed_G.has_edge(o_s, o_t)
+            assert compressed_graph_exists == verified_compressed_exists
+        assert len(c_nodes) == len(self.c_nodes)
+
+    def test_dedensify_edge_count(self):
+        """
+        Verifies that dedensify produced the correct number of compressor nodes
+        in a directed graph
+        """
+        G = self.build_original_graph()
+        original_edge_count = len(G.edges())
+        c_G, c_nodes = nx.dedensify(G, threshold=2)
+        compressed_edge_count = len(c_G.edges())
+        assert compressed_edge_count <= original_edge_count
+        compressed_G = self.build_compressed_graph()
+        assert compressed_edge_count == len(compressed_G.edges())
+
+    def test_densify_edges(self):
+        """
+        Verifies that densification produces the correct edges from the
+        original directed graph
+        """
+        compressed_G = self.build_compressed_graph()
+        original_graph = self.densify(compressed_G, self.c_nodes, copy=True)
+        G = self.build_original_graph()
+        for s, t in G.edges():
+            assert G.has_edge(s, t) == original_graph.has_edge(s, t)
+
+    def test_densify_edge_count(self):
+        """
+        Verifies that densification produces the correct number of edges in the
+        original directed graph
+        """
+        compressed_G = self.build_compressed_graph()
+        compressed_edge_count = len(compressed_G.edges())
+        original_graph = self.densify(compressed_G, self.c_nodes)
+        original_edge_count = len(original_graph.edges())
+        assert compressed_edge_count <= original_edge_count
+        G = self.build_original_graph()
+        assert original_edge_count == len(G.edges())
+
+
+class TestUnDirectedDedensification:
+    def build_original_graph(self):
+        """
+        Builds graph shown in the original research paper
+        """
+        original_matrix = [
+            ("1", "CB"),
+            ("2", "ABC"),
+            ("3", ["A", "B", "6"]),
+            ("4", "ABC"),
+            ("5", "AB"),
+            ("6", ["5"]),
+            ("A", ["6"]),
+        ]
+        graph = nx.Graph()
+        for source, targets in original_matrix:
+            for target in targets:
+                graph.add_edge(source, target)
+        return graph
+
+    def test_empty(self):
+        """
+        Verify that an empty undirected graph results in no compressor nodes
+        """
+        G = nx.Graph()
+        compressed_G, c_nodes = nx.dedensify(G, threshold=2)
+        assert c_nodes == set()
+
+    def setup_method(self):
+        self.c_nodes = ("6AB", "ABC")
+
+    def build_compressed_graph(self):
+        compressed_matrix = [
+            ("1", ["B", "C"]),
+            ("2", ["ABC"]),
+            ("3", ["6AB"]),
+            ("4", ["ABC"]),
+            ("5", ["6AB"]),
+            ("6", ["6AB", "A"]),
+            ("A", ["6AB", "ABC"]),
+            ("B", ["ABC", "6AB"]),
+            ("C", ["ABC"]),
+        ]
+        compressed_graph = nx.Graph()
+        for source, targets in compressed_matrix:
+            for target in targets:
+                compressed_graph.add_edge(source, target)
+        return compressed_graph
+
+    def test_dedensify_edges(self):
+        """
+        Verifies that dedensify produced correct compressor nodes and the
+        correct edges to/from the compressor nodes in an undirected graph
+        """
+        G = self.build_original_graph()
+        c_G, c_nodes = nx.dedensify(G, threshold=2)
+        v_compressed_G = self.build_compressed_graph()
+        for s, t in c_G.edges():
+            o_s = "".join(sorted(s))
+            o_t = "".join(sorted(t))
+            has_compressed_edge = c_G.has_edge(s, t)
+            verified_has_compressed_edge = v_compressed_G.has_edge(o_s, o_t)
+            assert has_compressed_edge == verified_has_compressed_edge
+        assert len(c_nodes) == len(self.c_nodes)
+
+    def test_dedensify_edge_count(self):
+        """
+        Verifies that dedensify produced the correct number of edges in an
+        undirected graph
+        """
+        G = self.build_original_graph()
+        c_G, c_nodes = nx.dedensify(G, threshold=2, copy=True)
+        compressed_edge_count = len(c_G.edges())
+        verified_original_edge_count = len(G.edges())
+        assert compressed_edge_count <= verified_original_edge_count
+        verified_compressed_G = self.build_compressed_graph()
+        verified_compressed_edge_count = len(verified_compressed_G.edges())
+        assert compressed_edge_count == verified_compressed_edge_count
+
+
+@pytest.mark.parametrize(
+    "graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
+)
+def test_summarization_empty(graph_type):
+    G = graph_type()
+    summary_graph = nx.snap_aggregation(G, node_attributes=("color",))
+    assert nx.is_isomorphic(summary_graph, G)
+
+
+class AbstractSNAP:
+    node_attributes = ("color",)
+
+    def build_original_graph(self):
+        pass
+
+    def build_summary_graph(self):
+        pass
+
+    def test_summary_graph(self):
+        original_graph = self.build_original_graph()
+        summary_graph = self.build_summary_graph()
+
+        relationship_attributes = ("type",)
+        generated_summary_graph = nx.snap_aggregation(
+            original_graph, self.node_attributes, relationship_attributes
+        )
+        relabeled_summary_graph = self.deterministic_labels(generated_summary_graph)
+        assert nx.is_isomorphic(summary_graph, relabeled_summary_graph)
+
+    def deterministic_labels(self, G):
+        node_labels = list(G.nodes)
+        node_labels = sorted(node_labels, key=lambda n: sorted(G.nodes[n]["group"])[0])
+        node_labels.sort()
+
+        label_mapping = {}
+        for index, node in enumerate(node_labels):
+            label = f"Supernode-{index}"
+            label_mapping[node] = label
+
+        return nx.relabel_nodes(G, label_mapping)
+
+
+class TestSNAPNoEdgeTypes(AbstractSNAP):
+    relationship_attributes = ()
+
+    def test_summary_graph(self):
+        original_graph = self.build_original_graph()
+        summary_graph = self.build_summary_graph()
+
+        relationship_attributes = ("type",)
+        generated_summary_graph = nx.snap_aggregation(
+            original_graph, self.node_attributes
+        )
+        relabeled_summary_graph = self.deterministic_labels(generated_summary_graph)
+        assert nx.is_isomorphic(summary_graph, relabeled_summary_graph)
+
+    def build_original_graph(self):
+        nodes = {
+            "A": {"color": "Red"},
+            "B": {"color": "Red"},
+            "C": {"color": "Red"},
+            "D": {"color": "Red"},
+            "E": {"color": "Blue"},
+            "F": {"color": "Blue"},
+            "G": {"color": "Blue"},
+            "H": {"color": "Blue"},
+            "I": {"color": "Yellow"},
+            "J": {"color": "Yellow"},
+            "K": {"color": "Yellow"},
+            "L": {"color": "Yellow"},
+        }
+        edges = [
+            ("A", "B"),
+            ("A", "C"),
+            ("A", "E"),
+            ("A", "I"),
+            ("B", "D"),
+            ("B", "J"),
+            ("B", "F"),
+            ("C", "G"),
+            ("D", "H"),
+            ("I", "J"),
+            ("J", "K"),
+            ("I", "L"),
+        ]
+        G = nx.Graph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target in edges:
+            G.add_edge(source, target)
+
+        return G
+
+    def build_summary_graph(self):
+        nodes = {
+            "Supernode-0": {"color": "Red"},
+            "Supernode-1": {"color": "Red"},
+            "Supernode-2": {"color": "Blue"},
+            "Supernode-3": {"color": "Blue"},
+            "Supernode-4": {"color": "Yellow"},
+            "Supernode-5": {"color": "Yellow"},
+        }
+        edges = [
+            ("Supernode-0", "Supernode-0"),
+            ("Supernode-0", "Supernode-1"),
+            ("Supernode-0", "Supernode-2"),
+            ("Supernode-0", "Supernode-4"),
+            ("Supernode-1", "Supernode-3"),
+            ("Supernode-4", "Supernode-4"),
+            ("Supernode-4", "Supernode-5"),
+        ]
+        G = nx.Graph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target in edges:
+            G.add_edge(source, target)
+
+        supernodes = {
+            "Supernode-0": {"A", "B"},
+            "Supernode-1": {"C", "D"},
+            "Supernode-2": {"E", "F"},
+            "Supernode-3": {"G", "H"},
+            "Supernode-4": {"I", "J"},
+            "Supernode-5": {"K", "L"},
+        }
+        nx.set_node_attributes(G, supernodes, "group")
+        return G
+
+
+class TestSNAPUndirected(AbstractSNAP):
+    def build_original_graph(self):
+        nodes = {
+            "A": {"color": "Red"},
+            "B": {"color": "Red"},
+            "C": {"color": "Red"},
+            "D": {"color": "Red"},
+            "E": {"color": "Blue"},
+            "F": {"color": "Blue"},
+            "G": {"color": "Blue"},
+            "H": {"color": "Blue"},
+            "I": {"color": "Yellow"},
+            "J": {"color": "Yellow"},
+            "K": {"color": "Yellow"},
+            "L": {"color": "Yellow"},
+        }
+        edges = [
+            ("A", "B", "Strong"),
+            ("A", "C", "Weak"),
+            ("A", "E", "Strong"),
+            ("A", "I", "Weak"),
+            ("B", "D", "Weak"),
+            ("B", "J", "Weak"),
+            ("B", "F", "Strong"),
+            ("C", "G", "Weak"),
+            ("D", "H", "Weak"),
+            ("I", "J", "Strong"),
+            ("J", "K", "Strong"),
+            ("I", "L", "Strong"),
+        ]
+        G = nx.Graph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target, type in edges:
+            G.add_edge(source, target, type=type)
+
+        return G
+
+    def build_summary_graph(self):
+        nodes = {
+            "Supernode-0": {"color": "Red"},
+            "Supernode-1": {"color": "Red"},
+            "Supernode-2": {"color": "Blue"},
+            "Supernode-3": {"color": "Blue"},
+            "Supernode-4": {"color": "Yellow"},
+            "Supernode-5": {"color": "Yellow"},
+        }
+        edges = [
+            ("Supernode-0", "Supernode-0", "Strong"),
+            ("Supernode-0", "Supernode-1", "Weak"),
+            ("Supernode-0", "Supernode-2", "Strong"),
+            ("Supernode-0", "Supernode-4", "Weak"),
+            ("Supernode-1", "Supernode-3", "Weak"),
+            ("Supernode-4", "Supernode-4", "Strong"),
+            ("Supernode-4", "Supernode-5", "Strong"),
+        ]
+        G = nx.Graph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target, type in edges:
+            G.add_edge(source, target, types=[{"type": type}])
+
+        supernodes = {
+            "Supernode-0": {"A", "B"},
+            "Supernode-1": {"C", "D"},
+            "Supernode-2": {"E", "F"},
+            "Supernode-3": {"G", "H"},
+            "Supernode-4": {"I", "J"},
+            "Supernode-5": {"K", "L"},
+        }
+        nx.set_node_attributes(G, supernodes, "group")
+        return G
+
+
+class TestSNAPDirected(AbstractSNAP):
+    def build_original_graph(self):
+        nodes = {
+            "A": {"color": "Red"},
+            "B": {"color": "Red"},
+            "C": {"color": "Green"},
+            "D": {"color": "Green"},
+            "E": {"color": "Blue"},
+            "F": {"color": "Blue"},
+            "G": {"color": "Yellow"},
+            "H": {"color": "Yellow"},
+        }
+        edges = [
+            ("A", "C", "Strong"),
+            ("A", "E", "Strong"),
+            ("A", "F", "Weak"),
+            ("B", "D", "Strong"),
+            ("B", "E", "Weak"),
+            ("B", "F", "Strong"),
+            ("C", "G", "Strong"),
+            ("C", "F", "Strong"),
+            ("D", "E", "Strong"),
+            ("D", "H", "Strong"),
+            ("G", "E", "Strong"),
+            ("H", "F", "Strong"),
+        ]
+        G = nx.DiGraph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target, type in edges:
+            G.add_edge(source, target, type=type)
+
+        return G
+
+    def build_summary_graph(self):
+        nodes = {
+            "Supernode-0": {"color": "Red"},
+            "Supernode-1": {"color": "Green"},
+            "Supernode-2": {"color": "Blue"},
+            "Supernode-3": {"color": "Yellow"},
+        }
+        edges = [
+            ("Supernode-0", "Supernode-1", [{"type": "Strong"}]),
+            ("Supernode-0", "Supernode-2", [{"type": "Weak"}, {"type": "Strong"}]),
+            ("Supernode-1", "Supernode-2", [{"type": "Strong"}]),
+            ("Supernode-1", "Supernode-3", [{"type": "Strong"}]),
+            ("Supernode-3", "Supernode-2", [{"type": "Strong"}]),
+        ]
+        G = nx.DiGraph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target, types in edges:
+            G.add_edge(source, target, types=types)
+
+        supernodes = {
+            "Supernode-0": {"A", "B"},
+            "Supernode-1": {"C", "D"},
+            "Supernode-2": {"E", "F"},
+            "Supernode-3": {"G", "H"},
+            "Supernode-4": {"I", "J"},
+            "Supernode-5": {"K", "L"},
+        }
+        nx.set_node_attributes(G, supernodes, "group")
+        return G
+
+
+class TestSNAPUndirectedMulti(AbstractSNAP):
+    def build_original_graph(self):
+        nodes = {
+            "A": {"color": "Red"},
+            "B": {"color": "Red"},
+            "C": {"color": "Red"},
+            "D": {"color": "Blue"},
+            "E": {"color": "Blue"},
+            "F": {"color": "Blue"},
+            "G": {"color": "Yellow"},
+            "H": {"color": "Yellow"},
+            "I": {"color": "Yellow"},
+        }
+        edges = [
+            ("A", "D", ["Weak", "Strong"]),
+            ("B", "E", ["Weak", "Strong"]),
+            ("D", "I", ["Strong"]),
+            ("E", "H", ["Strong"]),
+            ("F", "G", ["Weak"]),
+            ("I", "G", ["Weak", "Strong"]),
+            ("I", "H", ["Weak", "Strong"]),
+            ("G", "H", ["Weak", "Strong"]),
+        ]
+        G = nx.MultiGraph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target, types in edges:
+            for type in types:
+                G.add_edge(source, target, type=type)
+
+        return G
+
+    def build_summary_graph(self):
+        nodes = {
+            "Supernode-0": {"color": "Red"},
+            "Supernode-1": {"color": "Blue"},
+            "Supernode-2": {"color": "Yellow"},
+            "Supernode-3": {"color": "Blue"},
+            "Supernode-4": {"color": "Yellow"},
+            "Supernode-5": {"color": "Red"},
+        }
+        edges = [
+            ("Supernode-1", "Supernode-2", [{"type": "Weak"}]),
+            ("Supernode-2", "Supernode-4", [{"type": "Weak"}, {"type": "Strong"}]),
+            ("Supernode-3", "Supernode-4", [{"type": "Strong"}]),
+            ("Supernode-3", "Supernode-5", [{"type": "Weak"}, {"type": "Strong"}]),
+            ("Supernode-4", "Supernode-4", [{"type": "Weak"}, {"type": "Strong"}]),
+        ]
+        G = nx.MultiGraph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target, types in edges:
+            for type in types:
+                G.add_edge(source, target, type=type)
+
+        supernodes = {
+            "Supernode-0": {"A", "B"},
+            "Supernode-1": {"C", "D"},
+            "Supernode-2": {"E", "F"},
+            "Supernode-3": {"G", "H"},
+            "Supernode-4": {"I", "J"},
+            "Supernode-5": {"K", "L"},
+        }
+        nx.set_node_attributes(G, supernodes, "group")
+        return G
+
+
+class TestSNAPDirectedMulti(AbstractSNAP):
+    def build_original_graph(self):
+        nodes = {
+            "A": {"color": "Red"},
+            "B": {"color": "Red"},
+            "C": {"color": "Green"},
+            "D": {"color": "Green"},
+            "E": {"color": "Blue"},
+            "F": {"color": "Blue"},
+            "G": {"color": "Yellow"},
+            "H": {"color": "Yellow"},
+        }
+        edges = [
+            ("A", "C", ["Weak", "Strong"]),
+            ("A", "E", ["Strong"]),
+            ("A", "F", ["Weak"]),
+            ("B", "D", ["Weak", "Strong"]),
+            ("B", "E", ["Weak"]),
+            ("B", "F", ["Strong"]),
+            ("C", "G", ["Weak", "Strong"]),
+            ("C", "F", ["Strong"]),
+            ("D", "E", ["Strong"]),
+            ("D", "H", ["Weak", "Strong"]),
+            ("G", "E", ["Strong"]),
+            ("H", "F", ["Strong"]),
+        ]
+        G = nx.MultiDiGraph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target, types in edges:
+            for type in types:
+                G.add_edge(source, target, type=type)
+
+        return G
+
+    def build_summary_graph(self):
+        nodes = {
+            "Supernode-0": {"color": "Red"},
+            "Supernode-1": {"color": "Blue"},
+            "Supernode-2": {"color": "Yellow"},
+            "Supernode-3": {"color": "Blue"},
+        }
+        edges = [
+            ("Supernode-0", "Supernode-1", ["Weak", "Strong"]),
+            ("Supernode-0", "Supernode-2", ["Weak", "Strong"]),
+            ("Supernode-1", "Supernode-2", ["Strong"]),
+            ("Supernode-1", "Supernode-3", ["Weak", "Strong"]),
+            ("Supernode-3", "Supernode-2", ["Strong"]),
+        ]
+        G = nx.MultiDiGraph()
+        for node in nodes:
+            attributes = nodes[node]
+            G.add_node(node, **attributes)
+
+        for source, target, types in edges:
+            for type in types:
+                G.add_edge(source, target, type=type)
+
+        supernodes = {
+            "Supernode-0": {"A", "B"},
+            "Supernode-1": {"C", "D"},
+            "Supernode-2": {"E", "F"},
+            "Supernode-3": {"G", "H"},
+        }
+        nx.set_node_attributes(G, supernodes, "group")
+        return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_swap.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_swap.py
new file mode 100644
index 00000000..e765bd5e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_swap.py
@@ -0,0 +1,179 @@
+import pytest
+
+import networkx as nx
+
+cycle = nx.cycle_graph(5, create_using=nx.DiGraph)
+tree = nx.DiGraph()
+tree.add_edges_from(nx.random_labeled_tree(10, seed=42).edges)
+path = nx.path_graph(5, create_using=nx.DiGraph)
+binomial = nx.binomial_tree(3, create_using=nx.DiGraph)
+HH = nx.directed_havel_hakimi_graph([1, 2, 1, 2, 2, 2], [3, 1, 0, 1, 2, 3])
+balanced_tree = nx.balanced_tree(2, 3, create_using=nx.DiGraph)
+
+
+@pytest.mark.parametrize("G", [path, binomial, HH, cycle, tree, balanced_tree])
+def test_directed_edge_swap(G):
+    in_degree = set(G.in_degree)
+    out_degree = set(G.out_degree)
+    edges = set(G.edges)
+    nx.directed_edge_swap(G, nswap=1, max_tries=100, seed=1)
+    assert in_degree == set(G.in_degree)
+    assert out_degree == set(G.out_degree)
+    assert edges != set(G.edges)
+    assert 3 == sum(e not in edges for e in G.edges)
+
+
+def test_directed_edge_swap_undo_previous_swap():
+    G = nx.DiGraph(nx.path_graph(4).edges)  # only 1 swap possible
+    edges = set(G.edges)
+    nx.directed_edge_swap(G, nswap=2, max_tries=100)
+    assert edges == set(G.edges)
+
+    nx.directed_edge_swap(G, nswap=1, max_tries=100, seed=1)
+    assert {(0, 2), (1, 3), (2, 1)} == set(G.edges)
+    nx.directed_edge_swap(G, nswap=1, max_tries=100, seed=1)
+    assert edges == set(G.edges)
+
+
+def test_edge_cases_directed_edge_swap():
+    # Tests cases when swaps are impossible, either too few edges exist, or self loops/cycles are unavoidable
+    # TODO: Rewrite function to explicitly check for impossible swaps and raise error
+    e = (
+        "Maximum number of swap attempts \\(11\\) exceeded "
+        "before desired swaps achieved \\(\\d\\)."
+    )
+    graph = nx.DiGraph([(0, 0), (0, 1), (1, 0), (2, 3), (3, 2)])
+    with pytest.raises(nx.NetworkXAlgorithmError, match=e):
+        nx.directed_edge_swap(graph, nswap=1, max_tries=10, seed=1)
+
+
+def test_double_edge_swap():
+    graph = nx.barabasi_albert_graph(200, 1)
+    degrees = sorted(d for n, d in graph.degree())
+    G = nx.double_edge_swap(graph, 40)
+    assert degrees == sorted(d for n, d in graph.degree())
+
+
+def test_double_edge_swap_seed():
+    graph = nx.barabasi_albert_graph(200, 1)
+    degrees = sorted(d for n, d in graph.degree())
+    G = nx.double_edge_swap(graph, 40, seed=1)
+    assert degrees == sorted(d for n, d in graph.degree())
+
+
+def test_connected_double_edge_swap():
+    graph = nx.barabasi_albert_graph(200, 1)
+    degrees = sorted(d for n, d in graph.degree())
+    G = nx.connected_double_edge_swap(graph, 40, seed=1)
+    assert nx.is_connected(graph)
+    assert degrees == sorted(d for n, d in graph.degree())
+
+
+def test_connected_double_edge_swap_low_window_threshold():
+    graph = nx.barabasi_albert_graph(200, 1)
+    degrees = sorted(d for n, d in graph.degree())
+    G = nx.connected_double_edge_swap(graph, 40, _window_threshold=0, seed=1)
+    assert nx.is_connected(graph)
+    assert degrees == sorted(d for n, d in graph.degree())
+
+
+def test_connected_double_edge_swap_star():
+    # Testing ui==xi in connected_double_edge_swap
+    graph = nx.star_graph(40)
+    degrees = sorted(d for n, d in graph.degree())
+    G = nx.connected_double_edge_swap(graph, 1, seed=4)
+    assert nx.is_connected(graph)
+    assert degrees == sorted(d for n, d in graph.degree())
+
+
+def test_connected_double_edge_swap_star_low_window_threshold():
+    # Testing ui==xi in connected_double_edge_swap with low window threshold
+    graph = nx.star_graph(40)
+    degrees = sorted(d for n, d in graph.degree())
+    G = nx.connected_double_edge_swap(graph, 1, _window_threshold=0, seed=4)
+    assert nx.is_connected(graph)
+    assert degrees == sorted(d for n, d in graph.degree())
+
+
+def test_directed_edge_swap_small():
+    with pytest.raises(nx.NetworkXError):
+        G = nx.directed_edge_swap(nx.path_graph(3, create_using=nx.DiGraph))
+
+
+def test_directed_edge_swap_tries():
+    with pytest.raises(nx.NetworkXError):
+        G = nx.directed_edge_swap(
+            nx.path_graph(3, create_using=nx.DiGraph), nswap=1, max_tries=0
+        )
+
+
+def test_directed_exception_undirected():
+    graph = nx.Graph([(0, 1), (2, 3)])
+    with pytest.raises(nx.NetworkXNotImplemented):
+        G = nx.directed_edge_swap(graph)
+
+
+def test_directed_edge_max_tries():
+    with pytest.raises(nx.NetworkXAlgorithmError):
+        G = nx.directed_edge_swap(
+            nx.complete_graph(4, nx.DiGraph()), nswap=1, max_tries=5
+        )
+
+
+def test_double_edge_swap_small():
+    with pytest.raises(nx.NetworkXError):
+        G = nx.double_edge_swap(nx.path_graph(3))
+
+
+def test_double_edge_swap_tries():
+    with pytest.raises(nx.NetworkXError):
+        G = nx.double_edge_swap(nx.path_graph(10), nswap=1, max_tries=0)
+
+
+def test_double_edge_directed():
+    graph = nx.DiGraph([(0, 1), (2, 3)])
+    with pytest.raises(nx.NetworkXError, match="not defined for directed graphs."):
+        G = nx.double_edge_swap(graph)
+
+
+def test_double_edge_max_tries():
+    with pytest.raises(nx.NetworkXAlgorithmError):
+        G = nx.double_edge_swap(nx.complete_graph(4), nswap=1, max_tries=5)
+
+
+def test_connected_double_edge_swap_small():
+    with pytest.raises(nx.NetworkXError):
+        G = nx.connected_double_edge_swap(nx.path_graph(3))
+
+
+def test_connected_double_edge_swap_not_connected():
+    with pytest.raises(nx.NetworkXError):
+        G = nx.path_graph(3)
+        nx.add_path(G, [10, 11, 12])
+        G = nx.connected_double_edge_swap(G)
+
+
+def test_degree_seq_c4():
+    G = nx.cycle_graph(4)
+    degrees = sorted(d for n, d in G.degree())
+    G = nx.double_edge_swap(G, 1, 100)
+    assert degrees == sorted(d for n, d in G.degree())
+
+
+def test_fewer_than_4_nodes():
+    G = nx.DiGraph()
+    G.add_nodes_from([0, 1, 2])
+    with pytest.raises(nx.NetworkXError, match=".*fewer than four nodes."):
+        nx.directed_edge_swap(G)
+
+
+def test_less_than_3_edges():
+    G = nx.DiGraph([(0, 1), (1, 2)])
+    G.add_nodes_from([3, 4])
+    with pytest.raises(nx.NetworkXError, match=".*fewer than 3 edges"):
+        nx.directed_edge_swap(G)
+
+    G = nx.Graph()
+    G.add_nodes_from([0, 1, 2, 3])
+    with pytest.raises(nx.NetworkXError, match=".*fewer than 2 edges"):
+        nx.double_edge_swap(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_threshold.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_threshold.py
new file mode 100644
index 00000000..07aad44b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_threshold.py
@@ -0,0 +1,269 @@
+"""
+Threshold Graphs
+================
+"""
+
+import pytest
+
+import networkx as nx
+import networkx.algorithms.threshold as nxt
+from networkx.algorithms.isomorphism.isomorph import graph_could_be_isomorphic
+
+cnlti = nx.convert_node_labels_to_integers
+
+
+class TestGeneratorThreshold:
+    def test_threshold_sequence_graph_test(self):
+        G = nx.star_graph(10)
+        assert nxt.is_threshold_graph(G)
+        assert nxt.is_threshold_sequence([d for n, d in G.degree()])
+
+        G = nx.complete_graph(10)
+        assert nxt.is_threshold_graph(G)
+        assert nxt.is_threshold_sequence([d for n, d in G.degree()])
+
+        deg = [3, 2, 2, 1, 1, 1]
+        assert not nxt.is_threshold_sequence(deg)
+
+        deg = [3, 2, 2, 1]
+        assert nxt.is_threshold_sequence(deg)
+
+        G = nx.generators.havel_hakimi_graph(deg)
+        assert nxt.is_threshold_graph(G)
+
+    def test_creation_sequences(self):
+        deg = [3, 2, 2, 1]
+        G = nx.generators.havel_hakimi_graph(deg)
+
+        with pytest.raises(ValueError):
+            nxt.creation_sequence(deg, with_labels=True, compact=True)
+
+        cs0 = nxt.creation_sequence(deg)
+        H0 = nxt.threshold_graph(cs0)
+        assert "".join(cs0) == "ddid"
+
+        cs1 = nxt.creation_sequence(deg, with_labels=True)
+        H1 = nxt.threshold_graph(cs1)
+        assert cs1 == [(1, "d"), (2, "d"), (3, "i"), (0, "d")]
+
+        cs2 = nxt.creation_sequence(deg, compact=True)
+        H2 = nxt.threshold_graph(cs2)
+        assert cs2 == [2, 1, 1]
+        assert "".join(nxt.uncompact(cs2)) == "ddid"
+        assert graph_could_be_isomorphic(H0, G)
+        assert graph_could_be_isomorphic(H0, H1)
+        assert graph_could_be_isomorphic(H0, H2)
+
+    def test_make_compact(self):
+        assert nxt.make_compact(["d", "d", "d", "i", "d", "d"]) == [3, 1, 2]
+        assert nxt.make_compact([3, 1, 2]) == [3, 1, 2]
+        assert pytest.raises(TypeError, nxt.make_compact, [3.0, 1.0, 2.0])
+
+    def test_uncompact(self):
+        assert nxt.uncompact([3, 1, 2]) == ["d", "d", "d", "i", "d", "d"]
+        assert nxt.uncompact(["d", "d", "i", "d"]) == ["d", "d", "i", "d"]
+        assert nxt.uncompact(
+            nxt.uncompact([(1, "d"), (2, "d"), (3, "i"), (0, "d")])
+        ) == nxt.uncompact([(1, "d"), (2, "d"), (3, "i"), (0, "d")])
+        assert pytest.raises(TypeError, nxt.uncompact, [3.0, 1.0, 2.0])
+
+    def test_creation_sequence_to_weights(self):
+        assert nxt.creation_sequence_to_weights([3, 1, 2]) == [
+            0.5,
+            0.5,
+            0.5,
+            0.25,
+            0.75,
+            0.75,
+        ]
+        assert pytest.raises(
+            TypeError, nxt.creation_sequence_to_weights, [3.0, 1.0, 2.0]
+        )
+
+    def test_weights_to_creation_sequence(self):
+        deg = [3, 2, 2, 1]
+        with pytest.raises(ValueError):
+            nxt.weights_to_creation_sequence(deg, with_labels=True, compact=True)
+        assert nxt.weights_to_creation_sequence(deg, with_labels=True) == [
+            (3, "d"),
+            (1, "d"),
+            (2, "d"),
+            (0, "d"),
+        ]
+        assert nxt.weights_to_creation_sequence(deg, compact=True) == [4]
+
+    def test_find_alternating_4_cycle(self):
+        G = nx.Graph()
+        G.add_edge(1, 2)
+        assert not nxt.find_alternating_4_cycle(G)
+
+    def test_shortest_path(self):
+        deg = [3, 2, 2, 1]
+        G = nx.generators.havel_hakimi_graph(deg)
+        cs1 = nxt.creation_sequence(deg, with_labels=True)
+        for n, m in [(3, 0), (0, 3), (0, 2), (0, 1), (1, 3), (3, 1), (1, 2), (2, 3)]:
+            assert nxt.shortest_path(cs1, n, m) == nx.shortest_path(G, n, m)
+
+        spl = nxt.shortest_path_length(cs1, 3)
+        spl2 = nxt.shortest_path_length([t for v, t in cs1], 2)
+        assert spl == spl2
+
+        spld = {}
+        for j, pl in enumerate(spl):
+            n = cs1[j][0]
+            spld[n] = pl
+        assert spld == nx.single_source_shortest_path_length(G, 3)
+
+        assert nxt.shortest_path(["d", "d", "d", "i", "d", "d"], 1, 2) == [1, 2]
+        assert nxt.shortest_path([3, 1, 2], 1, 2) == [1, 2]
+        assert pytest.raises(TypeError, nxt.shortest_path, [3.0, 1.0, 2.0], 1, 2)
+        assert pytest.raises(ValueError, nxt.shortest_path, [3, 1, 2], "a", 2)
+        assert pytest.raises(ValueError, nxt.shortest_path, [3, 1, 2], 1, "b")
+        assert nxt.shortest_path([3, 1, 2], 1, 1) == [1]
+
+    def test_shortest_path_length(self):
+        assert nxt.shortest_path_length([3, 1, 2], 1) == [1, 0, 1, 2, 1, 1]
+        assert nxt.shortest_path_length(["d", "d", "d", "i", "d", "d"], 1) == [
+            1,
+            0,
+            1,
+            2,
+            1,
+            1,
+        ]
+        assert nxt.shortest_path_length(("d", "d", "d", "i", "d", "d"), 1) == [
+            1,
+            0,
+            1,
+            2,
+            1,
+            1,
+        ]
+        assert pytest.raises(TypeError, nxt.shortest_path, [3.0, 1.0, 2.0], 1)
+
+    def test_random_threshold_sequence(self):
+        assert len(nxt.random_threshold_sequence(10, 0.5)) == 10
+        assert nxt.random_threshold_sequence(10, 0.5, seed=42) == [
+            "d",
+            "i",
+            "d",
+            "d",
+            "d",
+            "i",
+            "i",
+            "i",
+            "d",
+            "d",
+        ]
+        assert pytest.raises(ValueError, nxt.random_threshold_sequence, 10, 1.5)
+
+    def test_right_d_threshold_sequence(self):
+        assert nxt.right_d_threshold_sequence(3, 2) == ["d", "i", "d"]
+        assert pytest.raises(ValueError, nxt.right_d_threshold_sequence, 2, 3)
+
+    def test_left_d_threshold_sequence(self):
+        assert nxt.left_d_threshold_sequence(3, 2) == ["d", "i", "d"]
+        assert pytest.raises(ValueError, nxt.left_d_threshold_sequence, 2, 3)
+
+    def test_weights_thresholds(self):
+        wseq = [3, 4, 3, 3, 5, 6, 5, 4, 5, 6]
+        cs = nxt.weights_to_creation_sequence(wseq, threshold=10)
+        wseq = nxt.creation_sequence_to_weights(cs)
+        cs2 = nxt.weights_to_creation_sequence(wseq)
+        assert cs == cs2
+
+        wseq = nxt.creation_sequence_to_weights(nxt.uncompact([3, 1, 2, 3, 3, 2, 3]))
+        assert wseq == [
+            s * 0.125 for s in [4, 4, 4, 3, 5, 5, 2, 2, 2, 6, 6, 6, 1, 1, 7, 7, 7]
+        ]
+
+        wseq = nxt.creation_sequence_to_weights([3, 1, 2, 3, 3, 2, 3])
+        assert wseq == [
+            s * 0.125 for s in [4, 4, 4, 3, 5, 5, 2, 2, 2, 6, 6, 6, 1, 1, 7, 7, 7]
+        ]
+
+        wseq = nxt.creation_sequence_to_weights(list(enumerate("ddidiiidididi")))
+        assert wseq == [s * 0.1 for s in [5, 5, 4, 6, 3, 3, 3, 7, 2, 8, 1, 9, 0]]
+
+        wseq = nxt.creation_sequence_to_weights("ddidiiidididi")
+        assert wseq == [s * 0.1 for s in [5, 5, 4, 6, 3, 3, 3, 7, 2, 8, 1, 9, 0]]
+
+        wseq = nxt.creation_sequence_to_weights("ddidiiidididid")
+        ws = [s / 12 for s in [6, 6, 5, 7, 4, 4, 4, 8, 3, 9, 2, 10, 1, 11]]
+        assert sum(abs(c - d) for c, d in zip(wseq, ws)) < 1e-14
+
+    def test_finding_routines(self):
+        G = nx.Graph({1: [2], 2: [3], 3: [4], 4: [5], 5: [6]})
+        G.add_edge(2, 4)
+        G.add_edge(2, 5)
+        G.add_edge(2, 7)
+        G.add_edge(3, 6)
+        G.add_edge(4, 6)
+
+        # Alternating 4 cycle
+        assert nxt.find_alternating_4_cycle(G) == [1, 2, 3, 6]
+
+        # Threshold graph
+        TG = nxt.find_threshold_graph(G)
+        assert nxt.is_threshold_graph(TG)
+        assert sorted(TG.nodes()) == [1, 2, 3, 4, 5, 7]
+
+        cs = nxt.creation_sequence(dict(TG.degree()), with_labels=True)
+        assert nxt.find_creation_sequence(G) == cs
+
+    def test_fast_versions_properties_threshold_graphs(self):
+        cs = "ddiiddid"
+        G = nxt.threshold_graph(cs)
+        assert nxt.density("ddiiddid") == nx.density(G)
+        assert sorted(nxt.degree_sequence(cs)) == sorted(d for n, d in G.degree())
+
+        ts = nxt.triangle_sequence(cs)
+        assert ts == list(nx.triangles(G).values())
+        assert sum(ts) // 3 == nxt.triangles(cs)
+
+        c1 = nxt.cluster_sequence(cs)
+        c2 = list(nx.clustering(G).values())
+        assert sum(abs(c - d) for c, d in zip(c1, c2)) == pytest.approx(0, abs=1e-7)
+
+        b1 = nx.betweenness_centrality(G).values()
+        b2 = nxt.betweenness_sequence(cs)
+        assert sum(abs(c - d) for c, d in zip(b1, b2)) < 1e-7
+
+        assert nxt.eigenvalues(cs) == [0, 1, 3, 3, 5, 7, 7, 8]
+
+        # Degree Correlation
+        assert abs(nxt.degree_correlation(cs) + 0.593038821954) < 1e-12
+        assert nxt.degree_correlation("diiiddi") == -0.8
+        assert nxt.degree_correlation("did") == -1.0
+        assert nxt.degree_correlation("ddd") == 1.0
+        assert nxt.eigenvalues("dddiii") == [0, 0, 0, 0, 3, 3]
+        assert nxt.eigenvalues("dddiiid") == [0, 1, 1, 1, 4, 4, 7]
+
+    def test_tg_creation_routines(self):
+        s = nxt.left_d_threshold_sequence(5, 7)
+        s = nxt.right_d_threshold_sequence(5, 7)
+        s1 = nxt.swap_d(s, 1.0, 1.0)
+        s1 = nxt.swap_d(s, 1.0, 1.0, seed=1)
+
+    def test_eigenvectors(self):
+        np = pytest.importorskip("numpy")
+        eigenval = np.linalg.eigvals
+        pytest.importorskip("scipy")
+
+        cs = "ddiiddid"
+        G = nxt.threshold_graph(cs)
+        (tgeval, tgevec) = nxt.eigenvectors(cs)
+        np.testing.assert_allclose([np.dot(lv, lv) for lv in tgevec], 1.0, rtol=1e-9)
+        lapl = nx.laplacian_matrix(G)
+
+    def test_create_using(self):
+        cs = "ddiiddid"
+        G = nxt.threshold_graph(cs)
+        assert pytest.raises(
+            nx.exception.NetworkXError,
+            nxt.threshold_graph,
+            cs,
+            create_using=nx.DiGraph(),
+        )
+        MG = nxt.threshold_graph(cs, create_using=nx.MultiGraph())
+        assert sorted(MG.edges()) == sorted(G.edges())
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_time_dependent.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_time_dependent.py
new file mode 100644
index 00000000..1e256f4b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_time_dependent.py
@@ -0,0 +1,431 @@
+"""Unit testing for time dependent algorithms."""
+
+from datetime import datetime, timedelta
+
+import pytest
+
+import networkx as nx
+
+_delta = timedelta(days=5 * 365)
+
+
+class TestCdIndex:
+    """Unit testing for the cd index function."""
+
+    def test_common_graph(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(4, 2)
+        G.add_edge(4, 0)
+        G.add_edge(4, 1)
+        G.add_edge(4, 3)
+        G.add_edge(5, 2)
+        G.add_edge(6, 2)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(9, 3)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": datetime(1992, 1, 1)},
+            1: {"time": datetime(1992, 1, 1)},
+            2: {"time": datetime(1993, 1, 1)},
+            3: {"time": datetime(1993, 1, 1)},
+            4: {"time": datetime(1995, 1, 1)},
+            5: {"time": datetime(1997, 1, 1)},
+            6: {"time": datetime(1998, 1, 1)},
+            7: {"time": datetime(1999, 1, 1)},
+            8: {"time": datetime(1999, 1, 1)},
+            9: {"time": datetime(1998, 1, 1)},
+            10: {"time": datetime(1997, 4, 1)},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        assert nx.cd_index(G, 4, time_delta=_delta) == 0.17
+
+    def test_common_graph_with_given_attributes(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(4, 2)
+        G.add_edge(4, 0)
+        G.add_edge(4, 1)
+        G.add_edge(4, 3)
+        G.add_edge(5, 2)
+        G.add_edge(6, 2)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(9, 3)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"date": datetime(1992, 1, 1)},
+            1: {"date": datetime(1992, 1, 1)},
+            2: {"date": datetime(1993, 1, 1)},
+            3: {"date": datetime(1993, 1, 1)},
+            4: {"date": datetime(1995, 1, 1)},
+            5: {"date": datetime(1997, 1, 1)},
+            6: {"date": datetime(1998, 1, 1)},
+            7: {"date": datetime(1999, 1, 1)},
+            8: {"date": datetime(1999, 1, 1)},
+            9: {"date": datetime(1998, 1, 1)},
+            10: {"date": datetime(1997, 4, 1)},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        assert nx.cd_index(G, 4, time_delta=_delta, time="date") == 0.17
+
+    def test_common_graph_with_int_attributes(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(4, 2)
+        G.add_edge(4, 0)
+        G.add_edge(4, 1)
+        G.add_edge(4, 3)
+        G.add_edge(5, 2)
+        G.add_edge(6, 2)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(9, 3)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": 20},
+            1: {"time": 20},
+            2: {"time": 30},
+            3: {"time": 30},
+            4: {"time": 50},
+            5: {"time": 70},
+            6: {"time": 80},
+            7: {"time": 90},
+            8: {"time": 90},
+            9: {"time": 80},
+            10: {"time": 74},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        assert nx.cd_index(G, 4, time_delta=50) == 0.17
+
+    def test_common_graph_with_float_attributes(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(4, 2)
+        G.add_edge(4, 0)
+        G.add_edge(4, 1)
+        G.add_edge(4, 3)
+        G.add_edge(5, 2)
+        G.add_edge(6, 2)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(9, 3)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": 20.2},
+            1: {"time": 20.2},
+            2: {"time": 30.7},
+            3: {"time": 30.7},
+            4: {"time": 50.9},
+            5: {"time": 70.1},
+            6: {"time": 80.6},
+            7: {"time": 90.7},
+            8: {"time": 90.7},
+            9: {"time": 80.6},
+            10: {"time": 74.2},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        assert nx.cd_index(G, 4, time_delta=50) == 0.17
+
+    def test_common_graph_with_weights(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(4, 2)
+        G.add_edge(4, 0)
+        G.add_edge(4, 1)
+        G.add_edge(4, 3)
+        G.add_edge(5, 2)
+        G.add_edge(6, 2)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(9, 3)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": datetime(1992, 1, 1)},
+            1: {"time": datetime(1992, 1, 1)},
+            2: {"time": datetime(1993, 1, 1)},
+            3: {"time": datetime(1993, 1, 1)},
+            4: {"time": datetime(1995, 1, 1)},
+            5: {"time": datetime(1997, 1, 1)},
+            6: {"time": datetime(1998, 1, 1), "weight": 5},
+            7: {"time": datetime(1999, 1, 1), "weight": 2},
+            8: {"time": datetime(1999, 1, 1), "weight": 6},
+            9: {"time": datetime(1998, 1, 1), "weight": 3},
+            10: {"time": datetime(1997, 4, 1), "weight": 10},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+        assert nx.cd_index(G, 4, time_delta=_delta, weight="weight") == 0.04
+
+    def test_node_with_no_predecessors(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(4, 2)
+        G.add_edge(4, 0)
+        G.add_edge(4, 3)
+        G.add_edge(5, 2)
+        G.add_edge(6, 2)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(9, 3)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": datetime(1992, 1, 1)},
+            1: {"time": datetime(1992, 1, 1)},
+            2: {"time": datetime(1993, 1, 1)},
+            3: {"time": datetime(1993, 1, 1)},
+            4: {"time": datetime(1995, 1, 1)},
+            5: {"time": datetime(2005, 1, 1)},
+            6: {"time": datetime(2010, 1, 1)},
+            7: {"time": datetime(2001, 1, 1)},
+            8: {"time": datetime(2020, 1, 1)},
+            9: {"time": datetime(2017, 1, 1)},
+            10: {"time": datetime(2004, 4, 1)},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+        assert nx.cd_index(G, 4, time_delta=_delta) == 0.0
+
+    def test_node_with_no_successors(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(8, 2)
+        G.add_edge(6, 0)
+        G.add_edge(6, 3)
+        G.add_edge(5, 2)
+        G.add_edge(6, 2)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(9, 3)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": datetime(1992, 1, 1)},
+            1: {"time": datetime(1992, 1, 1)},
+            2: {"time": datetime(1993, 1, 1)},
+            3: {"time": datetime(1993, 1, 1)},
+            4: {"time": datetime(1995, 1, 1)},
+            5: {"time": datetime(1997, 1, 1)},
+            6: {"time": datetime(1998, 1, 1)},
+            7: {"time": datetime(1999, 1, 1)},
+            8: {"time": datetime(1999, 1, 1)},
+            9: {"time": datetime(1998, 1, 1)},
+            10: {"time": datetime(1997, 4, 1)},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+        assert nx.cd_index(G, 4, time_delta=_delta) == 1.0
+
+    def test_n_equals_zero(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(4, 2)
+        G.add_edge(4, 0)
+        G.add_edge(4, 3)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": datetime(1992, 1, 1)},
+            1: {"time": datetime(1992, 1, 1)},
+            2: {"time": datetime(1993, 1, 1)},
+            3: {"time": datetime(1993, 1, 1)},
+            4: {"time": datetime(1995, 1, 1)},
+            5: {"time": datetime(2005, 1, 1)},
+            6: {"time": datetime(2010, 1, 1)},
+            7: {"time": datetime(2001, 1, 1)},
+            8: {"time": datetime(2020, 1, 1)},
+            9: {"time": datetime(2017, 1, 1)},
+            10: {"time": datetime(2004, 4, 1)},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        with pytest.raises(
+            nx.NetworkXError, match="The cd index cannot be defined."
+        ) as ve:
+            nx.cd_index(G, 4, time_delta=_delta)
+
+    def test_time_timedelta_compatibility(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(4, 2)
+        G.add_edge(4, 0)
+        G.add_edge(4, 3)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": 20.2},
+            1: {"time": 20.2},
+            2: {"time": 30.7},
+            3: {"time": 30.7},
+            4: {"time": 50.9},
+            5: {"time": 70.1},
+            6: {"time": 80.6},
+            7: {"time": 90.7},
+            8: {"time": 90.7},
+            9: {"time": 80.6},
+            10: {"time": 74.2},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        with pytest.raises(
+            nx.NetworkXError,
+            match="Addition and comparison are not supported between",
+        ) as ve:
+            nx.cd_index(G, 4, time_delta=_delta)
+
+    def test_node_with_no_time(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+        G.add_edge(8, 2)
+        G.add_edge(6, 0)
+        G.add_edge(6, 3)
+        G.add_edge(5, 2)
+        G.add_edge(6, 2)
+        G.add_edge(6, 4)
+        G.add_edge(7, 4)
+        G.add_edge(8, 4)
+        G.add_edge(9, 4)
+        G.add_edge(9, 1)
+        G.add_edge(9, 3)
+        G.add_edge(10, 4)
+
+        node_attrs = {
+            0: {"time": datetime(1992, 1, 1)},
+            1: {"time": datetime(1992, 1, 1)},
+            2: {"time": datetime(1993, 1, 1)},
+            3: {"time": datetime(1993, 1, 1)},
+            4: {"time": datetime(1995, 1, 1)},
+            6: {"time": datetime(1998, 1, 1)},
+            7: {"time": datetime(1999, 1, 1)},
+            8: {"time": datetime(1999, 1, 1)},
+            9: {"time": datetime(1998, 1, 1)},
+            10: {"time": datetime(1997, 4, 1)},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        with pytest.raises(
+            nx.NetworkXError, match="Not all nodes have a 'time' attribute."
+        ) as ve:
+            nx.cd_index(G, 4, time_delta=_delta)
+
+    def test_maximally_consolidating(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
+        G.add_edge(5, 1)
+        G.add_edge(5, 2)
+        G.add_edge(5, 3)
+        G.add_edge(5, 4)
+        G.add_edge(6, 1)
+        G.add_edge(6, 5)
+        G.add_edge(7, 1)
+        G.add_edge(7, 5)
+        G.add_edge(8, 2)
+        G.add_edge(8, 5)
+        G.add_edge(9, 5)
+        G.add_edge(9, 3)
+        G.add_edge(10, 5)
+        G.add_edge(10, 3)
+        G.add_edge(10, 4)
+        G.add_edge(11, 5)
+        G.add_edge(11, 4)
+
+        node_attrs = {
+            0: {"time": datetime(1992, 1, 1)},
+            1: {"time": datetime(1992, 1, 1)},
+            2: {"time": datetime(1993, 1, 1)},
+            3: {"time": datetime(1993, 1, 1)},
+            4: {"time": datetime(1995, 1, 1)},
+            5: {"time": datetime(1997, 1, 1)},
+            6: {"time": datetime(1998, 1, 1)},
+            7: {"time": datetime(1999, 1, 1)},
+            8: {"time": datetime(1999, 1, 1)},
+            9: {"time": datetime(1998, 1, 1)},
+            10: {"time": datetime(1997, 4, 1)},
+            11: {"time": datetime(1998, 5, 1)},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        assert nx.cd_index(G, 5, time_delta=_delta) == -1
+
+    def test_maximally_destabilizing(self):
+        G = nx.DiGraph()
+        G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
+        G.add_edge(5, 1)
+        G.add_edge(5, 2)
+        G.add_edge(5, 3)
+        G.add_edge(5, 4)
+        G.add_edge(6, 5)
+        G.add_edge(7, 5)
+        G.add_edge(8, 5)
+        G.add_edge(9, 5)
+        G.add_edge(10, 5)
+        G.add_edge(11, 5)
+
+        node_attrs = {
+            0: {"time": datetime(1992, 1, 1)},
+            1: {"time": datetime(1992, 1, 1)},
+            2: {"time": datetime(1993, 1, 1)},
+            3: {"time": datetime(1993, 1, 1)},
+            4: {"time": datetime(1995, 1, 1)},
+            5: {"time": datetime(1997, 1, 1)},
+            6: {"time": datetime(1998, 1, 1)},
+            7: {"time": datetime(1999, 1, 1)},
+            8: {"time": datetime(1999, 1, 1)},
+            9: {"time": datetime(1998, 1, 1)},
+            10: {"time": datetime(1997, 4, 1)},
+            11: {"time": datetime(1998, 5, 1)},
+        }
+
+        nx.set_node_attributes(G, node_attrs)
+
+        assert nx.cd_index(G, 5, time_delta=_delta) == 1
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_tournament.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_tournament.py
new file mode 100644
index 00000000..e75abf8f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_tournament.py
@@ -0,0 +1,163 @@
+"""Unit tests for the :mod:`networkx.algorithms.tournament` module."""
+
+from itertools import combinations
+
+import pytest
+
+from networkx import DiGraph
+from networkx.algorithms.tournament import (
+    hamiltonian_path,
+    index_satisfying,
+    is_reachable,
+    is_strongly_connected,
+    is_tournament,
+    random_tournament,
+    score_sequence,
+    tournament_matrix,
+)
+
+
+def test_condition_not_satisfied():
+    condition = lambda x: x > 0
+    iter_in = [0]
+    assert index_satisfying(iter_in, condition) == 1
+
+
+def test_empty_iterable():
+    condition = lambda x: x > 0
+    with pytest.raises(ValueError):
+        index_satisfying([], condition)
+
+
+def test_is_tournament():
+    G = DiGraph()
+    G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)])
+    assert is_tournament(G)
+
+
+def test_self_loops():
+    """A tournament must have no self-loops."""
+    G = DiGraph()
+    G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)])
+    G.add_edge(0, 0)
+    assert not is_tournament(G)
+
+
+def test_missing_edges():
+    """A tournament must not have any pair of nodes without at least
+    one edge joining the pair.
+
+    """
+    G = DiGraph()
+    G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3)])
+    assert not is_tournament(G)
+
+
+def test_bidirectional_edges():
+    """A tournament must not have any pair of nodes with greater
+    than one edge joining the pair.
+
+    """
+    G = DiGraph()
+    G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)])
+    G.add_edge(1, 0)
+    assert not is_tournament(G)
+
+
+def test_graph_is_tournament():
+    for _ in range(10):
+        G = random_tournament(5)
+        assert is_tournament(G)
+
+
+def test_graph_is_tournament_seed():
+    for _ in range(10):
+        G = random_tournament(5, seed=1)
+        assert is_tournament(G)
+
+
+def test_graph_is_tournament_one_node():
+    G = random_tournament(1)
+    assert is_tournament(G)
+
+
+def test_graph_is_tournament_zero_node():
+    G = random_tournament(0)
+    assert is_tournament(G)
+
+
+def test_hamiltonian_empty_graph():
+    path = hamiltonian_path(DiGraph())
+    assert len(path) == 0
+
+
+def test_path_is_hamiltonian():
+    G = DiGraph()
+    G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)])
+    path = hamiltonian_path(G)
+    assert len(path) == 4
+    assert all(v in G[u] for u, v in zip(path, path[1:]))
+
+
+def test_hamiltonian_cycle():
+    """Tests that :func:`networkx.tournament.hamiltonian_path`
+    returns a Hamiltonian cycle when provided a strongly connected
+    tournament.
+
+    """
+    G = DiGraph()
+    G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)])
+    path = hamiltonian_path(G)
+    assert len(path) == 4
+    assert all(v in G[u] for u, v in zip(path, path[1:]))
+    assert path[0] in G[path[-1]]
+
+
+def test_score_sequence_edge():
+    G = DiGraph([(0, 1)])
+    assert score_sequence(G) == [0, 1]
+
+
+def test_score_sequence_triangle():
+    G = DiGraph([(0, 1), (1, 2), (2, 0)])
+    assert score_sequence(G) == [1, 1, 1]
+
+
+def test_tournament_matrix():
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+    npt = np.testing
+    G = DiGraph([(0, 1)])
+    m = tournament_matrix(G)
+    npt.assert_array_equal(m.todense(), np.array([[0, 1], [-1, 0]]))
+
+
+def test_reachable_pair():
+    """Tests for a reachable pair of nodes."""
+    G = DiGraph([(0, 1), (1, 2), (2, 0)])
+    assert is_reachable(G, 0, 2)
+
+
+def test_same_node_is_reachable():
+    """Tests that a node is always reachable from it."""
+    # G is an arbitrary tournament on ten nodes.
+    G = DiGraph(sorted(p) for p in combinations(range(10), 2))
+    assert all(is_reachable(G, v, v) for v in G)
+
+
+def test_unreachable_pair():
+    """Tests for an unreachable pair of nodes."""
+    G = DiGraph([(0, 1), (0, 2), (1, 2)])
+    assert not is_reachable(G, 1, 0)
+
+
+def test_is_strongly_connected():
+    """Tests for a strongly connected tournament."""
+    G = DiGraph([(0, 1), (1, 2), (2, 0)])
+    assert is_strongly_connected(G)
+
+
+def test_not_strongly_connected():
+    """Tests for a tournament that is not strongly connected."""
+    G = DiGraph([(0, 1), (0, 2), (1, 2)])
+    assert not is_strongly_connected(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_triads.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_triads.py
new file mode 100644
index 00000000..62670351
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_triads.py
@@ -0,0 +1,289 @@
+"""Tests for the :mod:`networkx.algorithms.triads` module."""
+
+import itertools
+from collections import defaultdict
+from random import sample
+
+import pytest
+
+import networkx as nx
+
+
+def test_all_triplets_deprecated():
+    G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+    with pytest.deprecated_call():
+        nx.all_triplets(G)
+
+
+def test_random_triad_deprecated():
+    G = nx.path_graph(3, create_using=nx.DiGraph)
+    with pytest.deprecated_call():
+        nx.random_triad(G)
+
+
+def test_triadic_census():
+    """Tests the triadic_census function."""
+    G = nx.DiGraph()
+    G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"])
+    expected = {
+        "030T": 2,
+        "120C": 1,
+        "210": 0,
+        "120U": 0,
+        "012": 9,
+        "102": 3,
+        "021U": 0,
+        "111U": 0,
+        "003": 8,
+        "030C": 0,
+        "021D": 9,
+        "201": 0,
+        "111D": 1,
+        "300": 0,
+        "120D": 0,
+        "021C": 2,
+    }
+    actual = nx.triadic_census(G)
+    assert expected == actual
+
+
+def test_is_triad():
+    """Tests the is_triad function"""
+    G = nx.karate_club_graph()
+    G = G.to_directed()
+    for i in range(100):
+        nodes = sample(sorted(G.nodes()), 3)
+        G2 = G.subgraph(nodes)
+        assert nx.is_triad(G2)
+
+
+def test_all_triplets():
+    """Tests the all_triplets function."""
+    G = nx.DiGraph()
+    G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"])
+    expected = [
+        f"{i},{j},{k}"
+        for i in range(7)
+        for j in range(i + 1, 7)
+        for k in range(j + 1, 7)
+    ]
+    expected = [set(x.split(",")) for x in expected]
+    actual = [set(x) for x in nx.all_triplets(G)]
+    assert all(any(s1 == s2 for s1 in expected) for s2 in actual)
+
+
+def test_all_triads():
+    """Tests the all_triplets function."""
+    G = nx.DiGraph()
+    G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"])
+    expected = [
+        f"{i},{j},{k}"
+        for i in range(7)
+        for j in range(i + 1, 7)
+        for k in range(j + 1, 7)
+    ]
+    expected = [G.subgraph(x.split(",")) for x in expected]
+    actual = list(nx.all_triads(G))
+    assert all(any(nx.is_isomorphic(G1, G2) for G1 in expected) for G2 in actual)
+
+
+def test_triad_type():
+    """Tests the triad_type function."""
+    # 0 edges (1 type)
+    G = nx.DiGraph({0: [], 1: [], 2: []})
+    assert nx.triad_type(G) == "003"
+    # 1 edge (1 type)
+    G = nx.DiGraph({0: [1], 1: [], 2: []})
+    assert nx.triad_type(G) == "012"
+    # 2 edges (4 types)
+    G = nx.DiGraph([(0, 1), (0, 2)])
+    assert nx.triad_type(G) == "021D"
+    G = nx.DiGraph({0: [1], 1: [0], 2: []})
+    assert nx.triad_type(G) == "102"
+    G = nx.DiGraph([(0, 1), (2, 1)])
+    assert nx.triad_type(G) == "021U"
+    G = nx.DiGraph([(0, 1), (1, 2)])
+    assert nx.triad_type(G) == "021C"
+    # 3 edges (4 types)
+    G = nx.DiGraph([(0, 1), (1, 0), (2, 1)])
+    assert nx.triad_type(G) == "111D"
+    G = nx.DiGraph([(0, 1), (1, 0), (1, 2)])
+    assert nx.triad_type(G) == "111U"
+    G = nx.DiGraph([(0, 1), (1, 2), (0, 2)])
+    assert nx.triad_type(G) == "030T"
+    G = nx.DiGraph([(0, 1), (1, 2), (2, 0)])
+    assert nx.triad_type(G) == "030C"
+    # 4 edges (4 types)
+    G = nx.DiGraph([(0, 1), (1, 0), (2, 0), (0, 2)])
+    assert nx.triad_type(G) == "201"
+    G = nx.DiGraph([(0, 1), (1, 0), (2, 0), (2, 1)])
+    assert nx.triad_type(G) == "120D"
+    G = nx.DiGraph([(0, 1), (1, 0), (0, 2), (1, 2)])
+    assert nx.triad_type(G) == "120U"
+    G = nx.DiGraph([(0, 1), (1, 0), (0, 2), (2, 1)])
+    assert nx.triad_type(G) == "120C"
+    # 5 edges (1 type)
+    G = nx.DiGraph([(0, 1), (1, 0), (2, 1), (1, 2), (0, 2)])
+    assert nx.triad_type(G) == "210"
+    # 6 edges (1 type)
+    G = nx.DiGraph([(0, 1), (1, 0), (1, 2), (2, 1), (0, 2), (2, 0)])
+    assert nx.triad_type(G) == "300"
+
+
+def test_triads_by_type():
+    """Tests the all_triplets function."""
+    G = nx.DiGraph()
+    G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"])
+    all_triads = nx.all_triads(G)
+    expected = defaultdict(list)
+    for triad in all_triads:
+        name = nx.triad_type(triad)
+        expected[name].append(triad)
+    actual = nx.triads_by_type(G)
+    assert set(actual.keys()) == set(expected.keys())
+    for tri_type, actual_Gs in actual.items():
+        expected_Gs = expected[tri_type]
+        for a in actual_Gs:
+            assert any(nx.is_isomorphic(a, e) for e in expected_Gs)
+
+
+def test_random_triad():
+    """Tests the random_triad function"""
+    G = nx.karate_club_graph()
+    G = G.to_directed()
+    for i in range(100):
+        assert nx.is_triad(nx.random_triad(G))
+
+    G = nx.DiGraph()
+    msg = "at least 3 nodes to form a triad"
+    with pytest.raises(nx.NetworkXError, match=msg):
+        nx.random_triad(G)
+
+
+def test_triadic_census_short_path_nodelist():
+    G = nx.path_graph("abc", create_using=nx.DiGraph)
+    expected = {"021C": 1}
+    for nl in ["a", "b", "c", "ab", "ac", "bc", "abc"]:
+        triad_census = nx.triadic_census(G, nodelist=nl)
+        assert expected == {typ: cnt for typ, cnt in triad_census.items() if cnt > 0}
+
+
+def test_triadic_census_correct_nodelist_values():
+    G = nx.path_graph(5, create_using=nx.DiGraph)
+    msg = r"nodelist includes duplicate nodes or nodes not in G"
+    with pytest.raises(ValueError, match=msg):
+        nx.triadic_census(G, [1, 2, 2, 3])
+    with pytest.raises(ValueError, match=msg):
+        nx.triadic_census(G, [1, 2, "a", 3])
+
+
+def test_triadic_census_tiny_graphs():
+    tc = nx.triadic_census(nx.empty_graph(0, create_using=nx.DiGraph))
+    assert {} == {typ: cnt for typ, cnt in tc.items() if cnt > 0}
+    tc = nx.triadic_census(nx.empty_graph(1, create_using=nx.DiGraph))
+    assert {} == {typ: cnt for typ, cnt in tc.items() if cnt > 0}
+    tc = nx.triadic_census(nx.empty_graph(2, create_using=nx.DiGraph))
+    assert {} == {typ: cnt for typ, cnt in tc.items() if cnt > 0}
+    tc = nx.triadic_census(nx.DiGraph([(1, 2)]))
+    assert {} == {typ: cnt for typ, cnt in tc.items() if cnt > 0}
+
+
+def test_triadic_census_selfloops():
+    GG = nx.path_graph("abc", create_using=nx.DiGraph)
+    expected = {"021C": 1}
+    for n in GG:
+        G = GG.copy()
+        G.add_edge(n, n)
+        tc = nx.triadic_census(G)
+        assert expected == {typ: cnt for typ, cnt in tc.items() if cnt > 0}
+
+    GG = nx.path_graph("abcde", create_using=nx.DiGraph)
+    tbt = nx.triads_by_type(GG)
+    for n in GG:
+        GG.add_edge(n, n)
+    tc = nx.triadic_census(GG)
+    assert tc == {tt: len(tbt[tt]) for tt in tc}
+
+
+def test_triadic_census_four_path():
+    G = nx.path_graph("abcd", create_using=nx.DiGraph)
+    expected = {"012": 2, "021C": 2}
+    triad_census = nx.triadic_census(G)
+    assert expected == {typ: cnt for typ, cnt in triad_census.items() if cnt > 0}
+
+
+def test_triadic_census_four_path_nodelist():
+    G = nx.path_graph("abcd", create_using=nx.DiGraph)
+    expected_end = {"012": 2, "021C": 1}
+    expected_mid = {"012": 1, "021C": 2}
+    a_triad_census = nx.triadic_census(G, nodelist=["a"])
+    assert expected_end == {typ: cnt for typ, cnt in a_triad_census.items() if cnt > 0}
+    b_triad_census = nx.triadic_census(G, nodelist=["b"])
+    assert expected_mid == {typ: cnt for typ, cnt in b_triad_census.items() if cnt > 0}
+    c_triad_census = nx.triadic_census(G, nodelist=["c"])
+    assert expected_mid == {typ: cnt for typ, cnt in c_triad_census.items() if cnt > 0}
+    d_triad_census = nx.triadic_census(G, nodelist=["d"])
+    assert expected_end == {typ: cnt for typ, cnt in d_triad_census.items() if cnt > 0}
+
+
+def test_triadic_census_nodelist():
+    """Tests the triadic_census function."""
+    G = nx.DiGraph()
+    G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"])
+    expected = {
+        "030T": 2,
+        "120C": 1,
+        "210": 0,
+        "120U": 0,
+        "012": 9,
+        "102": 3,
+        "021U": 0,
+        "111U": 0,
+        "003": 8,
+        "030C": 0,
+        "021D": 9,
+        "201": 0,
+        "111D": 1,
+        "300": 0,
+        "120D": 0,
+        "021C": 2,
+    }
+    actual = {k: 0 for k in expected}
+    for node in G.nodes():
+        node_triad_census = nx.triadic_census(G, nodelist=[node])
+        for triad_key in expected:
+            actual[triad_key] += node_triad_census[triad_key]
+    # Divide all counts by 3
+    for k, v in actual.items():
+        actual[k] //= 3
+    assert expected == actual
+
+
+@pytest.mark.parametrize("N", [5, 10])
+def test_triadic_census_on_random_graph(N):
+    G = nx.binomial_graph(N, 0.3, directed=True, seed=42)
+    tc1 = nx.triadic_census(G)
+    tbt = nx.triads_by_type(G)
+    tc2 = {tt: len(tbt[tt]) for tt in tc1}
+    assert tc1 == tc2
+
+    for n in G:
+        tc1 = nx.triadic_census(G, nodelist={n})
+        tc2 = {tt: sum(1 for t in tbt.get(tt, []) if n in t) for tt in tc1}
+        assert tc1 == tc2
+
+    for ns in itertools.combinations(G, 2):
+        ns = set(ns)
+        tc1 = nx.triadic_census(G, nodelist=ns)
+        tc2 = {
+            tt: sum(1 for t in tbt.get(tt, []) if any(n in ns for n in t)) for tt in tc1
+        }
+        assert tc1 == tc2
+
+    for ns in itertools.combinations(G, 3):
+        ns = set(ns)
+        tc1 = nx.triadic_census(G, nodelist=ns)
+        tc2 = {
+            tt: sum(1 for t in tbt.get(tt, []) if any(n in ns for n in t)) for tt in tc1
+        }
+        assert tc1 == tc2
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_vitality.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_vitality.py
new file mode 100644
index 00000000..248206e6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_vitality.py
@@ -0,0 +1,41 @@
+import networkx as nx
+
+
+class TestClosenessVitality:
+    def test_unweighted(self):
+        G = nx.cycle_graph(3)
+        vitality = nx.closeness_vitality(G)
+        assert vitality == {0: 2, 1: 2, 2: 2}
+
+    def test_weighted(self):
+        G = nx.Graph()
+        nx.add_cycle(G, [0, 1, 2], weight=2)
+        vitality = nx.closeness_vitality(G, weight="weight")
+        assert vitality == {0: 4, 1: 4, 2: 4}
+
+    def test_unweighted_digraph(self):
+        G = nx.DiGraph(nx.cycle_graph(3))
+        vitality = nx.closeness_vitality(G)
+        assert vitality == {0: 4, 1: 4, 2: 4}
+
+    def test_weighted_digraph(self):
+        G = nx.DiGraph()
+        nx.add_cycle(G, [0, 1, 2], weight=2)
+        nx.add_cycle(G, [2, 1, 0], weight=2)
+        vitality = nx.closeness_vitality(G, weight="weight")
+        assert vitality == {0: 8, 1: 8, 2: 8}
+
+    def test_weighted_multidigraph(self):
+        G = nx.MultiDiGraph()
+        nx.add_cycle(G, [0, 1, 2], weight=2)
+        nx.add_cycle(G, [2, 1, 0], weight=2)
+        vitality = nx.closeness_vitality(G, weight="weight")
+        assert vitality == {0: 8, 1: 8, 2: 8}
+
+    def test_disconnecting_graph(self):
+        """Tests that the closeness vitality of a node whose removal
+        disconnects the graph is negative infinity.
+
+        """
+        G = nx.path_graph(3)
+        assert nx.closeness_vitality(G, node=1) == -float("inf")
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_voronoi.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_voronoi.py
new file mode 100644
index 00000000..3269ae62
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_voronoi.py
@@ -0,0 +1,103 @@
+import networkx as nx
+from networkx.utils import pairwise
+
+
+class TestVoronoiCells:
+    """Unit tests for the Voronoi cells function."""
+
+    def test_isolates(self):
+        """Tests that a graph with isolated nodes has all isolates in
+        one block of the partition.
+
+        """
+        G = nx.empty_graph(5)
+        cells = nx.voronoi_cells(G, {0, 2, 4})
+        expected = {0: {0}, 2: {2}, 4: {4}, "unreachable": {1, 3}}
+        assert expected == cells
+
+    def test_undirected_unweighted(self):
+        G = nx.cycle_graph(6)
+        cells = nx.voronoi_cells(G, {0, 3})
+        expected = {0: {0, 1, 5}, 3: {2, 3, 4}}
+        assert expected == cells
+
+    def test_directed_unweighted(self):
+        # This is the singly-linked directed cycle graph on six nodes.
+        G = nx.DiGraph(pairwise(range(6), cyclic=True))
+        cells = nx.voronoi_cells(G, {0, 3})
+        expected = {0: {0, 1, 2}, 3: {3, 4, 5}}
+        assert expected == cells
+
+    def test_directed_inward(self):
+        """Tests that reversing the graph gives the "inward" Voronoi
+        partition.
+
+        """
+        # This is the singly-linked reverse directed cycle graph on six nodes.
+        G = nx.DiGraph(pairwise(range(6), cyclic=True))
+        G = G.reverse(copy=False)
+        cells = nx.voronoi_cells(G, {0, 3})
+        expected = {0: {0, 4, 5}, 3: {1, 2, 3}}
+        assert expected == cells
+
+    def test_undirected_weighted(self):
+        edges = [(0, 1, 10), (1, 2, 1), (2, 3, 1)]
+        G = nx.Graph()
+        G.add_weighted_edges_from(edges)
+        cells = nx.voronoi_cells(G, {0, 3})
+        expected = {0: {0}, 3: {1, 2, 3}}
+        assert expected == cells
+
+    def test_directed_weighted(self):
+        edges = [(0, 1, 10), (1, 2, 1), (2, 3, 1), (3, 2, 1), (2, 1, 1)]
+        G = nx.DiGraph()
+        G.add_weighted_edges_from(edges)
+        cells = nx.voronoi_cells(G, {0, 3})
+        expected = {0: {0}, 3: {1, 2, 3}}
+        assert expected == cells
+
+    def test_multigraph_unweighted(self):
+        """Tests that the Voronoi cells for a multigraph are the same as
+        for a simple graph.
+
+        """
+        edges = [(0, 1), (1, 2), (2, 3)]
+        G = nx.MultiGraph(2 * edges)
+        H = nx.Graph(G)
+        G_cells = nx.voronoi_cells(G, {0, 3})
+        H_cells = nx.voronoi_cells(H, {0, 3})
+        assert G_cells == H_cells
+
+    def test_multidigraph_unweighted(self):
+        # This is the twice-singly-linked directed cycle graph on six nodes.
+        edges = list(pairwise(range(6), cyclic=True))
+        G = nx.MultiDiGraph(2 * edges)
+        H = nx.DiGraph(G)
+        G_cells = nx.voronoi_cells(G, {0, 3})
+        H_cells = nx.voronoi_cells(H, {0, 3})
+        assert G_cells == H_cells
+
+    def test_multigraph_weighted(self):
+        edges = [(0, 1, 10), (0, 1, 10), (1, 2, 1), (1, 2, 100), (2, 3, 1), (2, 3, 100)]
+        G = nx.MultiGraph()
+        G.add_weighted_edges_from(edges)
+        cells = nx.voronoi_cells(G, {0, 3})
+        expected = {0: {0}, 3: {1, 2, 3}}
+        assert expected == cells
+
+    def test_multidigraph_weighted(self):
+        edges = [
+            (0, 1, 10),
+            (0, 1, 10),
+            (1, 2, 1),
+            (2, 3, 1),
+            (3, 2, 10),
+            (3, 2, 1),
+            (2, 1, 10),
+            (2, 1, 1),
+        ]
+        G = nx.MultiDiGraph()
+        G.add_weighted_edges_from(edges)
+        cells = nx.voronoi_cells(G, {0, 3})
+        expected = {0: {0}, 3: {1, 2, 3}}
+        assert expected == cells
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_walks.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_walks.py
new file mode 100644
index 00000000..7a6b3239
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_walks.py
@@ -0,0 +1,54 @@
+"""Unit tests for the :mod:`networkx.algorithms.walks` module."""
+
+import pytest
+
+import networkx as nx
+
+pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+
+def test_directed():
+    G = nx.DiGraph([(0, 1), (1, 2), (2, 0)])
+    num_walks = nx.number_of_walks(G, 3)
+    expected = {0: {0: 1, 1: 0, 2: 0}, 1: {0: 0, 1: 1, 2: 0}, 2: {0: 0, 1: 0, 2: 1}}
+    assert num_walks == expected
+
+
+def test_undirected():
+    G = nx.cycle_graph(3)
+    num_walks = nx.number_of_walks(G, 3)
+    expected = {0: {0: 2, 1: 3, 2: 3}, 1: {0: 3, 1: 2, 2: 3}, 2: {0: 3, 1: 3, 2: 2}}
+    assert num_walks == expected
+
+
+def test_non_integer_nodes():
+    G = nx.DiGraph([("A", "B"), ("B", "C"), ("C", "A")])
+    num_walks = nx.number_of_walks(G, 2)
+    expected = {
+        "A": {"A": 0, "B": 0, "C": 1},
+        "B": {"A": 1, "B": 0, "C": 0},
+        "C": {"A": 0, "B": 1, "C": 0},
+    }
+    assert num_walks == expected
+
+
+def test_zero_length():
+    G = nx.cycle_graph(3)
+    num_walks = nx.number_of_walks(G, 0)
+    expected = {0: {0: 1, 1: 0, 2: 0}, 1: {0: 0, 1: 1, 2: 0}, 2: {0: 0, 1: 0, 2: 1}}
+    assert num_walks == expected
+
+
+def test_negative_length_exception():
+    G = nx.cycle_graph(3)
+    with pytest.raises(ValueError):
+        nx.number_of_walks(G, -1)
+
+
+def test_hidden_weight_attr():
+    G = nx.cycle_graph(3)
+    G.add_edge(1, 2, weight=5)
+    num_walks = nx.number_of_walks(G, 3)
+    expected = {0: {0: 2, 1: 3, 2: 3}, 1: {0: 3, 1: 2, 2: 3}, 2: {0: 3, 1: 3, 2: 2}}
+    assert num_walks == expected
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_wiener.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_wiener.py
new file mode 100644
index 00000000..aded9514
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_wiener.py
@@ -0,0 +1,123 @@
+import networkx as nx
+
+
+def test_wiener_index_of_disconnected_graph():
+    assert nx.wiener_index(nx.empty_graph(2)) == float("inf")
+
+
+def test_wiener_index_of_directed_graph():
+    G = nx.complete_graph(3)
+    H = nx.DiGraph(G)
+    assert (2 * nx.wiener_index(G)) == nx.wiener_index(H)
+
+
+def test_wiener_index_of_complete_graph():
+    n = 10
+    G = nx.complete_graph(n)
+    assert nx.wiener_index(G) == (n * (n - 1) / 2)
+
+
+def test_wiener_index_of_path_graph():
+    # In P_n, there are n - 1 pairs of vertices at distance one, n -
+    # 2 pairs at distance two, n - 3 at distance three, ..., 1 at
+    # distance n - 1, so the Wiener index should be
+    #
+    #     1 * (n - 1) + 2 * (n - 2) + ... + (n - 2) * 2 + (n - 1) * 1
+    #
+    # For example, in P_5,
+    #
+    #     1 * 4 + 2 * 3 + 3 * 2 + 4 * 1 = 2 (1 * 4 + 2 * 3)
+    #
+    # and in P_6,
+    #
+    #     1 * 5 + 2 * 4 + 3 * 3 + 4 * 2 + 5 * 1 = 2 (1 * 5 + 2 * 4) + 3 * 3
+    #
+    # assuming n is *odd*, this gives the formula
+    #
+    #     2 \sum_{i = 1}^{(n - 1) / 2} [i * (n - i)]
+    #
+    # assuming n is *even*, this gives the formula
+    #
+    #     2 \sum_{i = 1}^{n / 2} [i * (n - i)] - (n / 2) ** 2
+    #
+    n = 9
+    G = nx.path_graph(n)
+    expected = 2 * sum(i * (n - i) for i in range(1, (n // 2) + 1))
+    actual = nx.wiener_index(G)
+    assert expected == actual
+
+
+def test_schultz_and_gutman_index_of_disconnected_graph():
+    n = 4
+    G = nx.Graph()
+    G.add_nodes_from(list(range(1, n + 1)))
+    expected = float("inf")
+
+    G.add_edge(1, 2)
+    G.add_edge(3, 4)
+
+    actual_1 = nx.schultz_index(G)
+    actual_2 = nx.gutman_index(G)
+
+    assert expected == actual_1
+    assert expected == actual_2
+
+
+def test_schultz_and_gutman_index_of_complete_bipartite_graph_1():
+    n = 3
+    m = 3
+    cbg = nx.complete_bipartite_graph(n, m)
+
+    expected_1 = n * m * (n + m) + 2 * n * (n - 1) * m + 2 * m * (m - 1) * n
+    actual_1 = nx.schultz_index(cbg)
+
+    expected_2 = n * m * (n * m) + n * (n - 1) * m * m + m * (m - 1) * n * n
+    actual_2 = nx.gutman_index(cbg)
+
+    assert expected_1 == actual_1
+    assert expected_2 == actual_2
+
+
+def test_schultz_and_gutman_index_of_complete_bipartite_graph_2():
+    n = 2
+    m = 5
+    cbg = nx.complete_bipartite_graph(n, m)
+
+    expected_1 = n * m * (n + m) + 2 * n * (n - 1) * m + 2 * m * (m - 1) * n
+    actual_1 = nx.schultz_index(cbg)
+
+    expected_2 = n * m * (n * m) + n * (n - 1) * m * m + m * (m - 1) * n * n
+    actual_2 = nx.gutman_index(cbg)
+
+    assert expected_1 == actual_1
+    assert expected_2 == actual_2
+
+
+def test_schultz_and_gutman_index_of_complete_graph():
+    n = 5
+    cg = nx.complete_graph(n)
+
+    expected_1 = n * (n - 1) * (n - 1)
+    actual_1 = nx.schultz_index(cg)
+
+    assert expected_1 == actual_1
+
+    expected_2 = n * (n - 1) * (n - 1) * (n - 1) / 2
+    actual_2 = nx.gutman_index(cg)
+
+    assert expected_2 == actual_2
+
+
+def test_schultz_and_gutman_index_of_odd_cycle_graph():
+    k = 5
+    n = 2 * k + 1
+    ocg = nx.cycle_graph(n)
+
+    expected_1 = 2 * n * k * (k + 1)
+    actual_1 = nx.schultz_index(ocg)
+
+    expected_2 = 2 * n * k * (k + 1)
+    actual_2 = nx.gutman_index(ocg)
+
+    assert expected_1 == actual_1
+    assert expected_2 == actual_2