fix(layout): group_in_a_box_layout returned duplicate node ids at conflicting coordinates (#1961) - #1970
Merged
Merged
Conversation
…licting coordinates (#1961) partitioned_layout defaults to bulk_mode=True, where layout_bulk_mode positions the WHOLE graph in one pass. The singleton / pair / edgeless fallbacks were then appended on top of that already-positioned frame instead of replacing those rows, so every node in a size-1, size-2 or degree-0 partition came back twice, each copy at a different x/y. Those fallbacks exist for bulk_mode=False, where layout_non_bulk_mode only positions partitions with id_count > 2 and degree_max > 0; they are now scoped to it. Two adjacent defects in the same function go with it: the edgeless branch assigned x twice and never assigned y, and the NaN backstop asserted the negation of its own guard, so an unpositioned node raised a message-less AssertionError instead of reaching the fillna below it. The backstop is now a single engine-agnostic loop, which also drops the 2-D cupy array that cudf.Series rejects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm
lmeyerov
force-pushed
the
fix/1961-gib-duplicate-nodes
branch
from
August 19, 2026 06:33
189f561 to
05a0795
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1961
Root cause
partitioned_layout()defaults tobulk_mode=True, and no caller ever passesFalse. In bulk modelayout_bulk_mode()positions the whole graph in one pass and its frame is appended at:105. The singleton (id_count == 1), pair (id_count == 2) and edgeless (id_count > 2 and degree_max == 0) fallbacks were then appended again at:122/:134/:154— on top of rows the bulk pass had already positioned, rather than replacing them. Each such node came back twice at two differentx/y.Those fallbacks are correct and necessary for
bulk_mode=False, wherelayout_non_bulk_mode()only lays outremaining(id_count > 2 and degree_max > 0). They are now scoped to that mode — theif True and ...prefixes were the tell that the guard was never finished.Why skip rather than drop the bulk rows. Both restore the invariant; the question is which positions survive. Measured on the issue's repro:
0either way)10.95/15.92, spread across the whole box10.95/12.76, clustered at0and0.33of box widthuniform(0, 1)noiseBulk positions are identical for singletons and strictly better for pairs and edgeless groups, because
layout_bulk_modealready handscircle_layout_params={'partition_by': partition_key}tofa2_layout— that is the degenerate-partition handling the constants were a stand-in for. So no positioning quality is traded away;bulk_mode=Falsekeeps the constants unchanged.Adjacent defects fixed in the same function
edgeless['y']was never assigned (:146re-assignedx). Reachable viabulk_mode=False.assert combined_nodes.y.isna().sum() == 0insideif combined_nodes.y.isna().any():), so thefillnatwo lines below was dead and any unpositioned node raised a message-lessAssertionError. The backstop is now one engine-agnostic loop over['x', 'y']built ondf_cons(engine), which also removes the cuDF half'scp.random.rand(n, 1, ...)— verified locally thatcudf.Seriesrejects it withValueError: Data must be 1-dimensional, i.e. the cuDF backstop would have crashed had it ever been reachable.Invariant pinned
The output node frame carries the same id multiset as the input — no duplicates, no drops — for every partition-size mix, on pandas and cuDF.
Verification
Red at merge-base: the 7 new pins were run against merge-base
09633aaproduct code (also verified at0aaa862before rebase) in a throwaway worktree — 7/7 red (9 failures total; 2 are the pre-existingtest_gib_cudf/test_gib_cudf_with_partitions, which needcugraph, absent here). Green with the fix.Mutation check, reverting each site individually (counts exclude the 2 pre-existing cugraph failures):
if Trueif Trueif Trueedgeless['y']→edgeless['x']assert na_count == 0['x', 'y']→['x']Failure-set comparison, full
graphistry/tests(14774 tests) at head vs merge-base, both directions: identical 120-failure set, zero new, zero disappeared (the 120 are pre-existing optional-dependency failures). Head is+5 passed(new CPU pins) and+2 skipped(new cuDF pins, skipped withoutTEST_CUDF=1).GPU exercised for real, not indicative:
cudf 25.10+cupy 13.6on an RTX 3080 Ti, withLD_LIBRARY_PATHpointed at thenvidia/*/libwheels (the worktree default is missinglibnvrtc.so.12/libcurand.so.10).TEST_CUDF=1runs both new cuDF pins green on the fix and red at merge-base.cugraphis not installed, so the cuGraph-backed default layout path is untested — the cuDF pins use a layout callable, which still drives the cuDFdf_concat/isna/fillna/groupby/ merge path end to end.bin/ci_comment_density_guard.py,./bin/lint.sh,./bin/typecheck.shall rc=0, run from inside the worktree.Changed-line coverage: 10/10 = 100% (the engine-agnostic NA loop is what makes the cuDF-side lines reachable from CPU lanes; the previous engine-branched form scored 4/6 = 67% and would have failed the 80% gate).
Deliberately not changed
seed=parameter) — the issue itself files it as a separate documentation/API question. Noted in the CHANGELOG as still-unseeded.typecolumn staysNaNfor pairs inbulk_mode=False— pre-existing, unrelated to the duplication.dx = max(x_max - x_min, 1)makes a degenerate range normalize to0). Pre-existing and identical in both modes before and after this change; thex=0.5, y=0.5singleton constant was already defeated by it.🤖 Generated with Claude Code
https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm
Rebased onto
09633aaafter #1969 landed; the only conflict was the adjacent## [Development]→### FixedCHANGELOG line, resolved by keeping both entries.