Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 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,87 @@
"""Basic algorithms for breadth-first searching the nodes of a graph."""
from .breadth_first_search import generic_bfs_edges
__all__ = ["bfs_beam_edges"]
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.
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)
>>> source = 0
>>> width = 5
>>> for u, v in nx.bfs_beam_edges(G, source, centrality.get, width):
... print((u, v)) # doctest: +SKIP
"""
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.
The list returned by this function is in decreasing value as
measured by the `value` function.
"""
# 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 generic_bfs_edges(G, source, successors)

View file

@ -0,0 +1,399 @@
"""Basic algorithms for breadth-first searching the nodes of a graph."""
import networkx as nx
from collections import deque
__all__ = [
"bfs_edges",
"bfs_tree",
"bfs_predecessors",
"bfs_successors",
"descendants_at_distance",
]
def generic_bfs_edges(G, source, neighbors=None, depth_limit=None, sort_neighbors=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. 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
sort_neighbors : function
A function that takes the list of neighbors of given node as input, and
returns an *iterator* over these neighbors but with custom ordering.
Yields
------
edge
Edges in the breadth-first search starting from `source`.
Examples
--------
>>> G = nx.path_graph(3)
>>> print(list(nx.bfs_edges(G, 0)))
[(0, 1), (1, 2)]
>>> print(list(nx.bfs_edges(G, source=0, depth_limit=1)))
[(0, 1)]
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 callable(sort_neighbors):
_neighbors = neighbors
neighbors = lambda node: iter(sort_neighbors(_neighbors(node)))
visited = {source}
if depth_limit is None:
depth_limit = len(G)
queue = deque([(source, depth_limit, neighbors(source))])
while queue:
parent, depth_now, children = queue[0]
try:
child = next(children)
if child not in visited:
yield parent, child
visited.add(child)
if depth_now > 1:
queue.append((child, depth_now - 1, neighbors(child)))
except StopIteration:
queue.popleft()
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
A function that takes the list of neighbors of given node as input, and
returns an *iterator* over these neighbors but with custom ordering.
Returns
-------
edges: generator
A generator of edges in 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 edge_bfs. 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 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 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
dfs_edges
edge_bfs
"""
if reverse and G.is_directed():
successors = G.predecessors
else:
successors = G.neighbors
yield from generic_bfs_edges(G, source, successors, depth_limit, sort_neighbors)
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
A function that takes the list of neighbors of given node as input, and
returns an *iterator* over these neighbors but with custom ordering.
Returns
-------
T: NetworkX DiGraph
An oriented tree
Examples
--------
>>> G = nx.path_graph(3)
>>> print(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])
>>> print(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
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
A function that takes the list of neighbors of given node as input, and
returns an *iterator* over these neighbors but with custom ordering.
Returns
-------
pred: iterator
(node, predecessors) iterator where predecessors is the list of
predecessors of the node.
Examples
--------
>>> G = nx.path_graph(3)
>>> print(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)])
>>> print(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])
>>> print(sorted(nx.bfs_predecessors(M, source=1, depth_limit=3)))
[(0, 1), (2, 1), (3, 2), (4, 3), (7, 2), (8, 7)]
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)
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
A function that takes the list of neighbors of given node as input, and
returns an *iterator* over these neighbors but with custom ordering.
Returns
-------
succ: iterator
(node, successors) iterator where successors is the list of
successors of the node.
Examples
--------
>>> G = nx.path_graph(3)
>>> print(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)])
>>> print(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])
>>> print(dict(nx.bfs_successors(G, source=1, depth_limit=3)))
{1: [0, 2], 2: [3, 7], 3: [4], 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
--------
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)
def descendants_at_distance(G, source, distance):
"""Returns all nodes at a fixed `distance` from `source` in `G`.
Parameters
----------
G : NetworkX DiGraph
A directed 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`
"""
if not G.has_node(source):
raise nx.NetworkXError(f"The node {source} is not in the graph.")
current_distance = 0
queue = {source}
visited = {source}
# this is basically BFS, except that the queue only stores the nodes at
# current_distance from source at each iteration
while queue:
if current_distance == distance:
return queue
current_distance += 1
next_vertices = set()
for vertex in queue:
for child in G[vertex]:
if child not in visited:
visited.add(child)
next_vertices.add(child)
queue = next_vertices
return set()

View file

@ -0,0 +1,439 @@
"""Basic algorithms for depth-first searching the nodes of a graph."""
import networkx as nx
from collections import defaultdict
__all__ = [
"dfs_edges",
"dfs_tree",
"dfs_predecessors",
"dfs_successors",
"dfs_preorder_nodes",
"dfs_postorder_nodes",
"dfs_labeled_edges",
]
def dfs_edges(G, source=None, depth_limit=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 edge_dfs).
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.
Returns
-------
edges: generator
A generator of edges in 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`_, 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
edge_dfs
bfs_edges
"""
if source is None:
# edges for all components
nodes = G
else:
# edges for components with source
nodes = [source]
visited = set()
if depth_limit is None:
depth_limit = len(G)
for start in nodes:
if start in visited:
continue
visited.add(start)
stack = [(start, depth_limit, iter(G[start]))]
while stack:
parent, depth_now, children = stack[-1]
try:
child = next(children)
if child not in visited:
yield parent, child
visited.add(child)
if depth_now > 1:
stack.append((child, depth_now - 1, iter(G[child])))
except StopIteration:
stack.pop()
def dfs_tree(G, source=None, depth_limit=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.
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
edge_dfs
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))
return T
def dfs_predecessors(G, source=None, depth_limit=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.
depth_limit : int, optional (default=len(G))
Specify the maximum search depth.
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
edge_dfs
bfs_tree
"""
return {t: s for s, t in dfs_edges(G, source, depth_limit)}
def dfs_successors(G, source=None, depth_limit=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.
depth_limit : int, optional (default=len(G))
Specify the maximum search depth.
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
edge_dfs
bfs_tree
"""
d = defaultdict(list)
for s, t in dfs_edges(G, source=source, depth_limit=depth_limit):
d[s].append(t)
return dict(d)
def dfs_postorder_nodes(G, source=None, depth_limit=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.
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
edge_dfs
bfs_tree
"""
edges = nx.dfs_labeled_edges(G, source=source, depth_limit=depth_limit)
return (v for u, v, d in edges if d == "reverse")
def dfs_preorder_nodes(G, source=None, depth_limit=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.
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
bfs_edges
"""
edges = nx.dfs_labeled_edges(G, source=source, depth_limit=depth_limit)
return (v for u, v, d in edges if d == "forward")
def dfs_labeled_edges(G, source=None, depth_limit=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.
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', or 'reverse'. 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
on in which both *u* and *v* have been visited and the edge is in
the DFS tree.
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]
visited = set()
if depth_limit is None:
depth_limit = len(G)
for start in nodes:
if start in visited:
continue
yield start, start, "forward"
visited.add(start)
stack = [(start, depth_limit, iter(G[start]))]
while stack:
parent, depth_now, children = stack[-1]
try:
child = next(children)
if child in visited:
yield parent, child, "nontree"
else:
yield parent, child, "forward"
visited.add(child)
if depth_now > 1:
stack.append((child, depth_now - 1, iter(G[child])))
except StopIteration:
stack.pop()
if stack:
yield stack[-1][0], parent, "reverse"
yield start, start, "reverse"

View file

@ -0,0 +1,175 @@
"""
=============================
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"]
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 = {n for n in 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,174 @@
"""
===========================
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"]
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
--------
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,27 @@
"""Unit tests for the beam search functions."""
import networkx as nx
def identity(x):
return x
class TestBeamSearch:
"""Unit tests for the beam search function."""
def test_narrow(self):
"""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
# that node 1, but node 3 has already been visited, so the
# search terminates.
G = nx.cycle_graph(4)
edges = nx.bfs_beam_edges(G, 0, identity, width=1)
assert list(edges) == [(0, 3), (3, 2)]
def test_wide(self):
G = nx.cycle_graph(4)
edges = nx.bfs_beam_edges(G, 0, identity, width=2)
assert list(edges) == [(0, 3), (0, 1), (3, 2)]

View file

@ -0,0 +1,100 @@
from functools import partial
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()) == []
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)]

View file

@ -0,0 +1,148 @@
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, 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]
def test_postorder_nodes(self):
assert list(nx.dfs_postorder_nodes(self.G, source=0)) == [3, 4, 2, 1, 0]
assert list(nx.dfs_postorder_nodes(self.D)) == [1, 0, 3, 2]
def test_successor(self):
assert nx.dfs_successors(self.G, source=0) == {0: [1], 1: [2], 2: [4], 4: [3]}
assert nx.dfs_successors(self.D) == {0: [1], 2: [3]}
def test_predecessor(self):
assert nx.dfs_predecessors(self.G, source=0) == {1: 0, 2: 1, 3: 4, 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), (2, 4), (4, 3)]
# 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), (4, 3)]
edges = nx.dfs_edges(self.D)
assert list(edges) == [(0, 1), (2, 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), (4, 3)]
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)]
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(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)]
def test_dls_labeled_disconnected_edges(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)]

View file

@ -0,0 +1,151 @@
import pytest
import networkx as nx
edge_bfs = nx.edge_bfs
FORWARD = nx.algorithms.edgedfs.FORWARD
REVERSE = nx.algorithms.edgedfs.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(edge_bfs(G))
assert edges == []
def test_graph_single_source(self):
G = nx.Graph(self.edges)
G.add_edge(4, 5)
x = list(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(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(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 = 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(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(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(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(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(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(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(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(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(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(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(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,134 @@
import pytest
import networkx as nx
edge_dfs = nx.algorithms.edge_dfs
FORWARD = nx.algorithms.edgedfs.FORWARD
REVERSE = nx.algorithms.edgedfs.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_