beam: deterministic topo sort with cycle detection + tests
This commit is contained in:
1 parent
0728fbeb89
commit
ac15b7f717
2 files changed
+152
No files matched your search
@@ -0,0 +1,60 @@
|
||||
import sys
|
||||
sys.path.insert(0, "/root/.hermes/wizard-outputs/beam")
|
||||
from topo_sort import topo_sort
|
||||
|
||||
def check(order, edges):
|
||||
pos = {n:i for i,n in enumerate(order)}
|
||||
for u,v in edges:
|
||||
assert pos[u] < pos[v], (u,v,order)
|
||||
|
||||
# DAG basic
|
||||
order, cyc = topo_sort([1,2,3], [(1,2),(2,3)])
|
||||
assert cyc is None and order == [1,2,3], (order,cyc)
|
||||
check(order, [(1,2),(2,3)])
|
||||
|
||||
# disconnected
|
||||
order, cyc = topo_sort([1,2,3,4], [(1,2)])
|
||||
assert cyc is None and set(order)=={1,2,3,4}
|
||||
check(order, [(1,2)])
|
||||
|
||||
# deterministic tie-break
|
||||
order, cyc = topo_sort(["b","a","c"], [])
|
||||
assert order == ["a","b","c"], order
|
||||
|
||||
# duplicate edges deduped
|
||||
order, cyc = topo_sort([1,2], [(1,2),(1,2)])
|
||||
assert cyc is None and order==[1,2]
|
||||
|
||||
# cycle
|
||||
order, cyc = topo_sort([1,2,3], [(1,2),(2,3),(3,1)])
|
||||
assert cyc is not None
|
||||
# verify it's a cycle
|
||||
s = set(cyc)
|
||||
assert len(cyc) == len(s)+1 or len(cyc)==len(s), cyc # cycle list may include repeat? ours no repeat of first? we built v..u no repeat
|
||||
# check edges close
|
||||
edgeset = {(1,2),(2,3),(3,1)}
|
||||
# cycle consecutive
|
||||
for i in range(len(cyc)):
|
||||
a=cyc[i]; b=cyc[(i+1)%len(cyc)]
|
||||
assert (a,b) in edgeset, (a,b)
|
||||
print("cycle:", cyc)
|
||||
|
||||
# self loop
|
||||
order, cyc = topo_sort([1], [(1,1)])
|
||||
assert cyc == [1], cyc
|
||||
|
||||
# unknown node raises
|
||||
try:
|
||||
topo_sort([1], [(1,2)])
|
||||
raise SystemExit("expected ValueError")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# duplicate node raises
|
||||
try:
|
||||
topo_sort([1,1], [])
|
||||
raise SystemExit("expected ValueError")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
print("ALL TESTS PASSED")
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Deterministic topological sort with cycle detection.
|
||||
|
||||
Public API:
|
||||
topo_sort(nodes, edges) -> (order, cycle)
|
||||
|
||||
- nodes: iterable of hashable node labels.
|
||||
- edges: iterable of (u, v) meaning u must come before v.
|
||||
- Returns (order, None) for a DAG: `order` contains every node exactly once
|
||||
in a topologically valid order (ties broken by node ordering for determinism).
|
||||
- Returns (order, cycle) when a cycle exists: `order` contains the nodes that
|
||||
could be ordered, `cycle` is a list of node labels forming a directed cycle
|
||||
(last -> first edge closes the cycle).
|
||||
- Raises ValueError on unknown nodes in edges or duplicate node labels.
|
||||
"""
|
||||
|
||||
import heapq
|
||||
import sys
|
||||
|
||||
|
||||
def topo_sort(nodes, edges):
|
||||
node_list = list(nodes)
|
||||
if len(node_list) != len(set(node_list)):
|
||||
raise ValueError("duplicate node labels")
|
||||
adj = {n: [] for n in node_list}
|
||||
indeg = {n: 0 for n in node_list}
|
||||
seen = set()
|
||||
for u, v in edges:
|
||||
if u not in adj or v not in adj:
|
||||
raise ValueError("unknown node in edge: %r -> %r" % (u, v))
|
||||
if (u, v) in seen:
|
||||
continue
|
||||
seen.add((u, v))
|
||||
adj[u].append(v)
|
||||
indeg[v] += 1
|
||||
|
||||
# Kahn's algorithm with a min-heap for deterministic tie-breaking.
|
||||
q = [n for n in node_list if indeg[n] == 0]
|
||||
heapq.heapify(q)
|
||||
order = []
|
||||
work = dict(indeg)
|
||||
while q:
|
||||
u = heapq.heappop(q)
|
||||
order.append(u)
|
||||
for v in sorted(adj[u]):
|
||||
work[v] -= 1
|
||||
if work[v] == 0:
|
||||
heapq.heappush(q, v)
|
||||
|
||||
if len(order) == len(node_list):
|
||||
return order, None
|
||||
|
||||
remaining = set(n for n in node_list if n not in set(order))
|
||||
# subgraph induced by remaining nodes
|
||||
radj = {n: [] for n in remaining}
|
||||
for u in remaining:
|
||||
for v in adj[u]:
|
||||
if v in remaining:
|
||||
radj[u].append(v)
|
||||
|
||||
sys.setrecursionlimit(max(sys.getrecursionlimit(), len(remaining) + 100))
|
||||
color = {n: 0 for n in remaining} # 0=unvisited,1=visiting,2=done
|
||||
parent = {n: None for n in remaining}
|
||||
cycle = [None]
|
||||
|
||||
def dfs(u):
|
||||
color[u] = 1
|
||||
for v in radj[u]:
|
||||
if color[v] == 0:
|
||||
parent[v] = u
|
||||
if dfs(v):
|
||||
return True
|
||||
elif color[v] == 1:
|
||||
# back edge u -> v: cycle is v .. u -> v
|
||||
chain = []
|
||||
cur = u
|
||||
while cur != v and cur is not None:
|
||||
chain.append(cur)
|
||||
cur = parent[cur]
|
||||
chain.append(v)
|
||||
chain.reverse()
|
||||
cycle[0] = chain
|
||||
return True
|
||||
color[u] = 2
|
||||
return False
|
||||
|
||||
for s in sorted(remaining):
|
||||
if color[s] == 0:
|
||||
parent[s] = None
|
||||
if dfs(s):
|
||||
break
|
||||
|
||||
return order, cycle[0]
|
||||
Reference in new issue
Block a user