Skip to content

fix: DGraph.nc no longer loops forever; one components algorithm - #52

Merged
petercorke merged 2 commits into
mainfrom
fix/dgraph-components-hang
Oct 12, 2026
Merged

petercorke merged 2 commits into
mainfrom
fix/dgraph-components-hang

Conversation

@petercorke

Copy link
Copy Markdown
Owner

Summary

DGraph.nc loops forever for many directed graphs, and so does everything that asks for the components: component(), samecomponent() and plot(). This fixes it.

The bug

DGraph._graphcolor merged component labels with

while vlabel in merge:
    vlabel = merge[vlabel]

and the merge map can contain a cycle, so it never ends. The smallest case is a plain directed 4-cycle:

g = DGraph()
v = [g.add_vertex() for _ in range(4)]
for a, b in [(0, 2), (2, 1), (1, 3), (3, 0)]:
    g.add_edge(v[a], v[b])
g.nc        # never returns

A random 100-vertex, 300-edge digraph hangs too, and so does an acyclic one. It is the same on 0.6.5. The existing directed tests use tiny graphs that happen not to trigger it. I found it by timing plot() on a larger directed graph.

The fix

One algorithm, on Graph: a single O(V+E) traversal that follows each edge in either direction, replacing the two separate ones in UGraph and DGraph.

  • Weakly connected components for a directed graph (direction ignored). That is what the existing tests and downstream code assume, and what the undirected algorithm already did; it is now stated in the docstrings.
  • Components are numbered from 0 by the first vertex of each, in the order the vertices were added, for both kinds. (A directed graph's numbering was arbitrary.)

Also fixed in the same code

  • Recomputed on every call. The graph's own dirty flag was never cleared, so once a graph had changed, every nc, component() and samecomponent() re-colored the whole graph. plot() asked for each component in turn. Now a call after no change is free.
  • Vertex.label was None until something else had asked for the components (the tests had to prime it with g.nc). Reading it now finds them.
  • A removed vertex no longer keeps its old label.
  • The samecomponent() docstring was backwards: it said that for a directed graph "same component" implies a path and for an undirected graph it does not. It now says what a weak component is, with a -> c <- b as the example.

Tests

tests/test_components.py (36, both kinds), with a timeout guard so a hang fails rather than freezes the run, and expectations from hand-written cases or a union-find in the test:

  • the 4-cycle, every call that asks for components, 30 random digraphs, and a 2,000-vertex digraph
  • isolated vertices, paths, loops, parallel edges, labels in order, component() order, 0 to nc-1
  • add/remove an edge or vertex changes the components, and nothing is recomputed when unchanged
  • direction ignored (a -> c <- b), opposite edges, and DGraph agrees with UGraph on the same edges

On main, the 4-cycle tests hang and the label tests fail.

🤖 Generated with Claude Code

… one algorithm

For many directed graphs, the smallest being the 4-cycle 0->2->1->3->0,
DGraph._graphcolor never returned: it merged labels with
'while vlabel in merge: vlabel = merge[vlabel]', and the merge map could contain
a cycle.  nc, component(), samecomponent() and plot() all ask for the
components, so all of them hung.  It was the same in 0.6.5, and a random
100-vertex digraph hangs.

The graph now has one algorithm, a single O(V+E) traversal that follows each
edge in either direction, so the components of a directed graph are weakly
connected, which is what its tests and downstream code assume and what the
undirected algorithm already did.  Components are numbered from 0 by the first
vertex of each, in the order the vertices were added, for both kinds.

Also:
- the components were found again by every nc, component() or samecomponent()
  once the graph had ever changed, because the graph's own flag was never
  cleared; it is now, so a call after no change is free (plot() asked for each
  component in turn)
- Vertex.label finds the components when it is read, instead of being None
  until something else had asked; the tests had to prime it with g.nc
- a removed vertex has no label
- samecomponent() documented that in a directed graph 'same component' implies
  a path, and in an undirected graph does not, which is backwards: it now
  says what a weak component is, with the a -> c <- b example

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 59 complexity · 0 duplication

Metric Results
Complexity 59
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.88%. Comparing base (6bff60e) to head (aaf3e2e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #52      +/-   ##
==========================================
+ Coverage   95.37%   95.88%   +0.50%     
==========================================
  Files           3        3              
  Lines        1277     1238      -39     
==========================================
- Hits         1218     1187      -31     
+ Misses         59       51       -8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@petercorke
petercorke merged commit 5acb105 into main Oct 12, 2026
12 checks passed
@petercorke
petercorke deleted the fix/dgraph-components-hang branch October 12, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant