Repository navigation
fix(circle_layout): all five #1968 entry guards (finishes #2008) - #2118
Merged
Merged
Conversation
…ons, and duplicate bounding_box keys (#1968) Signed-off-by: Sai Asish Y <[email protected]>
…_box columns; one angle expression (#1968) Completes #1968 on top of #2008 by @SAY-5, which fixed defects 1-3 and declared 4-5 non-goals. Defect 4: a null value in a partition_by column raised 'Unexpected NaNs in node indices', which points at node ids; groupby().cumcount() returns NaN for a null group key, so the indices were only the messenger. The partition columns are checked where partition_by is validated. Defect 5: `engine_concrete in [EngineAbstract.CUDF, 'cudf', 'Engine.CUDF']` is always False -- Engine is a different enum and not a str-enum -- so only `hasattr(node_idx_relative, 'to_pandas')` selected the cuDF arm. The arms differed in one thing, the CPU arm's `.fillna(0.0)`, so a NaN angle became angle 0 on CPU and raised on GPU. One expression serves both, and a NaN angle is unreachable (node indices and partition sizes are each already checked, a partition is never empty), leaving the existing 'Unexpected NaNs in node angles' check as the single backstop. Also from reviewing #2008: the duplicate-key listing went through s_to_arr, whose cuDF arm is to_cupy() and raises on a string key column, so it used series_to_pylist; and the duplicate check sat behind `if partition_by in bounding_box_keyed.columns`, which made it skippable and left a missing key column to KeyError inside the merge -- the required columns are now named in a typed error. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…n the GFQL wire surface Review wave 1 on this branch. The duplicate-partition_key guard formatted its keys with sorted(set(...)), which raises TypeError: '<' not supported between instances of 'str' and 'NoneType' when the bounding_box key column holds a null beside another duplicated value -- a guard whose purpose is replacing an untyped crash was raising one. Sorting by str gives any key dtype a total order. #1968 motivates the typed errors by wire reachability -- circle_layout is a safelisted GFQL call -- and only its happy path was covered there, so two execute_call pins carry the error path and the edge-only path. The missing-positions pin also matched the bare substring 'bounding_box', which the unrelated missing-columns message satisfies too; it now matches the precondition phrase. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…; document the preconditions
Review waves 2-3 on this branch.
A partition-keyed bounding_box frame passed without partition_by reaches
the 4-tuple branch, where len(frame) is its row count and
`center_x, center_y, width, height = frame` unpacks column NAMES: a
5-column frame raises 'too many values to unpack (expected 4)', a 2-row
frame dumps the whole frame through an AssertionError, and a 4x4 frame
would put the string 'partition_key' in center_x and fail later in
arithmetic. It now raises a ValueError naming partition_by=. Both
in-repo callers are unaffected: fa2_layout passes the tuple when
partition_key is None and the frame only alongside
circle_layout_params={'partition_by': ...}, and gib goes through fa2.
The bounding_box and partition_by docstrings state the preconditions the
guards enforce, and the cuDF parity pin is parametrized per case so a
failure names it. cuDF gets the mixed-sortability message pin too: the
null-bearing key list is identical on both engines.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…das 2 and 3 agree on
pandas 3.0.5 stores an object-column None as nan, so the duplicate-key
message reads ['a', nan] there and [None, 'a'] on pandas 2.3.3 -- the
guard is right on both, the pin assumed one. The pandas pin now uses
mixed int/str keys, which is the property under test (a set with no total
order; sorted({1, 'a'}) raises the same TypeError) in a spelling
identical on both majors. The null spelling stays pinned on the cuDF
lane, where series_to_pylist goes through arrow and yields None, and
where a mixed-type column cannot exist.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This was referenced Oct 3, 2026
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.
Summary
Closes #1968Finishes @SAY-5's #2008, which fixed defects 1–3 of #1968 and declared 4–5 non-goals while still claiming to close the issue. Their commit is cherry-picked here with authorship intact; the rest is review of it plus the two remaining defects, so all five are now covered and
Closes #1968is honest.From #2008 (unchanged): nodes are materialized before they are counted, so an edge-only graph lays out instead of raising
TypeError: object of type 'NoneType' has no len(); the no-argument form on a graph withoutx/yraises aValueErrornaming the documented precondition instead ofKeyError: 'x'; abounding_boxframe with duplicatepartition_keyvalues raises aValueErrornaming the duplicates instead of pandas'IntCastingNaNErrorfrom the row-multiplying merge.Defect 4 — the misleading message. A null value in a
partition_bycolumn raisedValueError('Unexpected NaNs in node indices'), which sends the reader to node ids;groupby(...).cumcount()returns NaN for a null group key, so the indices were only the messenger. The partition columns are checked wherepartition_byis already validated, and the message names the column.Defect 5 — the dead comparison and the CPU/GPU divergence.
engine_concrete in [EngineAbstract.CUDF, 'cudf', 'Engine.CUDF']is alwaysFalse(engine_concreteis anEngine, a different enum, and not a str-enum), so onlyhasattr(node_idx_relative, 'to_pandas')ever selected the cuDF arm. The two arms differed in exactly one thing — the CPU arm's.fillna(0.0)— so a NaN angle became angle 0 on CPU and raised on GPU. They are one expression now (s_pi(PANDAS)isnp.piandcupy.piis the same float, so nothing numeric changes), and a NaN angle is unreachable: the node indices are checked at the cumcount guard, the per-partition sizes after the merge, and a partition is never empty — leaving the existingUnexpected NaNs in node anglescheck as the single backstop on both engines. The divergence was labelled INDICATIVE in the issue; it is confirmed latent, not reachable, which is why the A/B below is identical.Found by reviewing #2008 (
.agents/skills/review/SKILL.md, 3 waves to convergence):s_to_arr, whose cuDF arm isto_cupy(), which raises on a string key column. On GPU fix(circle_layout): entry guards for edge-only graphs, missing positions, and duplicate bounding_box keys #2008's guard raisedTypeError: cupy does not support object— worse than the error it replaced. Verified by running fix(circle_layout): entry guards for edge-only graphs, missing positions, and duplicate bounding_box keys #2008's owncircle.pyon a GB10. Nowseries_to_pylist(graphistry/Engine.py:1002).sorted(set(...)), which has no total order when the key column holds a null beside another duplicate:TypeError: '<' not supported between instances of 'str' and 'NoneType'. A guard that exists to replace an untyped crash must not raise one — sorted bystr.if partition_by in bounding_box_keyed.columns, which made it skippable and left a missing key column toKeyErrorinside the merge. The required columns are named in a typed error instead.partition_byreaches the 4-tuple branch, wherelen(frame)is its row count andcenter_x, center_y, width, height = frameunpacks column names —ValueError: too many values to unpack (expected 4)for a 5-column frame, a whole frame dumped through anAssertionErrorfor a 2-row one, andcenter_x = 'partition_key'for a 4×4 one. Typed error namingpartition_by=. Beyond layout: circle_layout entry guards — TypeError on edge-only graphs (len() before materialize_nodes), bare KeyError('x') for the no-arg form, IntCastingNaNError on duplicate bounding_box keys #1968's five; same branch, same defect class.circle_layoutis a safelisted GFQL call) and only the happy path was covered there — twoexecute_callpins now carry the error path and the edge-only path.Also: the
bounding_box/partition_bydocstrings state the preconditions the guards enforce, and the deadimport numpy as npand deadEngineimport go with the collapsed branch (bin/ci/ci_comment_density_baseline.jsonratchetscircle.py17 → 16; only that entry, not a blanket--update-baseline).Non-goals:
num_nodes == 0still returnsself, so an empty-edge graph gets_nodes is Noneback. Sibling layouts disagree with each other on that input (tree_layout→None,fa2_layout→ 0-row frame withx/y,layout_igraph/group_in_a_box/modularity_weighted→ mergeValueError), so there is no parity contract to meet, and #1968 does not raise it.Validation
Local lint/type/tests run for touched scope
CI is green — the only red is pre-existing and repo-wide:
docs/test_bench_numbers.py's three staleness assertions (filter-pagerank-20260728measured 66 days ago, the threegraphbench/snbkeys 67-68, againstpolicy.max_age_days = 60). They fail identically onorigin/masterlocally, on the RTD build, and on perf(gfql/cypher): multi-seedWHERE a.id IN [...]hop 4,004 ms → 10 ms (vectorized IN, index reaches the row pipeline, IN seeds the pattern, kernel takes a seed set) #2117 — an unrelated PR touching no docs — and they takedocs/readthedocs.org,test-docsand bothtest-minimal-pythonlanes with them ((3.8)is the matrix sibling cancelled by fail-fast). Fix is a pyg-bench re-measure and republish ofpublished/docs-numbers.json, not a change here. Bothtest-minimal-pythonlanes report 2139 passed, 3 failed — the 3 being exactly those.Cross-version: every pin here passes on pandas 2.x (py3.8 lane) and pandas 3.0.5 (py3.14 lane). The first CI run caught one that did not: pandas 3 stores an object-column
Noneasnan, so the duplicate-key message reads['a', nan]there and[None, 'a']on 2.3.3. The guard is correct on both — the pin assumed one — so the pandas pin now uses mixedint/strkeys ([1, 'a']), which is the property under test (sorted({1, 'a'})raises the sameTypeErrorthe fix removed) in a spelling both majors agree on. The null spelling stays pinned on the cuDF lane, whereseries_to_pylistgoes through arrow and yieldsNone, and where a mixed-type column cannot exist.ruff clean;
./bin/typecheck.sh→ mypy clean (352 files);ci_pyright_guard.py,ci_comment_density_guard.py,ci_type_hygiene_guard.pyall "no growth"449 tests green in
graphistry/tests/layout/+test_call_operations.py(19 of them the newtest_circle_entry_guards.py), on pandas and on a GB10 (cudf 26.02,TEST_CUDF=1, run through~/dgx-guard/safe_run.sh)./bin/test-minimal-lite.sh: 2690 passed, 52 failed — all 52 pre-existing and unrelated (49CuPy failed to load libnvrtc.so.12in this box's CPU env, 3docs/test_bench_numbers.pystaleness). No layout failure.Differential A/B, 16 graph shapes × 2 engines,
origin/master'scircle.pyvs this branch: 11/16 byte-identical on pandas and 11/16 on cuDF — every success path, positions included. The 5 pandas / 6 cuDF differences are exactly the defects:edge_only,one_nodeTypeError: object of type 'NoneType' has no len()no_bb_no_xyKeyError: 'x'requires either bounding_box= or existing x/y node positionsnull_partitionUnexpected NaNs in node indicespartition_by columns with nulls: ['p']dup_bb_keysIntCastingNaNError/CuPy currently does not support masked arraysbb_missing_partition_rowstaying identical is the empirical half of the dead-fillnaargument: a partition with no bounding-box row still fills 0/1 and still lands in the same place.Cypher Frontend CI Evidence (when PR touches cypher frontend / IR scope)
Not applicable — layout-only change.
🤖 Generated with Claude Code