-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better cycle detection #167
base: main
Are you sure you want to change the base?
Changes from all commits
a519347
2a6d9ae
b9ac7b8
e9d1f57
27255a5
29775e6
2ff7def
18d241b
a95c4c0
b5d3af6
96bd3a6
080ff03
e0ba463
3640c88
85a4a29
dca95b9
67f948e
7e79f5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,7 @@ | |
.. autoexception:: CycleError | ||
.. autofunction:: compute_topological_order | ||
.. autofunction:: compute_transitive_closure | ||
.. autofunction:: find_cycles | ||
.. autofunction:: contains_cycle | ||
.. autofunction:: compute_induced_subgraph | ||
.. autofunction:: as_graphviz_dot | ||
|
@@ -68,6 +69,8 @@ | |
Mapping, MutableSet, Optional, Set, Tuple, TypeVar) | ||
|
||
|
||
from enum import Enum | ||
|
||
try: | ||
from typing import TypeAlias | ||
except ImportError: | ||
|
@@ -242,6 +245,52 @@ def __init__(self, node: NodeT) -> None: | |
self.node = node | ||
|
||
|
||
class _NodeState(Enum): | ||
WHITE = 0 # Not visited yet | ||
GREY = 1 # Currently visiting | ||
BLACK = 2 # Done visiting | ||
|
||
|
||
def find_cycles(graph: GraphT, all_cycles: bool = True) -> List[List[NodeT]]: | ||
""" | ||
Find cycles in *graph* using DFS. | ||
|
||
:arg all_cycles: If False, only return the first cycle found. | ||
|
||
:returns: A :class:`list` in which each element represents another :class:`list` | ||
of nodes that form a cycle. | ||
""" | ||
def dfs(node: NodeT, path: List[NodeT]) -> List[NodeT]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constructing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
# Cycle detected | ||
if visited[node] == _NodeState.GREY: | ||
return path + [node] | ||
|
||
# Visit this node, explore its children | ||
visited[node] = _NodeState.GREY | ||
for child in graph[node]: | ||
if visited[child] != _NodeState.BLACK and dfs(child, path): | ||
return path + [node] + ( | ||
[child] if child != node else []) | ||
|
||
# Done visiting node | ||
visited[node] = _NodeState.BLACK | ||
return [] | ||
|
||
visited = {node: _NodeState.WHITE for node in graph.keys()} | ||
|
||
res = [] | ||
|
||
for node in graph: | ||
if visited[node] == _NodeState.WHITE: | ||
cycle = dfs(node, []) | ||
if cycle: | ||
res.append(cycle) | ||
if not all_cycles: | ||
return res | ||
|
||
return res | ||
|
||
|
||
class HeapEntry: | ||
""" | ||
Helper class to compare associated keys while comparing the elements in | ||
|
@@ -259,14 +308,17 @@ def __lt__(self, other: "HeapEntry") -> bool: | |
|
||
|
||
def compute_topological_order(graph: GraphT[NodeT], | ||
key: Optional[Callable[[NodeT], Any]] = None) \ | ||
-> List[NodeT]: | ||
key: Optional[Callable[[NodeT], Any]] = None, | ||
verbose_cycle: bool = True) -> List[NodeT]: | ||
"""Compute a topological order of nodes in a directed graph. | ||
|
||
:arg key: A custom key function may be supplied to determine the order in | ||
break-even cases. Expects a function of one argument that is used to | ||
extract a comparison key from each node of the *graph*. | ||
|
||
:arg verbose_cycle: Verbose reporting in case *graph* contains a cycle, i.e. | ||
return a :class:`CycleError` which has a node that is part of a cycle. | ||
|
||
:returns: A :class:`list` representing a valid topological ordering of the | ||
nodes in the directed graph. | ||
|
||
|
@@ -318,9 +370,17 @@ def compute_topological_order(graph: GraphT[NodeT], | |
heappush(heap, HeapEntry(child, keyfunc(child))) | ||
|
||
if len(order) != total_num_nodes: | ||
# any node which has a predecessor left is a part of a cycle | ||
raise CycleError(next(iter(n for n, num_preds in | ||
nodes_to_num_predecessors.items() if num_preds != 0))) | ||
# There is a cycle in the graph | ||
inducer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if not verbose_cycle: | ||
raise CycleError(None) | ||
|
||
try: | ||
cycles: List[List[NodeT]] = find_cycles(graph) | ||
except KeyError: | ||
# Graph is invalid | ||
raise CycleError(None) | ||
else: | ||
raise CycleError(cycles[0][0]) | ||
Comment on lines
+381
to
+383
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add to the documentation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, the current doc of |
||
|
||
return order | ||
|
||
|
@@ -373,11 +433,7 @@ def contains_cycle(graph: GraphT[NodeT]) -> bool: | |
.. versionadded:: 2020.2 | ||
""" | ||
|
||
try: | ||
compute_topological_order(graph) | ||
return False | ||
except CycleError: | ||
return True | ||
return bool(find_cycles(graph, all_cycles=False)) | ||
|
||
# }}} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use descriptive names for the node state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you prefer, I can rename these, but I thought white/grey/black were standard labels in DFS (see e.g. http://www.cs.cmu.edu/afs/cs/academic/class/15750-s17/ScribeNotes/lecture9.pdf)