about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/algorithms/traversal
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-4a52a71956a8d46fcb7294ac71734504bb09bcc2.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/traversal')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/__init__.py5
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/beamsearch.py90
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/breadth_first_search.py575
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/depth_first_search.py529
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/edgebfs.py178
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/edgedfs.py176
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_beamsearch.py25
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_bfs.py203
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_dfs.py305
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_edgebfs.py147
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_edgedfs.py131
12 files changed, 2364 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/__init__.py
new file mode 100644
index 00000000..93e6cdd0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/__init__.py
@@ -0,0 +1,5 @@
+from .beamsearch import *
+from .breadth_first_search import *
+from .depth_first_search import *
+from .edgedfs import *
+from .edgebfs import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/beamsearch.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/beamsearch.py
new file mode 100644
index 00000000..23fbe7bb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/beamsearch.py
@@ -0,0 +1,90 @@
+"""Basic algorithms for breadth-first searching the nodes of a graph."""
+
+import networkx as nx
+
+__all__ = ["bfs_beam_edges"]
+
+
+@nx._dispatchable
+def bfs_beam_edges(G, source, value, width=None):
+    """Iterates over edges in a beam search.
+
+    The beam search is a generalized breadth-first search in which only
+    the "best" *w* neighbors of the current node are enqueued, where *w*
+    is the beam width and "best" is an application-specific
+    heuristic. In general, a beam search with a small beam width might
+    not visit each node in the graph.
+
+    .. note::
+
+       With the default value of ``width=None`` or `width` greater than the
+       maximum degree of the graph, this function equates to a slower
+       version of `~networkx.algorithms.traversal.breadth_first_search.bfs_edges`.
+       All nodes will be visited, though the order of the reported edges may
+       vary. In such cases, `value` has no effect - consider using `bfs_edges`
+       directly instead.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node
+        Starting node for the breadth-first search; this function
+        iterates over only those edges in the component reachable from
+        this node.
+
+    value : function
+        A function that takes a node of the graph as input and returns a
+        real number indicating how "good" it is. A higher value means it
+        is more likely to be visited sooner during the search. When
+        visiting a new node, only the `width` neighbors with the highest
+        `value` are enqueued (in decreasing order of `value`).
+
+    width : int (default = None)
+        The beam width for the search. This is the number of neighbors
+        (ordered by `value`) to enqueue when visiting each new node.
+
+    Yields
+    ------
+    edge
+        Edges in the beam search starting from `source`, given as a pair
+        of nodes.
+
+    Examples
+    --------
+    To give nodes with, for example, a higher centrality precedence
+    during the search, set the `value` function to return the centrality
+    value of the node:
+
+    >>> G = nx.karate_club_graph()
+    >>> centrality = nx.eigenvector_centrality(G)
+    >>> list(nx.bfs_beam_edges(G, source=0, value=centrality.get, width=3))
+    [(0, 2), (0, 1), (0, 8), (2, 32), (1, 13), (8, 33)]
+    """
+
+    if width is None:
+        width = len(G)
+
+    def successors(v):
+        """Returns a list of the best neighbors of a node.
+
+        `v` is a node in the graph `G`.
+
+        The "best" neighbors are chosen according to the `value`
+        function (higher is better). Only the `width` best neighbors of
+        `v` are returned.
+        """
+        # TODO The Python documentation states that for small values, it
+        # is better to use `heapq.nlargest`. We should determine the
+        # threshold at which its better to use `heapq.nlargest()`
+        # instead of `sorted()[:]` and apply that optimization here.
+        #
+        # If `width` is greater than the number of neighbors of `v`, all
+        # neighbors are returned by the semantics of slicing in
+        # Python. This occurs in the special case that the user did not
+        # specify a `width`: in this case all neighbors are always
+        # returned, so this is just a (slower) implementation of
+        # `bfs_edges(G, source)` but with a sorted enqueue step.
+        return iter(sorted(G.neighbors(v), key=value, reverse=True)[:width])
+
+    yield from nx.generic_bfs_edges(G, source, successors)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/breadth_first_search.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/breadth_first_search.py
new file mode 100644
index 00000000..899dc92b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/breadth_first_search.py
@@ -0,0 +1,575 @@
+"""Basic algorithms for breadth-first searching the nodes of a graph."""
+
+from collections import deque
+
+import networkx as nx
+
+__all__ = [
+    "bfs_edges",
+    "bfs_tree",
+    "bfs_predecessors",
+    "bfs_successors",
+    "descendants_at_distance",
+    "bfs_layers",
+    "bfs_labeled_edges",
+    "generic_bfs_edges",
+]
+
+
+@nx._dispatchable
+def generic_bfs_edges(G, source, neighbors=None, depth_limit=None):
+    """Iterate over edges in a breadth-first search.
+
+    The breadth-first search begins at `source` and enqueues the
+    neighbors of newly visited nodes specified by the `neighbors`
+    function.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node
+        Starting node for the breadth-first search; this function
+        iterates over only those edges in the component reachable from
+        this node.
+
+    neighbors : function
+        A function that takes a newly visited node of the graph as input
+        and returns an *iterator* (not just a list) of nodes that are
+        neighbors of that node with custom ordering. If not specified, this is
+        just the ``G.neighbors`` method, but in general it can be any function
+        that returns an iterator over some or all of the neighbors of a
+        given node, in any order.
+
+    depth_limit : int, optional(default=len(G))
+        Specify the maximum search depth.
+
+    Yields
+    ------
+    edge
+        Edges in the breadth-first search starting from `source`.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(7)
+    >>> list(nx.generic_bfs_edges(G, source=0))
+    [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]
+    >>> list(nx.generic_bfs_edges(G, source=2))
+    [(2, 1), (2, 3), (1, 0), (3, 4), (4, 5), (5, 6)]
+    >>> list(nx.generic_bfs_edges(G, source=2, depth_limit=2))
+    [(2, 1), (2, 3), (1, 0), (3, 4)]
+
+    The `neighbors` param can be used to specify the visitation order of each
+    node's neighbors generically. In the following example, we modify the default
+    neighbor to return *odd* nodes first:
+
+    >>> def odd_first(n):
+    ...     return sorted(G.neighbors(n), key=lambda x: x % 2, reverse=True)
+
+    >>> G = nx.star_graph(5)
+    >>> list(nx.generic_bfs_edges(G, source=0))  # Default neighbor ordering
+    [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)]
+    >>> list(nx.generic_bfs_edges(G, source=0, neighbors=odd_first))
+    [(0, 1), (0, 3), (0, 5), (0, 2), (0, 4)]
+
+    Notes
+    -----
+    This implementation is from `PADS`_, which was in the public domain
+    when it was first accessed in July, 2004.  The modifications
+    to allow depth limits are based on the Wikipedia article
+    "`Depth-limited-search`_".
+
+    .. _PADS: http://www.ics.uci.edu/~eppstein/PADS/BFS.py
+    .. _Depth-limited-search: https://en.wikipedia.org/wiki/Depth-limited_search
+    """
+    if neighbors is None:
+        neighbors = G.neighbors
+    if depth_limit is None:
+        depth_limit = len(G)
+
+    seen = {source}
+    n = len(G)
+    depth = 0
+    next_parents_children = [(source, neighbors(source))]
+    while next_parents_children and depth < depth_limit:
+        this_parents_children = next_parents_children
+        next_parents_children = []
+        for parent, children in this_parents_children:
+            for child in children:
+                if child not in seen:
+                    seen.add(child)
+                    next_parents_children.append((child, neighbors(child)))
+                    yield parent, child
+            if len(seen) == n:
+                return
+        depth += 1
+
+
+@nx._dispatchable
+def bfs_edges(G, source, reverse=False, depth_limit=None, sort_neighbors=None):
+    """Iterate over edges in a breadth-first-search starting at source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node
+       Specify starting node for breadth-first search; this function
+       iterates over only those edges in the component reachable from
+       this node.
+
+    reverse : bool, optional
+       If True traverse a directed graph in the reverse direction
+
+    depth_limit : int, optional(default=len(G))
+        Specify the maximum search depth
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Yields
+    ------
+    edge: 2-tuple of nodes
+       Yields edges resulting from the breadth-first search.
+
+    Examples
+    --------
+    To get the edges in a breadth-first search::
+
+        >>> G = nx.path_graph(3)
+        >>> list(nx.bfs_edges(G, 0))
+        [(0, 1), (1, 2)]
+        >>> list(nx.bfs_edges(G, source=0, depth_limit=1))
+        [(0, 1)]
+
+    To get the nodes in a breadth-first search order::
+
+        >>> G = nx.path_graph(3)
+        >>> root = 2
+        >>> edges = nx.bfs_edges(G, root)
+        >>> nodes = [root] + [v for u, v in edges]
+        >>> nodes
+        [2, 1, 0]
+
+    Notes
+    -----
+    The naming of this function is very similar to
+    :func:`~networkx.algorithms.traversal.edgebfs.edge_bfs`. The difference
+    is that ``edge_bfs`` yields edges even if they extend back to an already
+    explored node while this generator yields the edges of the tree that results
+    from a breadth-first-search (BFS) so no edges are reported if they extend
+    to already explored nodes. That means ``edge_bfs`` reports all edges while
+    ``bfs_edges`` only reports those traversed by a node-based BFS. Yet another
+    description is that ``bfs_edges`` reports the edges traversed during BFS
+    while ``edge_bfs`` reports all edges in the order they are explored.
+
+    Based on the breadth-first search implementation in PADS [1]_
+    by D. Eppstein, July 2004; with modifications to allow depth limits
+    as described in [2]_.
+
+    References
+    ----------
+    .. [1] http://www.ics.uci.edu/~eppstein/PADS/BFS.py.
+    .. [2] https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    bfs_tree
+    :func:`~networkx.algorithms.traversal.depth_first_search.dfs_edges`
+    :func:`~networkx.algorithms.traversal.edgebfs.edge_bfs`
+
+    """
+    if reverse and G.is_directed():
+        successors = G.predecessors
+    else:
+        successors = G.neighbors
+
+    if sort_neighbors is not None:
+        yield from generic_bfs_edges(
+            G, source, lambda node: iter(sort_neighbors(successors(node))), depth_limit
+        )
+    else:
+        yield from generic_bfs_edges(G, source, successors, depth_limit)
+
+
+@nx._dispatchable(returns_graph=True)
+def bfs_tree(G, source, reverse=False, depth_limit=None, sort_neighbors=None):
+    """Returns an oriented tree constructed from of a breadth-first-search
+    starting at source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node
+       Specify starting node for breadth-first search
+
+    reverse : bool, optional
+       If True traverse a directed graph in the reverse direction
+
+    depth_limit : int, optional(default=len(G))
+        Specify the maximum search depth
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    T: NetworkX DiGraph
+       An oriented tree
+
+    Examples
+    --------
+    >>> G = nx.path_graph(3)
+    >>> list(nx.bfs_tree(G, 1).edges())
+    [(1, 0), (1, 2)]
+    >>> H = nx.Graph()
+    >>> nx.add_path(H, [0, 1, 2, 3, 4, 5, 6])
+    >>> nx.add_path(H, [2, 7, 8, 9, 10])
+    >>> sorted(list(nx.bfs_tree(H, source=3, depth_limit=3).edges()))
+    [(1, 0), (2, 1), (2, 7), (3, 2), (3, 4), (4, 5), (5, 6), (7, 8)]
+
+
+    Notes
+    -----
+    Based on http://www.ics.uci.edu/~eppstein/PADS/BFS.py
+    by D. Eppstein, July 2004. The modifications
+    to allow depth limits based on the Wikipedia article
+    "`Depth-limited-search`_".
+
+    .. _Depth-limited-search: https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    dfs_tree
+    bfs_edges
+    edge_bfs
+    """
+    T = nx.DiGraph()
+    T.add_node(source)
+    edges_gen = bfs_edges(
+        G,
+        source,
+        reverse=reverse,
+        depth_limit=depth_limit,
+        sort_neighbors=sort_neighbors,
+    )
+    T.add_edges_from(edges_gen)
+    return T
+
+
+@nx._dispatchable
+def bfs_predecessors(G, source, depth_limit=None, sort_neighbors=None):
+    """Returns an iterator of predecessors in breadth-first-search from source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node
+       Specify starting node for breadth-first search
+
+    depth_limit : int, optional(default=len(G))
+        Specify the maximum search depth
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    pred: iterator
+        (node, predecessor) iterator where `predecessor` is the predecessor of
+        `node` in a breadth first search starting from `source`.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(3)
+    >>> dict(nx.bfs_predecessors(G, 0))
+    {1: 0, 2: 1}
+    >>> H = nx.Graph()
+    >>> H.add_edges_from([(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)])
+    >>> dict(nx.bfs_predecessors(H, 0))
+    {1: 0, 2: 0, 3: 1, 4: 1, 5: 2, 6: 2}
+    >>> M = nx.Graph()
+    >>> nx.add_path(M, [0, 1, 2, 3, 4, 5, 6])
+    >>> nx.add_path(M, [2, 7, 8, 9, 10])
+    >>> sorted(nx.bfs_predecessors(M, source=1, depth_limit=3))
+    [(0, 1), (2, 1), (3, 2), (4, 3), (7, 2), (8, 7)]
+    >>> N = nx.DiGraph()
+    >>> nx.add_path(N, [0, 1, 2, 3, 4, 7])
+    >>> nx.add_path(N, [3, 5, 6, 7])
+    >>> sorted(nx.bfs_predecessors(N, source=2))
+    [(3, 2), (4, 3), (5, 3), (6, 5), (7, 4)]
+
+    Notes
+    -----
+    Based on http://www.ics.uci.edu/~eppstein/PADS/BFS.py
+    by D. Eppstein, July 2004. The modifications
+    to allow depth limits based on the Wikipedia article
+    "`Depth-limited-search`_".
+
+    .. _Depth-limited-search: https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    bfs_tree
+    bfs_edges
+    edge_bfs
+    """
+    for s, t in bfs_edges(
+        G, source, depth_limit=depth_limit, sort_neighbors=sort_neighbors
+    ):
+        yield (t, s)
+
+
+@nx._dispatchable
+def bfs_successors(G, source, depth_limit=None, sort_neighbors=None):
+    """Returns an iterator of successors in breadth-first-search from source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node
+       Specify starting node for breadth-first search
+
+    depth_limit : int, optional(default=len(G))
+        Specify the maximum search depth
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    succ: iterator
+       (node, successors) iterator where `successors` is the non-empty list of
+       successors of `node` in a breadth first search from `source`.
+       To appear in the iterator, `node` must have successors.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(3)
+    >>> dict(nx.bfs_successors(G, 0))
+    {0: [1], 1: [2]}
+    >>> H = nx.Graph()
+    >>> H.add_edges_from([(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)])
+    >>> dict(nx.bfs_successors(H, 0))
+    {0: [1, 2], 1: [3, 4], 2: [5, 6]}
+    >>> G = nx.Graph()
+    >>> nx.add_path(G, [0, 1, 2, 3, 4, 5, 6])
+    >>> nx.add_path(G, [2, 7, 8, 9, 10])
+    >>> dict(nx.bfs_successors(G, source=1, depth_limit=3))
+    {1: [0, 2], 2: [3, 7], 3: [4], 7: [8]}
+    >>> G = nx.DiGraph()
+    >>> nx.add_path(G, [0, 1, 2, 3, 4, 5])
+    >>> dict(nx.bfs_successors(G, source=3))
+    {3: [4], 4: [5]}
+
+    Notes
+    -----
+    Based on http://www.ics.uci.edu/~eppstein/PADS/BFS.py
+    by D. Eppstein, July 2004.The modifications
+    to allow depth limits based on the Wikipedia article
+    "`Depth-limited-search`_".
+
+    .. _Depth-limited-search: https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    bfs_tree
+    bfs_edges
+    edge_bfs
+    """
+    parent = source
+    children = []
+    for p, c in bfs_edges(
+        G, source, depth_limit=depth_limit, sort_neighbors=sort_neighbors
+    ):
+        if p == parent:
+            children.append(c)
+            continue
+        yield (parent, children)
+        children = [c]
+        parent = p
+    yield (parent, children)
+
+
+@nx._dispatchable
+def bfs_layers(G, sources):
+    """Returns an iterator of all the layers in breadth-first search traversal.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        A graph over which to find the layers using breadth-first search.
+
+    sources : node in `G` or list of nodes in `G`
+        Specify starting nodes for single source or multiple sources breadth-first search
+
+    Yields
+    ------
+    layer: list of nodes
+        Yields list of nodes at the same distance from sources
+
+    Examples
+    --------
+    >>> G = nx.path_graph(5)
+    >>> dict(enumerate(nx.bfs_layers(G, [0, 4])))
+    {0: [0, 4], 1: [1, 3], 2: [2]}
+    >>> H = nx.Graph()
+    >>> H.add_edges_from([(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)])
+    >>> dict(enumerate(nx.bfs_layers(H, [1])))
+    {0: [1], 1: [0, 3, 4], 2: [2], 3: [5, 6]}
+    >>> dict(enumerate(nx.bfs_layers(H, [1, 6])))
+    {0: [1, 6], 1: [0, 3, 4, 2], 2: [5]}
+    """
+    if sources in G:
+        sources = [sources]
+
+    current_layer = list(sources)
+    visited = set(sources)
+
+    for source in current_layer:
+        if source not in G:
+            raise nx.NetworkXError(f"The node {source} is not in the graph.")
+
+    # this is basically BFS, except that the current layer only stores the nodes at
+    # same distance from sources at each iteration
+    while current_layer:
+        yield current_layer
+        next_layer = []
+        for node in current_layer:
+            for child in G[node]:
+                if child not in visited:
+                    visited.add(child)
+                    next_layer.append(child)
+        current_layer = next_layer
+
+
+REVERSE_EDGE = "reverse"
+TREE_EDGE = "tree"
+FORWARD_EDGE = "forward"
+LEVEL_EDGE = "level"
+
+
+@nx._dispatchable
+def bfs_labeled_edges(G, sources):
+    """Iterate over edges in a breadth-first search (BFS) labeled by type.
+
+    We generate triple of the form (*u*, *v*, *d*), where (*u*, *v*) is the
+    edge being explored in the breadth-first search and *d* is one of the
+    strings 'tree', 'forward', 'level', or 'reverse'.  A 'tree' edge is one in
+    which *v* is first discovered and placed into the layer below *u*.  A
+    'forward' edge is one in which *u* is on the layer above *v* and *v* has
+    already been discovered.  A 'level' edge is one in which both *u* and *v*
+    occur on the same layer.  A 'reverse' edge is one in which *u* is on a layer
+    below *v*.
+
+    We emit each edge exactly once.  In an undirected graph, 'reverse' edges do
+    not occur, because each is discovered either as a 'tree' or 'forward' edge.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        A graph over which to find the layers using breadth-first search.
+
+    sources : node in `G` or list of nodes in `G`
+        Starting nodes for single source or multiple sources breadth-first search
+
+    Yields
+    ------
+    edges: generator
+       A generator of triples (*u*, *v*, *d*) where (*u*, *v*) is the edge being
+       explored and *d* is described above.
+
+    Examples
+    --------
+    >>> G = nx.cycle_graph(4, create_using=nx.DiGraph)
+    >>> list(nx.bfs_labeled_edges(G, 0))
+    [(0, 1, 'tree'), (1, 2, 'tree'), (2, 3, 'tree'), (3, 0, 'reverse')]
+    >>> G = nx.complete_graph(3)
+    >>> list(nx.bfs_labeled_edges(G, 0))
+    [(0, 1, 'tree'), (0, 2, 'tree'), (1, 2, 'level')]
+    >>> list(nx.bfs_labeled_edges(G, [0, 1]))
+    [(0, 1, 'level'), (0, 2, 'tree'), (1, 2, 'forward')]
+    """
+    if sources in G:
+        sources = [sources]
+
+    neighbors = G._adj
+    directed = G.is_directed()
+    visited = set()
+    visit = visited.discard if directed else visited.add
+    # We use visited in a negative sense, so the visited set stays empty for the
+    # directed case and level edges are reported on their first occurrence in
+    # the undirected case.  Note our use of visited.discard -- this is built-in
+    # thus somewhat faster than a python-defined def nop(x): pass
+    depth = {s: 0 for s in sources}
+    queue = deque(depth.items())
+    push = queue.append
+    pop = queue.popleft
+    while queue:
+        u, du = pop()
+        for v in neighbors[u]:
+            if v not in depth:
+                depth[v] = dv = du + 1
+                push((v, dv))
+                yield u, v, TREE_EDGE
+            else:
+                dv = depth[v]
+                if du == dv:
+                    if v not in visited:
+                        yield u, v, LEVEL_EDGE
+                elif du < dv:
+                    yield u, v, FORWARD_EDGE
+                elif directed:
+                    yield u, v, REVERSE_EDGE
+        visit(u)
+
+
+@nx._dispatchable
+def descendants_at_distance(G, source, distance):
+    """Returns all nodes at a fixed `distance` from `source` in `G`.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        A graph
+    source : node in `G`
+    distance : the distance of the wanted nodes from `source`
+
+    Returns
+    -------
+    set()
+        The descendants of `source` in `G` at the given `distance` from `source`
+
+    Examples
+    --------
+    >>> G = nx.path_graph(5)
+    >>> nx.descendants_at_distance(G, 2, 2)
+    {0, 4}
+    >>> H = nx.DiGraph()
+    >>> H.add_edges_from([(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)])
+    >>> nx.descendants_at_distance(H, 0, 2)
+    {3, 4, 5, 6}
+    >>> nx.descendants_at_distance(H, 5, 0)
+    {5}
+    >>> nx.descendants_at_distance(H, 5, 1)
+    set()
+    """
+    if source not in G:
+        raise nx.NetworkXError(f"The node {source} is not in the graph.")
+
+    bfs_generator = nx.bfs_layers(G, source)
+    for i, layer in enumerate(bfs_generator):
+        if i == distance:
+            return set(layer)
+    return set()
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/depth_first_search.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/depth_first_search.py
new file mode 100644
index 00000000..5bac5ecf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/depth_first_search.py
@@ -0,0 +1,529 @@
+"""Basic algorithms for depth-first searching the nodes of a graph."""
+
+from collections import defaultdict
+
+import networkx as nx
+
+__all__ = [
+    "dfs_edges",
+    "dfs_tree",
+    "dfs_predecessors",
+    "dfs_successors",
+    "dfs_preorder_nodes",
+    "dfs_postorder_nodes",
+    "dfs_labeled_edges",
+]
+
+
+@nx._dispatchable
+def dfs_edges(G, source=None, depth_limit=None, *, sort_neighbors=None):
+    """Iterate over edges in a depth-first-search (DFS).
+
+    Perform a depth-first-search over the nodes of `G` and yield
+    the edges in order. This may not generate all edges in `G`
+    (see `~networkx.algorithms.traversal.edgedfs.edge_dfs`).
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node, optional
+       Specify starting node for depth-first search and yield edges in
+       the component reachable from source.
+
+    depth_limit : int, optional (default=len(G))
+       Specify the maximum search depth.
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Yields
+    ------
+    edge: 2-tuple of nodes
+       Yields edges resulting from the depth-first-search.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(5)
+    >>> list(nx.dfs_edges(G, source=0))
+    [(0, 1), (1, 2), (2, 3), (3, 4)]
+    >>> list(nx.dfs_edges(G, source=0, depth_limit=2))
+    [(0, 1), (1, 2)]
+
+    Notes
+    -----
+    If a source is not specified then a source is chosen arbitrarily and
+    repeatedly until all components in the graph are searched.
+
+    The implementation of this function is adapted from David Eppstein's
+    depth-first search function in PADS [1]_, with modifications
+    to allow depth limits based on the Wikipedia article
+    "Depth-limited search" [2]_.
+
+    See Also
+    --------
+    dfs_preorder_nodes
+    dfs_postorder_nodes
+    dfs_labeled_edges
+    :func:`~networkx.algorithms.traversal.edgedfs.edge_dfs`
+    :func:`~networkx.algorithms.traversal.breadth_first_search.bfs_edges`
+
+    References
+    ----------
+    .. [1] http://www.ics.uci.edu/~eppstein/PADS
+    .. [2] https://en.wikipedia.org/wiki/Depth-limited_search
+    """
+    if source is None:
+        # edges for all components
+        nodes = G
+    else:
+        # edges for components with source
+        nodes = [source]
+    if depth_limit is None:
+        depth_limit = len(G)
+
+    get_children = (
+        G.neighbors
+        if sort_neighbors is None
+        else lambda n: iter(sort_neighbors(G.neighbors(n)))
+    )
+
+    visited = set()
+    for start in nodes:
+        if start in visited:
+            continue
+        visited.add(start)
+        stack = [(start, get_children(start))]
+        depth_now = 1
+        while stack:
+            parent, children = stack[-1]
+            for child in children:
+                if child not in visited:
+                    yield parent, child
+                    visited.add(child)
+                    if depth_now < depth_limit:
+                        stack.append((child, get_children(child)))
+                        depth_now += 1
+                        break
+            else:
+                stack.pop()
+                depth_now -= 1
+
+
+@nx._dispatchable(returns_graph=True)
+def dfs_tree(G, source=None, depth_limit=None, *, sort_neighbors=None):
+    """Returns oriented tree constructed from a depth-first-search from source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node, optional
+       Specify starting node for depth-first search.
+
+    depth_limit : int, optional (default=len(G))
+       Specify the maximum search depth.
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    T : NetworkX DiGraph
+       An oriented tree
+
+    Examples
+    --------
+    >>> G = nx.path_graph(5)
+    >>> T = nx.dfs_tree(G, source=0, depth_limit=2)
+    >>> list(T.edges())
+    [(0, 1), (1, 2)]
+    >>> T = nx.dfs_tree(G, source=0)
+    >>> list(T.edges())
+    [(0, 1), (1, 2), (2, 3), (3, 4)]
+
+    See Also
+    --------
+    dfs_preorder_nodes
+    dfs_postorder_nodes
+    dfs_labeled_edges
+    :func:`~networkx.algorithms.traversal.edgedfs.edge_dfs`
+    :func:`~networkx.algorithms.traversal.breadth_first_search.bfs_tree`
+    """
+    T = nx.DiGraph()
+    if source is None:
+        T.add_nodes_from(G)
+    else:
+        T.add_node(source)
+    T.add_edges_from(dfs_edges(G, source, depth_limit, sort_neighbors=sort_neighbors))
+    return T
+
+
+@nx._dispatchable
+def dfs_predecessors(G, source=None, depth_limit=None, *, sort_neighbors=None):
+    """Returns dictionary of predecessors in depth-first-search from source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node, optional
+       Specify starting node for depth-first search.
+       Note that you will get predecessors for all nodes in the
+       component containing `source`. This input only specifies
+       where the DFS starts.
+
+    depth_limit : int, optional (default=len(G))
+       Specify the maximum search depth.
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    pred: dict
+       A dictionary with nodes as keys and predecessor nodes as values.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(4)
+    >>> nx.dfs_predecessors(G, source=0)
+    {1: 0, 2: 1, 3: 2}
+    >>> nx.dfs_predecessors(G, source=0, depth_limit=2)
+    {1: 0, 2: 1}
+
+    Notes
+    -----
+    If a source is not specified then a source is chosen arbitrarily and
+    repeatedly until all components in the graph are searched.
+
+    The implementation of this function is adapted from David Eppstein's
+    depth-first search function in `PADS`_, with modifications
+    to allow depth limits based on the Wikipedia article
+    "`Depth-limited search`_".
+
+    .. _PADS: http://www.ics.uci.edu/~eppstein/PADS
+    .. _Depth-limited search: https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    dfs_preorder_nodes
+    dfs_postorder_nodes
+    dfs_labeled_edges
+    :func:`~networkx.algorithms.traversal.edgedfs.edge_dfs`
+    :func:`~networkx.algorithms.traversal.breadth_first_search.bfs_tree`
+    """
+    return {
+        t: s
+        for s, t in dfs_edges(G, source, depth_limit, sort_neighbors=sort_neighbors)
+    }
+
+
+@nx._dispatchable
+def dfs_successors(G, source=None, depth_limit=None, *, sort_neighbors=None):
+    """Returns dictionary of successors in depth-first-search from source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node, optional
+       Specify starting node for depth-first search.
+       Note that you will get successors for all nodes in the
+       component containing `source`. This input only specifies
+       where the DFS starts.
+
+    depth_limit : int, optional (default=len(G))
+       Specify the maximum search depth.
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    succ: dict
+       A dictionary with nodes as keys and list of successor nodes as values.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(5)
+    >>> nx.dfs_successors(G, source=0)
+    {0: [1], 1: [2], 2: [3], 3: [4]}
+    >>> nx.dfs_successors(G, source=0, depth_limit=2)
+    {0: [1], 1: [2]}
+
+    Notes
+    -----
+    If a source is not specified then a source is chosen arbitrarily and
+    repeatedly until all components in the graph are searched.
+
+    The implementation of this function is adapted from David Eppstein's
+    depth-first search function in `PADS`_, with modifications
+    to allow depth limits based on the Wikipedia article
+    "`Depth-limited search`_".
+
+    .. _PADS: http://www.ics.uci.edu/~eppstein/PADS
+    .. _Depth-limited search: https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    dfs_preorder_nodes
+    dfs_postorder_nodes
+    dfs_labeled_edges
+    :func:`~networkx.algorithms.traversal.edgedfs.edge_dfs`
+    :func:`~networkx.algorithms.traversal.breadth_first_search.bfs_tree`
+    """
+    d = defaultdict(list)
+    for s, t in dfs_edges(
+        G,
+        source=source,
+        depth_limit=depth_limit,
+        sort_neighbors=sort_neighbors,
+    ):
+        d[s].append(t)
+    return dict(d)
+
+
+@nx._dispatchable
+def dfs_postorder_nodes(G, source=None, depth_limit=None, *, sort_neighbors=None):
+    """Generate nodes in a depth-first-search post-ordering starting at source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node, optional
+       Specify starting node for depth-first search.
+
+    depth_limit : int, optional (default=len(G))
+       Specify the maximum search depth.
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    nodes: generator
+       A generator of nodes in a depth-first-search post-ordering.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(5)
+    >>> list(nx.dfs_postorder_nodes(G, source=0))
+    [4, 3, 2, 1, 0]
+    >>> list(nx.dfs_postorder_nodes(G, source=0, depth_limit=2))
+    [1, 0]
+
+    Notes
+    -----
+    If a source is not specified then a source is chosen arbitrarily and
+    repeatedly until all components in the graph are searched.
+
+    The implementation of this function is adapted from David Eppstein's
+    depth-first search function in `PADS`_, with modifications
+    to allow depth limits based on the Wikipedia article
+    "`Depth-limited search`_".
+
+    .. _PADS: http://www.ics.uci.edu/~eppstein/PADS
+    .. _Depth-limited search: https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    dfs_edges
+    dfs_preorder_nodes
+    dfs_labeled_edges
+    :func:`~networkx.algorithms.traversal.edgedfs.edge_dfs`
+    :func:`~networkx.algorithms.traversal.breadth_first_search.bfs_tree`
+    """
+    edges = nx.dfs_labeled_edges(
+        G, source=source, depth_limit=depth_limit, sort_neighbors=sort_neighbors
+    )
+    return (v for u, v, d in edges if d == "reverse")
+
+
+@nx._dispatchable
+def dfs_preorder_nodes(G, source=None, depth_limit=None, *, sort_neighbors=None):
+    """Generate nodes in a depth-first-search pre-ordering starting at source.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node, optional
+       Specify starting node for depth-first search and return nodes in
+       the component reachable from source.
+
+    depth_limit : int, optional (default=len(G))
+       Specify the maximum search depth.
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    nodes: generator
+       A generator of nodes in a depth-first-search pre-ordering.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(5)
+    >>> list(nx.dfs_preorder_nodes(G, source=0))
+    [0, 1, 2, 3, 4]
+    >>> list(nx.dfs_preorder_nodes(G, source=0, depth_limit=2))
+    [0, 1, 2]
+
+    Notes
+    -----
+    If a source is not specified then a source is chosen arbitrarily and
+    repeatedly until all components in the graph are searched.
+
+    The implementation of this function is adapted from David Eppstein's
+    depth-first search function in `PADS`_, with modifications
+    to allow depth limits based on the Wikipedia article
+    "`Depth-limited search`_".
+
+    .. _PADS: http://www.ics.uci.edu/~eppstein/PADS
+    .. _Depth-limited search: https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    dfs_edges
+    dfs_postorder_nodes
+    dfs_labeled_edges
+    :func:`~networkx.algorithms.traversal.breadth_first_search.bfs_edges`
+    """
+    edges = nx.dfs_labeled_edges(
+        G, source=source, depth_limit=depth_limit, sort_neighbors=sort_neighbors
+    )
+    return (v for u, v, d in edges if d == "forward")
+
+
+@nx._dispatchable
+def dfs_labeled_edges(G, source=None, depth_limit=None, *, sort_neighbors=None):
+    """Iterate over edges in a depth-first-search (DFS) labeled by type.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : node, optional
+       Specify starting node for depth-first search and return edges in
+       the component reachable from source.
+
+    depth_limit : int, optional (default=len(G))
+       Specify the maximum search depth.
+
+    sort_neighbors : function (default=None)
+        A function that takes an iterator over nodes as the input, and
+        returns an iterable of the same nodes with a custom ordering.
+        For example, `sorted` will sort the nodes in increasing order.
+
+    Returns
+    -------
+    edges: generator
+       A generator of triples of the form (*u*, *v*, *d*), where (*u*,
+       *v*) is the edge being explored in the depth-first search and *d*
+       is one of the strings 'forward', 'nontree', 'reverse', or 'reverse-depth_limit'.
+       A 'forward' edge is one in which *u* has been visited but *v* has
+       not. A 'nontree' edge is one in which both *u* and *v* have been
+       visited but the edge is not in the DFS tree. A 'reverse' edge is
+       one in which both *u* and *v* have been visited and the edge is in
+       the DFS tree. When the `depth_limit` is reached via a 'forward' edge,
+       a 'reverse' edge is immediately generated rather than the subtree
+       being explored. To indicate this flavor of 'reverse' edge, the string
+       yielded is 'reverse-depth_limit'.
+
+    Examples
+    --------
+
+    The labels reveal the complete transcript of the depth-first search
+    algorithm in more detail than, for example, :func:`dfs_edges`::
+
+        >>> from pprint import pprint
+        >>>
+        >>> G = nx.DiGraph([(0, 1), (1, 2), (2, 1)])
+        >>> pprint(list(nx.dfs_labeled_edges(G, source=0)))
+        [(0, 0, 'forward'),
+         (0, 1, 'forward'),
+         (1, 2, 'forward'),
+         (2, 1, 'nontree'),
+         (1, 2, 'reverse'),
+         (0, 1, 'reverse'),
+         (0, 0, 'reverse')]
+
+    Notes
+    -----
+    If a source is not specified then a source is chosen arbitrarily and
+    repeatedly until all components in the graph are searched.
+
+    The implementation of this function is adapted from David Eppstein's
+    depth-first search function in `PADS`_, with modifications
+    to allow depth limits based on the Wikipedia article
+    "`Depth-limited search`_".
+
+    .. _PADS: http://www.ics.uci.edu/~eppstein/PADS
+    .. _Depth-limited search: https://en.wikipedia.org/wiki/Depth-limited_search
+
+    See Also
+    --------
+    dfs_edges
+    dfs_preorder_nodes
+    dfs_postorder_nodes
+    """
+    # Based on http://www.ics.uci.edu/~eppstein/PADS/DFS.py
+    # by D. Eppstein, July 2004.
+    if source is None:
+        # edges for all components
+        nodes = G
+    else:
+        # edges for components with source
+        nodes = [source]
+    if depth_limit is None:
+        depth_limit = len(G)
+
+    get_children = (
+        G.neighbors
+        if sort_neighbors is None
+        else lambda n: iter(sort_neighbors(G.neighbors(n)))
+    )
+
+    visited = set()
+    for start in nodes:
+        if start in visited:
+            continue
+        yield start, start, "forward"
+        visited.add(start)
+        stack = [(start, get_children(start))]
+        depth_now = 1
+        while stack:
+            parent, children = stack[-1]
+            for child in children:
+                if child in visited:
+                    yield parent, child, "nontree"
+                else:
+                    yield parent, child, "forward"
+                    visited.add(child)
+                    if depth_now < depth_limit:
+                        stack.append((child, iter(get_children(child))))
+                        depth_now += 1
+                        break
+                    else:
+                        yield parent, child, "reverse-depth_limit"
+            else:
+                stack.pop()
+                depth_now -= 1
+                if stack:
+                    yield stack[-1][0], parent, "reverse"
+        yield start, start, "reverse"
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/edgebfs.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/edgebfs.py
new file mode 100644
index 00000000..6320ddc2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/edgebfs.py
@@ -0,0 +1,178 @@
+"""
+=============================
+Breadth First Search on Edges
+=============================
+
+Algorithms for a breadth-first traversal of edges in a graph.
+
+"""
+
+from collections import deque
+
+import networkx as nx
+
+FORWARD = "forward"
+REVERSE = "reverse"
+
+__all__ = ["edge_bfs"]
+
+
+@nx._dispatchable
+def edge_bfs(G, source=None, orientation=None):
+    """A directed, breadth-first-search of edges in `G`, beginning at `source`.
+
+    Yield the edges of G in a breadth-first-search order continuing until
+    all edges are generated.
+
+    Parameters
+    ----------
+    G : graph
+        A directed/undirected graph/multigraph.
+
+    source : node, list of nodes
+        The node from which the traversal begins. If None, then a source
+        is chosen arbitrarily and repeatedly until all edges from each node in
+        the graph are searched.
+
+    orientation : None | 'original' | 'reverse' | 'ignore' (default: None)
+        For directed graphs and directed multigraphs, edge traversals need not
+        respect the original orientation of the edges.
+        When set to 'reverse' every edge is traversed in the reverse direction.
+        When set to 'ignore', every edge is treated as undirected.
+        When set to 'original', every edge is treated as directed.
+        In all three cases, the yielded edge tuples add a last entry to
+        indicate the direction in which that edge was traversed.
+        If orientation is None, the yielded edge has no direction indicated.
+        The direction is respected, but not reported.
+
+    Yields
+    ------
+    edge : directed edge
+        A directed edge indicating the path taken by the breadth-first-search.
+        For graphs, `edge` is of the form `(u, v)` where `u` and `v`
+        are the tail and head of the edge as determined by the traversal.
+        For multigraphs, `edge` is of the form `(u, v, key)`, where `key` is
+        the key of the edge. When the graph is directed, then `u` and `v`
+        are always in the order of the actual directed edge.
+        If orientation is not None then the edge tuple is extended to include
+        the direction of traversal ('forward' or 'reverse') on that edge.
+
+    Examples
+    --------
+    >>> nodes = [0, 1, 2, 3]
+    >>> edges = [(0, 1), (1, 0), (1, 0), (2, 0), (2, 1), (3, 1)]
+
+    >>> list(nx.edge_bfs(nx.Graph(edges), nodes))
+    [(0, 1), (0, 2), (1, 2), (1, 3)]
+
+    >>> list(nx.edge_bfs(nx.DiGraph(edges), nodes))
+    [(0, 1), (1, 0), (2, 0), (2, 1), (3, 1)]
+
+    >>> list(nx.edge_bfs(nx.MultiGraph(edges), nodes))
+    [(0, 1, 0), (0, 1, 1), (0, 1, 2), (0, 2, 0), (1, 2, 0), (1, 3, 0)]
+
+    >>> list(nx.edge_bfs(nx.MultiDiGraph(edges), nodes))
+    [(0, 1, 0), (1, 0, 0), (1, 0, 1), (2, 0, 0), (2, 1, 0), (3, 1, 0)]
+
+    >>> list(nx.edge_bfs(nx.DiGraph(edges), nodes, orientation="ignore"))
+    [(0, 1, 'forward'), (1, 0, 'reverse'), (2, 0, 'reverse'), (2, 1, 'reverse'), (3, 1, 'reverse')]
+
+    >>> list(nx.edge_bfs(nx.MultiDiGraph(edges), nodes, orientation="ignore"))
+    [(0, 1, 0, 'forward'), (1, 0, 0, 'reverse'), (1, 0, 1, 'reverse'), (2, 0, 0, 'reverse'), (2, 1, 0, 'reverse'), (3, 1, 0, 'reverse')]
+
+    Notes
+    -----
+    The goal of this function is to visit edges. It differs from the more
+    familiar breadth-first-search of nodes, as provided by
+    :func:`networkx.algorithms.traversal.breadth_first_search.bfs_edges`, in
+    that it does not stop once every node has been visited. In a directed graph
+    with edges [(0, 1), (1, 2), (2, 1)], the edge (2, 1) would not be visited
+    if not for the functionality provided by this function.
+
+    The naming of this function is very similar to bfs_edges. The difference
+    is that 'edge_bfs' yields edges even if they extend back to an already
+    explored node while 'bfs_edges' yields the edges of the tree that results
+    from a breadth-first-search (BFS) so no edges are reported if they extend
+    to already explored nodes. That means 'edge_bfs' reports all edges while
+    'bfs_edges' only report those traversed by a node-based BFS. Yet another
+    description is that 'bfs_edges' reports the edges traversed during BFS
+    while 'edge_bfs' reports all edges in the order they are explored.
+
+    See Also
+    --------
+    bfs_edges
+    bfs_tree
+    edge_dfs
+
+    """
+    nodes = list(G.nbunch_iter(source))
+    if not nodes:
+        return
+
+    directed = G.is_directed()
+    kwds = {"data": False}
+    if G.is_multigraph() is True:
+        kwds["keys"] = True
+
+    # set up edge lookup
+    if orientation is None:
+
+        def edges_from(node):
+            return iter(G.edges(node, **kwds))
+
+    elif not directed or orientation == "original":
+
+        def edges_from(node):
+            for e in G.edges(node, **kwds):
+                yield e + (FORWARD,)
+
+    elif orientation == "reverse":
+
+        def edges_from(node):
+            for e in G.in_edges(node, **kwds):
+                yield e + (REVERSE,)
+
+    elif orientation == "ignore":
+
+        def edges_from(node):
+            for e in G.edges(node, **kwds):
+                yield e + (FORWARD,)
+            for e in G.in_edges(node, **kwds):
+                yield e + (REVERSE,)
+
+    else:
+        raise nx.NetworkXError("invalid orientation argument.")
+
+    if directed:
+        neighbors = G.successors
+
+        def edge_id(edge):
+            # remove direction indicator
+            return edge[:-1] if orientation is not None else edge
+
+    else:
+        neighbors = G.neighbors
+
+        def edge_id(edge):
+            return (frozenset(edge[:2]),) + edge[2:]
+
+    check_reverse = directed and orientation in ("reverse", "ignore")
+
+    # start BFS
+    visited_nodes = set(nodes)
+    visited_edges = set()
+    queue = deque([(n, edges_from(n)) for n in nodes])
+    while queue:
+        parent, children_edges = queue.popleft()
+        for edge in children_edges:
+            if check_reverse and edge[-1] == REVERSE:
+                child = edge[0]
+            else:
+                child = edge[1]
+            if child not in visited_nodes:
+                visited_nodes.add(child)
+                queue.append((child, edges_from(child)))
+            edgeid = edge_id(edge)
+            if edgeid not in visited_edges:
+                visited_edges.add(edgeid)
+                yield edge
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/edgedfs.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/edgedfs.py
new file mode 100644
index 00000000..8f657f39
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/edgedfs.py
@@ -0,0 +1,176 @@
+"""
+===========================
+Depth First Search on Edges
+===========================
+
+Algorithms for a depth-first traversal of edges in a graph.
+
+"""
+
+import networkx as nx
+
+FORWARD = "forward"
+REVERSE = "reverse"
+
+__all__ = ["edge_dfs"]
+
+
+@nx._dispatchable
+def edge_dfs(G, source=None, orientation=None):
+    """A directed, depth-first-search of edges in `G`, beginning at `source`.
+
+    Yield the edges of G in a depth-first-search order continuing until
+    all edges are generated.
+
+    Parameters
+    ----------
+    G : graph
+        A directed/undirected graph/multigraph.
+
+    source : node, list of nodes
+        The node from which the traversal begins. If None, then a source
+        is chosen arbitrarily and repeatedly until all edges from each node in
+        the graph are searched.
+
+    orientation : None | 'original' | 'reverse' | 'ignore' (default: None)
+        For directed graphs and directed multigraphs, edge traversals need not
+        respect the original orientation of the edges.
+        When set to 'reverse' every edge is traversed in the reverse direction.
+        When set to 'ignore', every edge is treated as undirected.
+        When set to 'original', every edge is treated as directed.
+        In all three cases, the yielded edge tuples add a last entry to
+        indicate the direction in which that edge was traversed.
+        If orientation is None, the yielded edge has no direction indicated.
+        The direction is respected, but not reported.
+
+    Yields
+    ------
+    edge : directed edge
+        A directed edge indicating the path taken by the depth-first traversal.
+        For graphs, `edge` is of the form `(u, v)` where `u` and `v`
+        are the tail and head of the edge as determined by the traversal.
+        For multigraphs, `edge` is of the form `(u, v, key)`, where `key` is
+        the key of the edge. When the graph is directed, then `u` and `v`
+        are always in the order of the actual directed edge.
+        If orientation is not None then the edge tuple is extended to include
+        the direction of traversal ('forward' or 'reverse') on that edge.
+
+    Examples
+    --------
+    >>> nodes = [0, 1, 2, 3]
+    >>> edges = [(0, 1), (1, 0), (1, 0), (2, 1), (3, 1)]
+
+    >>> list(nx.edge_dfs(nx.Graph(edges), nodes))
+    [(0, 1), (1, 2), (1, 3)]
+
+    >>> list(nx.edge_dfs(nx.DiGraph(edges), nodes))
+    [(0, 1), (1, 0), (2, 1), (3, 1)]
+
+    >>> list(nx.edge_dfs(nx.MultiGraph(edges), nodes))
+    [(0, 1, 0), (1, 0, 1), (0, 1, 2), (1, 2, 0), (1, 3, 0)]
+
+    >>> list(nx.edge_dfs(nx.MultiDiGraph(edges), nodes))
+    [(0, 1, 0), (1, 0, 0), (1, 0, 1), (2, 1, 0), (3, 1, 0)]
+
+    >>> list(nx.edge_dfs(nx.DiGraph(edges), nodes, orientation="ignore"))
+    [(0, 1, 'forward'), (1, 0, 'forward'), (2, 1, 'reverse'), (3, 1, 'reverse')]
+
+    >>> list(nx.edge_dfs(nx.MultiDiGraph(edges), nodes, orientation="ignore"))
+    [(0, 1, 0, 'forward'), (1, 0, 0, 'forward'), (1, 0, 1, 'reverse'), (2, 1, 0, 'reverse'), (3, 1, 0, 'reverse')]
+
+    Notes
+    -----
+    The goal of this function is to visit edges. It differs from the more
+    familiar depth-first traversal of nodes, as provided by
+    :func:`~networkx.algorithms.traversal.depth_first_search.dfs_edges`, in
+    that it does not stop once every node has been visited. In a directed graph
+    with edges [(0, 1), (1, 2), (2, 1)], the edge (2, 1) would not be visited
+    if not for the functionality provided by this function.
+
+    See Also
+    --------
+    :func:`~networkx.algorithms.traversal.depth_first_search.dfs_edges`
+
+    """
+    nodes = list(G.nbunch_iter(source))
+    if not nodes:
+        return
+
+    directed = G.is_directed()
+    kwds = {"data": False}
+    if G.is_multigraph() is True:
+        kwds["keys"] = True
+
+    # set up edge lookup
+    if orientation is None:
+
+        def edges_from(node):
+            return iter(G.edges(node, **kwds))
+
+    elif not directed or orientation == "original":
+
+        def edges_from(node):
+            for e in G.edges(node, **kwds):
+                yield e + (FORWARD,)
+
+    elif orientation == "reverse":
+
+        def edges_from(node):
+            for e in G.in_edges(node, **kwds):
+                yield e + (REVERSE,)
+
+    elif orientation == "ignore":
+
+        def edges_from(node):
+            for e in G.edges(node, **kwds):
+                yield e + (FORWARD,)
+            for e in G.in_edges(node, **kwds):
+                yield e + (REVERSE,)
+
+    else:
+        raise nx.NetworkXError("invalid orientation argument.")
+
+    # set up formation of edge_id to easily look up if edge already returned
+    if directed:
+
+        def edge_id(edge):
+            # remove direction indicator
+            return edge[:-1] if orientation is not None else edge
+
+    else:
+
+        def edge_id(edge):
+            # single id for undirected requires frozenset on nodes
+            return (frozenset(edge[:2]),) + edge[2:]
+
+    # Basic setup
+    check_reverse = directed and orientation in ("reverse", "ignore")
+
+    visited_edges = set()
+    visited_nodes = set()
+    edges = {}
+
+    # start DFS
+    for start_node in nodes:
+        stack = [start_node]
+        while stack:
+            current_node = stack[-1]
+            if current_node not in visited_nodes:
+                edges[current_node] = edges_from(current_node)
+                visited_nodes.add(current_node)
+
+            try:
+                edge = next(edges[current_node])
+            except StopIteration:
+                # No more edges from the current node.
+                stack.pop()
+            else:
+                edgeid = edge_id(edge)
+                if edgeid not in visited_edges:
+                    visited_edges.add(edgeid)
+                    # Mark the traversed "to" node as to-be-explored.
+                    if check_reverse and edge[-1] == REVERSE:
+                        stack.append(edge[0])
+                    else:
+                        stack.append(edge[1])
+                    yield edge
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_beamsearch.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_beamsearch.py
new file mode 100644
index 00000000..049f116b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_beamsearch.py
@@ -0,0 +1,25 @@
+"""Unit tests for the beam search functions."""
+
+import pytest
+
+import networkx as nx
+
+
+def test_narrow():
+    """Tests that a narrow beam width may cause an incomplete search."""
+    # In this search, we enqueue only the neighbor 3 at the first
+    # step, then only the neighbor 2 at the second step. Once at
+    # node 2, the search chooses node 3, since it has a higher value
+    # than node 1, but node 3 has already been visited, so the
+    # search terminates.
+    G = nx.cycle_graph(4)
+    edges = nx.bfs_beam_edges(G, source=0, value=lambda n: n, width=1)
+    assert list(edges) == [(0, 3), (3, 2)]
+
+
+@pytest.mark.parametrize("width", (2, None))
+def test_wide(width):
+    """All nodes are searched when `width` is None or >= max degree"""
+    G = nx.cycle_graph(4)
+    edges = nx.bfs_beam_edges(G, source=0, value=lambda n: n, width=width)
+    assert list(edges) == [(0, 3), (0, 1), (3, 2)]
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_bfs.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_bfs.py
new file mode 100644
index 00000000..fcfbbc68
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_bfs.py
@@ -0,0 +1,203 @@
+from functools import partial
+
+import pytest
+
+import networkx as nx
+
+
+class TestBFS:
+    @classmethod
+    def setup_class(cls):
+        # simple graph
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4)])
+        cls.G = G
+
+    def test_successor(self):
+        assert dict(nx.bfs_successors(self.G, source=0)) == {0: [1], 1: [2, 3], 2: [4]}
+
+    def test_predecessor(self):
+        assert dict(nx.bfs_predecessors(self.G, source=0)) == {1: 0, 2: 1, 3: 1, 4: 2}
+
+    def test_bfs_tree(self):
+        T = nx.bfs_tree(self.G, source=0)
+        assert sorted(T.nodes()) == sorted(self.G.nodes())
+        assert sorted(T.edges()) == [(0, 1), (1, 2), (1, 3), (2, 4)]
+
+    def test_bfs_edges(self):
+        edges = nx.bfs_edges(self.G, source=0)
+        assert list(edges) == [(0, 1), (1, 2), (1, 3), (2, 4)]
+
+    def test_bfs_edges_reverse(self):
+        D = nx.DiGraph()
+        D.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4)])
+        edges = nx.bfs_edges(D, source=4, reverse=True)
+        assert list(edges) == [(4, 2), (4, 3), (2, 1), (1, 0)]
+
+    def test_bfs_edges_sorting(self):
+        D = nx.DiGraph()
+        D.add_edges_from([(0, 1), (0, 2), (1, 4), (1, 3), (2, 5)])
+        sort_desc = partial(sorted, reverse=True)
+        edges_asc = nx.bfs_edges(D, source=0, sort_neighbors=sorted)
+        edges_desc = nx.bfs_edges(D, source=0, sort_neighbors=sort_desc)
+        assert list(edges_asc) == [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5)]
+        assert list(edges_desc) == [(0, 2), (0, 1), (2, 5), (1, 4), (1, 3)]
+
+    def test_bfs_tree_isolates(self):
+        G = nx.Graph()
+        G.add_node(1)
+        G.add_node(2)
+        T = nx.bfs_tree(G, source=1)
+        assert sorted(T.nodes()) == [1]
+        assert sorted(T.edges()) == []
+
+    def test_bfs_layers(self):
+        expected = {
+            0: [0],
+            1: [1],
+            2: [2, 3],
+            3: [4],
+        }
+        assert dict(enumerate(nx.bfs_layers(self.G, sources=[0]))) == expected
+        assert dict(enumerate(nx.bfs_layers(self.G, sources=0))) == expected
+
+    def test_bfs_layers_missing_source(self):
+        with pytest.raises(nx.NetworkXError):
+            next(nx.bfs_layers(self.G, sources="abc"))
+        with pytest.raises(nx.NetworkXError):
+            next(nx.bfs_layers(self.G, sources=["abc"]))
+
+    def test_descendants_at_distance(self):
+        for distance, descendants in enumerate([{0}, {1}, {2, 3}, {4}]):
+            assert nx.descendants_at_distance(self.G, 0, distance) == descendants
+
+    def test_descendants_at_distance_missing_source(self):
+        with pytest.raises(nx.NetworkXError):
+            nx.descendants_at_distance(self.G, "abc", 0)
+
+    def test_bfs_labeled_edges_directed(self):
+        D = nx.cycle_graph(5, create_using=nx.DiGraph)
+        expected = [
+            (0, 1, "tree"),
+            (1, 2, "tree"),
+            (2, 3, "tree"),
+            (3, 4, "tree"),
+            (4, 0, "reverse"),
+        ]
+        answer = list(nx.bfs_labeled_edges(D, 0))
+        assert expected == answer
+
+        D.add_edge(4, 4)
+        expected.append((4, 4, "level"))
+        answer = list(nx.bfs_labeled_edges(D, 0))
+        assert expected == answer
+
+        D.add_edge(0, 2)
+        D.add_edge(1, 5)
+        D.add_edge(2, 5)
+        D.remove_edge(4, 4)
+        expected = [
+            (0, 1, "tree"),
+            (0, 2, "tree"),
+            (1, 2, "level"),
+            (1, 5, "tree"),
+            (2, 3, "tree"),
+            (2, 5, "forward"),
+            (3, 4, "tree"),
+            (4, 0, "reverse"),
+        ]
+        answer = list(nx.bfs_labeled_edges(D, 0))
+        assert expected == answer
+
+        G = D.to_undirected()
+        G.add_edge(4, 4)
+        expected = [
+            (0, 1, "tree"),
+            (0, 2, "tree"),
+            (0, 4, "tree"),
+            (1, 2, "level"),
+            (1, 5, "tree"),
+            (2, 3, "tree"),
+            (2, 5, "forward"),
+            (4, 3, "forward"),
+            (4, 4, "level"),
+        ]
+        answer = list(nx.bfs_labeled_edges(G, 0))
+        assert expected == answer
+
+
+class TestBreadthLimitedSearch:
+    @classmethod
+    def setup_class(cls):
+        # a tree
+        G = nx.Graph()
+        nx.add_path(G, [0, 1, 2, 3, 4, 5, 6])
+        nx.add_path(G, [2, 7, 8, 9, 10])
+        cls.G = G
+        # a disconnected graph
+        D = nx.Graph()
+        D.add_edges_from([(0, 1), (2, 3)])
+        nx.add_path(D, [2, 7, 8, 9, 10])
+        cls.D = D
+
+    def test_limited_bfs_successor(self):
+        assert dict(nx.bfs_successors(self.G, source=1, depth_limit=3)) == {
+            1: [0, 2],
+            2: [3, 7],
+            3: [4],
+            7: [8],
+        }
+        result = {
+            n: sorted(s) for n, s in nx.bfs_successors(self.D, source=7, depth_limit=2)
+        }
+        assert result == {8: [9], 2: [3], 7: [2, 8]}
+
+    def test_limited_bfs_predecessor(self):
+        assert dict(nx.bfs_predecessors(self.G, source=1, depth_limit=3)) == {
+            0: 1,
+            2: 1,
+            3: 2,
+            4: 3,
+            7: 2,
+            8: 7,
+        }
+        assert dict(nx.bfs_predecessors(self.D, source=7, depth_limit=2)) == {
+            2: 7,
+            3: 2,
+            8: 7,
+            9: 8,
+        }
+
+    def test_limited_bfs_tree(self):
+        T = nx.bfs_tree(self.G, source=3, depth_limit=1)
+        assert sorted(T.edges()) == [(3, 2), (3, 4)]
+
+    def test_limited_bfs_edges(self):
+        edges = nx.bfs_edges(self.G, source=9, depth_limit=4)
+        assert list(edges) == [(9, 8), (9, 10), (8, 7), (7, 2), (2, 1), (2, 3)]
+
+    def test_limited_bfs_layers(self):
+        assert dict(enumerate(nx.bfs_layers(self.G, sources=[0]))) == {
+            0: [0],
+            1: [1],
+            2: [2],
+            3: [3, 7],
+            4: [4, 8],
+            5: [5, 9],
+            6: [6, 10],
+        }
+        assert dict(enumerate(nx.bfs_layers(self.D, sources=2))) == {
+            0: [2],
+            1: [3, 7],
+            2: [8],
+            3: [9],
+            4: [10],
+        }
+
+    def test_limited_descendants_at_distance(self):
+        for distance, descendants in enumerate(
+            [{0}, {1}, {2}, {3, 7}, {4, 8}, {5, 9}, {6, 10}]
+        ):
+            assert nx.descendants_at_distance(self.G, 0, distance) == descendants
+        for distance, descendants in enumerate([{2}, {3, 7}, {8}, {9}, {10}]):
+            assert nx.descendants_at_distance(self.D, 2, distance) == descendants
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_dfs.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_dfs.py
new file mode 100644
index 00000000..e43d7d61
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_dfs.py
@@ -0,0 +1,305 @@
+import networkx as nx
+
+
+class TestDFS:
+    @classmethod
+    def setup_class(cls):
+        # simple graph
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 0), (0, 4)])
+        cls.G = G
+        # simple graph, disconnected
+        D = nx.Graph()
+        D.add_edges_from([(0, 1), (2, 3)])
+        cls.D = D
+
+    def test_preorder_nodes(self):
+        assert list(nx.dfs_preorder_nodes(self.G, source=0)) == [0, 1, 2, 4, 3]
+        assert list(nx.dfs_preorder_nodes(self.D)) == [0, 1, 2, 3]
+        assert list(nx.dfs_preorder_nodes(self.D, source=2)) == [2, 3]
+
+    def test_postorder_nodes(self):
+        assert list(nx.dfs_postorder_nodes(self.G, source=0)) == [4, 2, 3, 1, 0]
+        assert list(nx.dfs_postorder_nodes(self.D)) == [1, 0, 3, 2]
+        assert list(nx.dfs_postorder_nodes(self.D, source=0)) == [1, 0]
+
+    def test_successor(self):
+        assert nx.dfs_successors(self.G, source=0) == {0: [1], 1: [2, 3], 2: [4]}
+        assert nx.dfs_successors(self.G, source=1) == {0: [3, 4], 1: [0], 4: [2]}
+        assert nx.dfs_successors(self.D) == {0: [1], 2: [3]}
+        assert nx.dfs_successors(self.D, source=1) == {1: [0]}
+
+    def test_predecessor(self):
+        assert nx.dfs_predecessors(self.G, source=0) == {1: 0, 2: 1, 3: 1, 4: 2}
+        assert nx.dfs_predecessors(self.D) == {1: 0, 3: 2}
+
+    def test_dfs_tree(self):
+        exp_nodes = sorted(self.G.nodes())
+        exp_edges = [(0, 1), (1, 2), (1, 3), (2, 4)]
+        # Search from first node
+        T = nx.dfs_tree(self.G, source=0)
+        assert sorted(T.nodes()) == exp_nodes
+        assert sorted(T.edges()) == exp_edges
+        # Check source=None
+        T = nx.dfs_tree(self.G, source=None)
+        assert sorted(T.nodes()) == exp_nodes
+        assert sorted(T.edges()) == exp_edges
+        # Check source=None is the default
+        T = nx.dfs_tree(self.G)
+        assert sorted(T.nodes()) == exp_nodes
+        assert sorted(T.edges()) == exp_edges
+
+    def test_dfs_edges(self):
+        edges = nx.dfs_edges(self.G, source=0)
+        assert list(edges) == [(0, 1), (1, 2), (2, 4), (1, 3)]
+        edges = nx.dfs_edges(self.D)
+        assert list(edges) == [(0, 1), (2, 3)]
+
+    def test_dfs_edges_sorting(self):
+        G = nx.Graph([(0, 1), (1, 2), (1, 3), (2, 4), (3, 0), (0, 4)])
+        edges_asc = nx.dfs_edges(G, source=0, sort_neighbors=sorted)
+        sorted_desc = lambda x: sorted(x, reverse=True)
+        edges_desc = nx.dfs_edges(G, source=0, sort_neighbors=sorted_desc)
+        assert list(edges_asc) == [(0, 1), (1, 2), (2, 4), (1, 3)]
+        assert list(edges_desc) == [(0, 4), (4, 2), (2, 1), (1, 3)]
+
+    def test_dfs_labeled_edges(self):
+        edges = list(nx.dfs_labeled_edges(self.G, source=0))
+        forward = [(u, v) for (u, v, d) in edges if d == "forward"]
+        assert forward == [(0, 0), (0, 1), (1, 2), (2, 4), (1, 3)]
+        assert edges == [
+            (0, 0, "forward"),
+            (0, 1, "forward"),
+            (1, 0, "nontree"),
+            (1, 2, "forward"),
+            (2, 1, "nontree"),
+            (2, 4, "forward"),
+            (4, 2, "nontree"),
+            (4, 0, "nontree"),
+            (2, 4, "reverse"),
+            (1, 2, "reverse"),
+            (1, 3, "forward"),
+            (3, 1, "nontree"),
+            (3, 0, "nontree"),
+            (1, 3, "reverse"),
+            (0, 1, "reverse"),
+            (0, 3, "nontree"),
+            (0, 4, "nontree"),
+            (0, 0, "reverse"),
+        ]
+
+    def test_dfs_labeled_edges_sorting(self):
+        G = nx.Graph([(0, 1), (1, 2), (1, 3), (2, 4), (3, 0), (0, 4)])
+        edges_asc = nx.dfs_labeled_edges(G, source=0, sort_neighbors=sorted)
+        sorted_desc = lambda x: sorted(x, reverse=True)
+        edges_desc = nx.dfs_labeled_edges(G, source=0, sort_neighbors=sorted_desc)
+        assert list(edges_asc) == [
+            (0, 0, "forward"),
+            (0, 1, "forward"),
+            (1, 0, "nontree"),
+            (1, 2, "forward"),
+            (2, 1, "nontree"),
+            (2, 4, "forward"),
+            (4, 0, "nontree"),
+            (4, 2, "nontree"),
+            (2, 4, "reverse"),
+            (1, 2, "reverse"),
+            (1, 3, "forward"),
+            (3, 0, "nontree"),
+            (3, 1, "nontree"),
+            (1, 3, "reverse"),
+            (0, 1, "reverse"),
+            (0, 3, "nontree"),
+            (0, 4, "nontree"),
+            (0, 0, "reverse"),
+        ]
+        assert list(edges_desc) == [
+            (0, 0, "forward"),
+            (0, 4, "forward"),
+            (4, 2, "forward"),
+            (2, 4, "nontree"),
+            (2, 1, "forward"),
+            (1, 3, "forward"),
+            (3, 1, "nontree"),
+            (3, 0, "nontree"),
+            (1, 3, "reverse"),
+            (1, 2, "nontree"),
+            (1, 0, "nontree"),
+            (2, 1, "reverse"),
+            (4, 2, "reverse"),
+            (4, 0, "nontree"),
+            (0, 4, "reverse"),
+            (0, 3, "nontree"),
+            (0, 1, "nontree"),
+            (0, 0, "reverse"),
+        ]
+
+    def test_dfs_labeled_disconnected_edges(self):
+        edges = list(nx.dfs_labeled_edges(self.D))
+        forward = [(u, v) for (u, v, d) in edges if d == "forward"]
+        assert forward == [(0, 0), (0, 1), (2, 2), (2, 3)]
+        assert edges == [
+            (0, 0, "forward"),
+            (0, 1, "forward"),
+            (1, 0, "nontree"),
+            (0, 1, "reverse"),
+            (0, 0, "reverse"),
+            (2, 2, "forward"),
+            (2, 3, "forward"),
+            (3, 2, "nontree"),
+            (2, 3, "reverse"),
+            (2, 2, "reverse"),
+        ]
+
+    def test_dfs_tree_isolates(self):
+        G = nx.Graph()
+        G.add_node(1)
+        G.add_node(2)
+        T = nx.dfs_tree(G, source=1)
+        assert sorted(T.nodes()) == [1]
+        assert sorted(T.edges()) == []
+        T = nx.dfs_tree(G, source=None)
+        assert sorted(T.nodes()) == [1, 2]
+        assert sorted(T.edges()) == []
+
+
+class TestDepthLimitedSearch:
+    @classmethod
+    def setup_class(cls):
+        # a tree
+        G = nx.Graph()
+        nx.add_path(G, [0, 1, 2, 3, 4, 5, 6])
+        nx.add_path(G, [2, 7, 8, 9, 10])
+        cls.G = G
+        # a disconnected graph
+        D = nx.Graph()
+        D.add_edges_from([(0, 1), (2, 3)])
+        nx.add_path(D, [2, 7, 8, 9, 10])
+        cls.D = D
+
+    def test_dls_preorder_nodes(self):
+        assert list(nx.dfs_preorder_nodes(self.G, source=0, depth_limit=2)) == [0, 1, 2]
+        assert list(nx.dfs_preorder_nodes(self.D, source=1, depth_limit=2)) == ([1, 0])
+
+    def test_dls_postorder_nodes(self):
+        assert list(nx.dfs_postorder_nodes(self.G, source=3, depth_limit=3)) == [
+            1,
+            7,
+            2,
+            5,
+            4,
+            3,
+        ]
+        assert list(nx.dfs_postorder_nodes(self.D, source=2, depth_limit=2)) == (
+            [3, 7, 2]
+        )
+
+    def test_dls_successor(self):
+        result = nx.dfs_successors(self.G, source=4, depth_limit=3)
+        assert {n: set(v) for n, v in result.items()} == {
+            2: {1, 7},
+            3: {2},
+            4: {3, 5},
+            5: {6},
+        }
+        result = nx.dfs_successors(self.D, source=7, depth_limit=2)
+        assert {n: set(v) for n, v in result.items()} == {8: {9}, 2: {3}, 7: {8, 2}}
+
+    def test_dls_predecessor(self):
+        assert nx.dfs_predecessors(self.G, source=0, depth_limit=3) == {
+            1: 0,
+            2: 1,
+            3: 2,
+            7: 2,
+        }
+        assert nx.dfs_predecessors(self.D, source=2, depth_limit=3) == {
+            8: 7,
+            9: 8,
+            3: 2,
+            7: 2,
+        }
+
+    def test_dls_tree(self):
+        T = nx.dfs_tree(self.G, source=3, depth_limit=1)
+        assert sorted(T.edges()) == [(3, 2), (3, 4)]
+
+    def test_dls_edges(self):
+        edges = nx.dfs_edges(self.G, source=9, depth_limit=4)
+        assert list(edges) == [(9, 8), (8, 7), (7, 2), (2, 1), (2, 3), (9, 10)]
+
+    def test_dls_labeled_edges_depth_1(self):
+        edges = list(nx.dfs_labeled_edges(self.G, source=5, depth_limit=1))
+        forward = [(u, v) for (u, v, d) in edges if d == "forward"]
+        assert forward == [(5, 5), (5, 4), (5, 6)]
+        # Note: reverse-depth_limit edge types were not reported before gh-6240
+        assert edges == [
+            (5, 5, "forward"),
+            (5, 4, "forward"),
+            (5, 4, "reverse-depth_limit"),
+            (5, 6, "forward"),
+            (5, 6, "reverse-depth_limit"),
+            (5, 5, "reverse"),
+        ]
+
+    def test_dls_labeled_edges_depth_2(self):
+        edges = list(nx.dfs_labeled_edges(self.G, source=6, depth_limit=2))
+        forward = [(u, v) for (u, v, d) in edges if d == "forward"]
+        assert forward == [(6, 6), (6, 5), (5, 4)]
+        assert edges == [
+            (6, 6, "forward"),
+            (6, 5, "forward"),
+            (5, 4, "forward"),
+            (5, 4, "reverse-depth_limit"),
+            (5, 6, "nontree"),
+            (6, 5, "reverse"),
+            (6, 6, "reverse"),
+        ]
+
+    def test_dls_labeled_disconnected_edges(self):
+        edges = list(nx.dfs_labeled_edges(self.D, depth_limit=1))
+        assert edges == [
+            (0, 0, "forward"),
+            (0, 1, "forward"),
+            (0, 1, "reverse-depth_limit"),
+            (0, 0, "reverse"),
+            (2, 2, "forward"),
+            (2, 3, "forward"),
+            (2, 3, "reverse-depth_limit"),
+            (2, 7, "forward"),
+            (2, 7, "reverse-depth_limit"),
+            (2, 2, "reverse"),
+            (8, 8, "forward"),
+            (8, 7, "nontree"),
+            (8, 9, "forward"),
+            (8, 9, "reverse-depth_limit"),
+            (8, 8, "reverse"),
+            (10, 10, "forward"),
+            (10, 9, "nontree"),
+            (10, 10, "reverse"),
+        ]
+        # large depth_limit has no impact
+        edges = list(nx.dfs_labeled_edges(self.D, depth_limit=19))
+        assert edges == [
+            (0, 0, "forward"),
+            (0, 1, "forward"),
+            (1, 0, "nontree"),
+            (0, 1, "reverse"),
+            (0, 0, "reverse"),
+            (2, 2, "forward"),
+            (2, 3, "forward"),
+            (3, 2, "nontree"),
+            (2, 3, "reverse"),
+            (2, 7, "forward"),
+            (7, 2, "nontree"),
+            (7, 8, "forward"),
+            (8, 7, "nontree"),
+            (8, 9, "forward"),
+            (9, 8, "nontree"),
+            (9, 10, "forward"),
+            (10, 9, "nontree"),
+            (9, 10, "reverse"),
+            (8, 9, "reverse"),
+            (7, 8, "reverse"),
+            (2, 7, "reverse"),
+            (2, 2, "reverse"),
+        ]
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_edgebfs.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_edgebfs.py
new file mode 100644
index 00000000..1bf3fae0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_edgebfs.py
@@ -0,0 +1,147 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms.traversal.edgedfs import FORWARD, REVERSE
+
+
+class TestEdgeBFS:
+    @classmethod
+    def setup_class(cls):
+        cls.nodes = [0, 1, 2, 3]
+        cls.edges = [(0, 1), (1, 0), (1, 0), (2, 0), (2, 1), (3, 1)]
+
+    def test_empty(self):
+        G = nx.Graph()
+        edges = list(nx.edge_bfs(G))
+        assert edges == []
+
+    def test_graph_single_source(self):
+        G = nx.Graph(self.edges)
+        G.add_edge(4, 5)
+        x = list(nx.edge_bfs(G, [0]))
+        x_ = [(0, 1), (0, 2), (1, 2), (1, 3)]
+        assert x == x_
+
+    def test_graph(self):
+        G = nx.Graph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes))
+        x_ = [(0, 1), (0, 2), (1, 2), (1, 3)]
+        assert x == x_
+
+    def test_digraph(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes))
+        x_ = [(0, 1), (1, 0), (2, 0), (2, 1), (3, 1)]
+        assert x == x_
+
+    def test_digraph_orientation_invalid(self):
+        G = nx.DiGraph(self.edges)
+        edge_iterator = nx.edge_bfs(G, self.nodes, orientation="hello")
+        pytest.raises(nx.NetworkXError, list, edge_iterator)
+
+    def test_digraph_orientation_none(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes, orientation=None))
+        x_ = [(0, 1), (1, 0), (2, 0), (2, 1), (3, 1)]
+        assert x == x_
+
+    def test_digraph_orientation_original(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes, orientation="original"))
+        x_ = [
+            (0, 1, FORWARD),
+            (1, 0, FORWARD),
+            (2, 0, FORWARD),
+            (2, 1, FORWARD),
+            (3, 1, FORWARD),
+        ]
+        assert x == x_
+
+    def test_digraph2(self):
+        G = nx.DiGraph()
+        nx.add_path(G, range(4))
+        x = list(nx.edge_bfs(G, [0]))
+        x_ = [(0, 1), (1, 2), (2, 3)]
+        assert x == x_
+
+    def test_digraph_rev(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes, orientation="reverse"))
+        x_ = [
+            (1, 0, REVERSE),
+            (2, 0, REVERSE),
+            (0, 1, REVERSE),
+            (2, 1, REVERSE),
+            (3, 1, REVERSE),
+        ]
+        assert x == x_
+
+    def test_digraph_rev2(self):
+        G = nx.DiGraph()
+        nx.add_path(G, range(4))
+        x = list(nx.edge_bfs(G, [3], orientation="reverse"))
+        x_ = [(2, 3, REVERSE), (1, 2, REVERSE), (0, 1, REVERSE)]
+        assert x == x_
+
+    def test_multigraph(self):
+        G = nx.MultiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes))
+        x_ = [(0, 1, 0), (0, 1, 1), (0, 1, 2), (0, 2, 0), (1, 2, 0), (1, 3, 0)]
+        # This is an example of where hash randomization can break.
+        # There are 3! * 2 alternative outputs, such as:
+        #    [(0, 1, 1), (1, 0, 0), (0, 1, 2), (1, 3, 0), (1, 2, 0)]
+        # But note, the edges (1,2,0) and (1,3,0) always follow the (0,1,k)
+        # edges. So the algorithm only guarantees a partial order. A total
+        # order is guaranteed only if the graph data structures are ordered.
+        assert x == x_
+
+    def test_multidigraph(self):
+        G = nx.MultiDiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes))
+        x_ = [(0, 1, 0), (1, 0, 0), (1, 0, 1), (2, 0, 0), (2, 1, 0), (3, 1, 0)]
+        assert x == x_
+
+    def test_multidigraph_rev(self):
+        G = nx.MultiDiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes, orientation="reverse"))
+        x_ = [
+            (1, 0, 0, REVERSE),
+            (1, 0, 1, REVERSE),
+            (2, 0, 0, REVERSE),
+            (0, 1, 0, REVERSE),
+            (2, 1, 0, REVERSE),
+            (3, 1, 0, REVERSE),
+        ]
+        assert x == x_
+
+    def test_digraph_ignore(self):
+        G = nx.DiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes, orientation="ignore"))
+        x_ = [
+            (0, 1, FORWARD),
+            (1, 0, REVERSE),
+            (2, 0, REVERSE),
+            (2, 1, REVERSE),
+            (3, 1, REVERSE),
+        ]
+        assert x == x_
+
+    def test_digraph_ignore2(self):
+        G = nx.DiGraph()
+        nx.add_path(G, range(4))
+        x = list(nx.edge_bfs(G, [0], orientation="ignore"))
+        x_ = [(0, 1, FORWARD), (1, 2, FORWARD), (2, 3, FORWARD)]
+        assert x == x_
+
+    def test_multidigraph_ignore(self):
+        G = nx.MultiDiGraph(self.edges)
+        x = list(nx.edge_bfs(G, self.nodes, orientation="ignore"))
+        x_ = [
+            (0, 1, 0, FORWARD),
+            (1, 0, 0, REVERSE),
+            (1, 0, 1, REVERSE),
+            (2, 0, 0, REVERSE),
+            (2, 1, 0, REVERSE),
+            (3, 1, 0, REVERSE),
+        ]
+        assert x == x_
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_edgedfs.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_edgedfs.py
new file mode 100644
index 00000000..7c1967cc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/traversal/tests/test_edgedfs.py
@@ -0,0 +1,131 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import edge_dfs
+from networkx.algorithms.traversal.edgedfs import FORWARD, REVERSE
+
+# These tests can fail with hash randomization. The easiest and clearest way
+# to write these unit tests is for the edges to be output in an expected total
+# order, but we cannot guarantee the order amongst outgoing edges from a node,
+# unless each class uses an ordered data structure for neighbors. This is
+# painful to do with the current API. The alternative is that the tests are
+# written (IMO confusingly) so that there is not a total order over the edges,
+# but only a partial order. Due to the small size of the graphs, hopefully
+# failures due to hash randomization will not occur. For an example of how
+# this can fail, see TestEdgeDFS.test_multigraph.
+
+
+class TestEdgeDFS:
+    @classmethod
+    def setup_class(cls):
+        cls.nodes = [0, 1, 2, 3]
+        cls.edges = [(0, 1), (1, 0), (1, 0), (2, 1), (3, 1)]
+
+    def test_empty(self):
+        G = nx.Graph()
+        edges = list(edge_dfs(G))
+        assert edges == []
+
+    def test_graph(self):
+        G = nx.Graph(self.edges)
+        x = list(edge_dfs(G, self.nodes))
+        x_ = [(0, 1), (1, 2), (1, 3)]
+        assert x == x_
+
+    def test_digraph(self):
+        G = nx.DiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes))
+        x_ = [(0, 1), (1, 0), (2, 1), (3, 1)]
+        assert x == x_
+
+    def test_digraph_orientation_invalid(self):
+        G = nx.DiGraph(self.edges)
+        edge_iterator = edge_dfs(G, self.nodes, orientation="hello")
+        pytest.raises(nx.NetworkXError, list, edge_iterator)
+
+    def test_digraph_orientation_none(self):
+        G = nx.DiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes, orientation=None))
+        x_ = [(0, 1), (1, 0), (2, 1), (3, 1)]
+        assert x == x_
+
+    def test_digraph_orientation_original(self):
+        G = nx.DiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes, orientation="original"))
+        x_ = [(0, 1, FORWARD), (1, 0, FORWARD), (2, 1, FORWARD), (3, 1, FORWARD)]
+        assert x == x_
+
+    def test_digraph2(self):
+        G = nx.DiGraph()
+        nx.add_path(G, range(4))
+        x = list(edge_dfs(G, [0]))
+        x_ = [(0, 1), (1, 2), (2, 3)]
+        assert x == x_
+
+    def test_digraph_rev(self):
+        G = nx.DiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes, orientation="reverse"))
+        x_ = [(1, 0, REVERSE), (0, 1, REVERSE), (2, 1, REVERSE), (3, 1, REVERSE)]
+        assert x == x_
+
+    def test_digraph_rev2(self):
+        G = nx.DiGraph()
+        nx.add_path(G, range(4))
+        x = list(edge_dfs(G, [3], orientation="reverse"))
+        x_ = [(2, 3, REVERSE), (1, 2, REVERSE), (0, 1, REVERSE)]
+        assert x == x_
+
+    def test_multigraph(self):
+        G = nx.MultiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes))
+        x_ = [(0, 1, 0), (1, 0, 1), (0, 1, 2), (1, 2, 0), (1, 3, 0)]
+        # This is an example of where hash randomization can break.
+        # There are 3! * 2 alternative outputs, such as:
+        #    [(0, 1, 1), (1, 0, 0), (0, 1, 2), (1, 3, 0), (1, 2, 0)]
+        # But note, the edges (1,2,0) and (1,3,0) always follow the (0,1,k)
+        # edges. So the algorithm only guarantees a partial order. A total
+        # order is guaranteed only if the graph data structures are ordered.
+        assert x == x_
+
+    def test_multidigraph(self):
+        G = nx.MultiDiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes))
+        x_ = [(0, 1, 0), (1, 0, 0), (1, 0, 1), (2, 1, 0), (3, 1, 0)]
+        assert x == x_
+
+    def test_multidigraph_rev(self):
+        G = nx.MultiDiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes, orientation="reverse"))
+        x_ = [
+            (1, 0, 0, REVERSE),
+            (0, 1, 0, REVERSE),
+            (1, 0, 1, REVERSE),
+            (2, 1, 0, REVERSE),
+            (3, 1, 0, REVERSE),
+        ]
+        assert x == x_
+
+    def test_digraph_ignore(self):
+        G = nx.DiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes, orientation="ignore"))
+        x_ = [(0, 1, FORWARD), (1, 0, FORWARD), (2, 1, REVERSE), (3, 1, REVERSE)]
+        assert x == x_
+
+    def test_digraph_ignore2(self):
+        G = nx.DiGraph()
+        nx.add_path(G, range(4))
+        x = list(edge_dfs(G, [0], orientation="ignore"))
+        x_ = [(0, 1, FORWARD), (1, 2, FORWARD), (2, 3, FORWARD)]
+        assert x == x_
+
+    def test_multidigraph_ignore(self):
+        G = nx.MultiDiGraph(self.edges)
+        x = list(edge_dfs(G, self.nodes, orientation="ignore"))
+        x_ = [
+            (0, 1, 0, FORWARD),
+            (1, 0, 0, FORWARD),
+            (1, 0, 1, REVERSE),
+            (2, 1, 0, REVERSE),
+            (3, 1, 0, REVERSE),
+        ]
+        assert x == x_