Repository navigation
fix(gfql): hop-family defects — cuDF NA-mask node drop (#1798), min_hops seed residue (#1918 F2), internal column leak (#1940) - #1945
Merged
Conversation
…ops seed residue (#1918 F2), internal column leak (#1940) Four surgical changes in hop.py, cross-engine (pandas + cuDF) value-verified: * #1798: the output-window node mask combined NULL hop labels with cuDF's non-Kleene boolean ops (NULL | True = NULL) and cuDF drops NULL-masked rows, silently deleting NA-labeled seed/self-loop node rows that pandas kept. fillna(False) each window comparison BEFORE combining. Seeded undirected [*1..1] over self-loops: cuDF 1 -> 5 (= pandas = hand oracle). * #1918 F2: min_hops>=2 excluded the unlabeled hop-0 seed from the node output, then the endpoint backfill resurrected it id-only (attrs NaN, int64->float64 upcast) on pandas while cuDF omitted it — divergent. Contract per docstring: hop 0 is labeled only under label_seeds, so the seed row is excluded entirely. The backfill no longer resurrects min-hop-pruned seeds (scoped: min_hop_prune_applied and not label_seeds). * Same family: the min-hop prune's node-label rebuild carried hop values under edge_hop_col while the groupby read node_hop_col (names coincide only when both labels are internal); min_hops=2 + label_node_hops returned NULL labels. Renamed into node_hop_col. * #1940: internal __gfqlhop__hop_0__ tracking columns leaked into user node/edge frames on min_hops>=2, label_edge_hops-only, and output-window arms (both engines). Final sweep drops internally-generated label columns before return; requested labels untouched. Pins: 55 red-at-master cells (38 leak-matrix, 10 #1798-family incl. 5 lifted strict xfails, 7 F2-family), all green at head; mutation checks re-redden each group (8/6/38/1 cells) when its fix is individually reverted. Intended legacy flips: min_hops>=2 memberships drop the seed row (test_compute_hops, test_hop_semantics_1918 F8, endpoint-closure matrix). Fixes #1798. Fixes #1940. Closes out #1918 (F2 was its last residual). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm
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 #1798
Fixes #1940
Pre-release fixes for the three outstanding hop-family defects, verified live at
e6625ed28with hand-computed oracles (cuDF 25.10 lanes really executed). Four surgical changes ingraphistry/compute/hop.py; the rest is pins.#1798 — cuDF drops ALL self-loop matches on seeded undirected
[*1..1]Before:
MATCH (a {kind:'a'})-[*1..1]-(b) RETURN count(*)on the self-loop fixture (nodes 0..3 kind[b,a,a,b]; edges(2,2)x3, (0,3)x2, (3,1), (1,1)): hand-enumerated oracle 5 (self-loops bind once per edge row, Neo4j relationship semantics; the issue text's "9" was the stale double-counting figure); pandas 5, cuDF 1.Root cause: the degenerate varlen window engages hop tracking; undirected seeds carry a NULL hop label, and the output-window node mask combined
(hop <= max)with the endpoint rescue using cuDF's non-Kleene boolean ops:NULL | True = NULL, and cuDF's boolean indexing silently drops NULL-masked rows. Both self-loop seed node rows vanished from the node output, so the Cypher join found 1 match. pandas survived only via KleeneNA | True = True.Fix:
fillna(False)on each window comparison BEFORE combining, so no NULL ever enters the and/or chain and both engines agree.After: pandas 5, cuDF 5; all 9 tracked/windowed undirected probe arms value-identical across engines. Four strict-xfail pins that recorded exactly this divergence XPASSed and had their markers lifted (
test_hop_kernel_contractsseed-label pair,test_endpoint_closure_matrixmin-hops seed cells,test_known_cross_engine_divergences::test_output_hop_window_backfills_the_source_node_row_on_cudf,test_varlen_bounded_engine_parity_1787::test_seeded_undirected_degenerate_window_agrees_with_the_oracle[cudf]) — all now run green on cuDF.#1918 F2 residual — min_hops>=2 seed row: NaN residue on pandas, divergent on cuDF
Before:
hop(nodes=[0], min_hops=2, hops=2)(unlabeled) on the attributed path0->1->2: pandas emitted the seed row id-only (attrs NaN,int64 -> float64upcast) via the endpoint backfill; cuDF omitted the seed row (by accident, through the same NULL-mask bug above).Contract decision (from the
hop()docstring): seeds are hop 0, and hop 0 is labeled only underlabel_seeds, so an UNLABELEDmin_hops>=2hop excludes the seed row entirely. Scope: labeled hops keep the NULL-labeled seed stub — the chain wavefront contract (mirrored verbatim by the polars lane's_min_hops_labeled_node_output) depends on that stub, and this PR deliberately leaves the chain contract untouched, so the 400-case polars chain min_hops parity stays green.label_seeds=Truekeeps the seed labeled 0; a seed re-reached athop >= min_hopskeeps its real row (cycle pin unchanged).Fix: the endpoint backfill no longer resurrects min-hop-pruned seed rows on the unlabeled arm (gated
min_hop_prune_applied and label_node_hops is None and not label_seeds).After: pandas == cuDF == nodes
[1,2], attrs intact, no upcast; on the labeled arm both engines now agree too (seed stub NULL-labeled — previously cuDF dropped it AND the goal node).Bonus fix in the same family: the min-hop prune's goal-label rebuild carried hop values under
edge_hop_colwhile the groupby readnode_hop_col(the names only coincide when both labels are internal), somin_hops=2 + label_node_hopsreturned NULL labels for every goal node on pandas and cuDF. Now renamed; goals the backward walk retains carry their real hop, and net node membership is unchanged in every arm (verified against the polars parity fuzz, 400 seeds x 2 suites).#1918 status: F2 was the last open residual and this closes it — #1918 can be CLOSED. F1/F3–F8 were fixed in the earlier rounds and their pins stay green here; the deliberate residue pin
test_f2_min_hops_forward_seed_is_still_attribute_less_residueis replaced by pins of the fixed contract.Known residual kept out of scope (chain contract, cross-lane): the min-hop prune's backward walk still drops qualifying branches that end below
max_hops(e.g. seedc,min=2,max=3, branchc->d->eends at hop 2 and vanishes). Fixing it flips ~29 polars chain-parity cells because the polars mirror reproduces the same behavior, so it needs a paired change in the polars lane; follow-up issue #1944 filed with the verified fix shape (retain every edge traversed at a level>= min_hopsoutright).#1940 — internal
__gfqlhop__hop_0__column leaks into user outputBefore (both pandas and cuDF):
min_hops>=2,label_edge_hops-only, andoutput_min/max_hopsarms leaked the internal tracking column into user node and/or edge frames — the mid-function drops ran before backfill blocks that re-add the column.Fix: a final sweep just before return drops internally-generated label columns (only when the corresponding
label_*_hopswas not requested; requested labels, including collision-suffixed ones, are untouched).After: all 20 engine x arm sweep cells clean.
Pins + anti-vacuity
test_hop_kernel_contracts.py: cuDF silently diverges from the pandas oracle on seeded undirected degenerate var-length(a {p:v})-[*1..1]-(b)#1798 hop-level pins (tracked undirected self-loop arms keep seed rows; edges + membership hand-oracled) and end-to-end Cyphercount(*) == 5; GFQL hop: internal __gfqlhop__hop_0__ column leaks into user node/edge output (pandas + cuDF, several arms) #1940 matrix pin — no__gfqlhop_-prefixed column ever reaches user node/edge output across 2 engines x 3 directions x 10 arms (windows x labels x label_seeds x wavefront x tfp), requested labels asserted to remain.test_hop_semantics_1918.py: F2 fixed-contract pins (pandas + cuDF membership/dtype agreement on the unlabeled arm; NULL-labeled stub + correct goal labels on the labeled arm).test_endpoint_closure_matrix.py: min_hops cells rewritten to the new contract (unlabeled seed row excluded; every non-seed endpoint backed by its full-attribute row; no stub rows), cuDF xfails lifted.test_compute_hops.py:test_hop_min_max_range,test_hop_exact_three_branchdrop the seed from unlabeled memberships;test_hop_output_slicekeeps it — labeled arm).e6625ed28(38 leak-matrix, 10 cuDF silently diverges from the pandas oracle on seeded undirected degenerate var-length(a {p:v})-[*1..1]-(b)#1798-family incl. 5 lifted strict xfails, 7 F2-family), all green at head.Gates
graphistry/tests/compute: failure set vs master baseline is identical (105 pre-existing cells, none hop-related) — zero unintended flips; the hop battery (boundary matrix 581 cells, semantics pins, kernel contracts, 1918 pins, endpoint-closure matrix, varlen parity, legacy hops) is 100% green.bin/test-polars.sh.🤖 Generated with Claude Code
https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm