I am done

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

View File

@ -0,0 +1,5 @@
from .beamsearch import *
from .breadth_first_search import *
from .depth_first_search import *
from .edgedfs import *
from .edgebfs import *

View File

@ -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)

View File

@ -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()

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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)]

View File

@ -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

View File

@ -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"),
]

View File

@ -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_

View File

@ -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_