Skip to content

Fix gridRing silently returning wrong results when the ring wraps a pentagon (#1241) - #1245

Open
4mthxmas20 wants to merge 17 commits into
uber:masterfrom
4mthxmas20:fix-gridring-pentagon-wrapped-ring
Open

4mthxmas20 wants to merge 17 commits into
uber:masterfrom
4mthxmas20:fix-gridring-pentagon-wrapped-ring

Conversation

@4mthxmas20

@4mthxmas20 4mthxmas20 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #1241.

gridRingUnsafe could silently return incorrect results when the ring encircles pentagons without stepping on one: the traversal is distorted by the enclosed face structure but still closes back on the origin, so the existing lastIndex != origin guard does not detect the distortion and gridRing never falls back to _gridRingInternal.

This adds a closure check: a correct ring consists of exactly 6 * k distinct cells, so any duplicate in the output indicates pentagonal distortion — gridRingUnsafe now returns E_PENTAGON and gridRing falls back to the correct algorithm. The fast path is preserved for all other rings: a correct ring has no duplicates by construction, so the check has no false positives. Verified against gridDiskDistancesSafe as ground truth over all res 0–2 cells for k = 1..25 plus targeted pentagon-neighborhood corpora (158k+ cases, zero false positives/negatives).

The duplicate check is O((6k)^2) integer comparisons with no allocations. Measured overhead: k=10: 0.8→1.4µs; k=100: 8→62µs — still orders of magnitude cheaper than the BFS fallback.

Tests

  • testGridRing.c: new gridRing_pentagonWrappedRing regression test — for the res-1 cell containing (0, 0) and k = 1..18, asserts gridRing output equals gridDisk(k) - gridDisk(k-1). Fails before the fix, passes after.
  • testGridRingUnsafe.c: new pentagonWrappedRing test asserting gridRingUnsafe returns E_PENTAGON for k = 12..16 (covers the new failure branch).
  • Full suite: 318/318 tests pass (Release build, this branch's contents merged over latest master; 317/317 on the branch as cut). The new branches are covered, keeping core library coverage at 100%.

Note: the CHANGELOG entry is intentionally left out of the diff for now to avoid an add/add merge conflict in the Unreleased section — see my comment below for the exact entry text.

…entagon

When a ring encircles pentagons without stepping on one, gridRingUnsafe's
traversal is distorted by the enclosed face structure but still closes
back on the origin, so the lastIndex guard did not detect the distortion
and gridRing silently returned incorrect results instead of falling back
to _gridRingInternal.

gridRingUnsafe now checks that the ring contains no duplicate cells: a
correct ring consists of exactly 6 * k distinct cells, so any duplicate
indicates pentagonal distortion and the function returns E_PENTAGON so
that gridRing falls back to the correct algorithm.

Fixes uber#1241
…entagon

When a ring encircles pentagons without stepping on one, gridRingUnsafe's
traversal is distorted by the enclosed face structure but still closes
back on the origin, so the lastIndex guard did not detect the distortion
and gridRing silently returned incorrect results instead of falling back
to _gridRingInternal.

gridRingUnsafe now checks that the ring contains no duplicate cells: a
correct ring consists of exactly 6 * k distinct cells, so any duplicate
indicates pentagonal distortion and the function returns E_PENTAGON so
that gridRing falls back to the correct algorithm.

Fixes uber#1241
…entagon

When a ring encircles pentagons without stepping on one, gridRingUnsafe's
traversal is distorted by the enclosed face structure but still closes
back on the origin, so the lastIndex guard did not detect the distortion
and gridRing silently returned incorrect results instead of falling back
to _gridRingInternal.

gridRingUnsafe now checks that the ring contains no duplicate cells: a
correct ring consists of exactly 6 * k distinct cells, so any duplicate
indicates pentagonal distortion and the function returns E_PENTAGON so
that gridRing falls back to the correct algorithm.

Fixes uber#1241
…entagon

When a ring encircles pentagons without stepping on one, gridRingUnsafe's
traversal is distorted by the enclosed face structure but still closes
back on the origin, so the lastIndex guard did not detect the distortion
and gridRing silently returned incorrect results instead of falling back
to _gridRingInternal.

gridRingUnsafe now checks that the ring contains no duplicate cells: a
correct ring consists of exactly 6 * k distinct cells, so any duplicate
indicates pentagonal distortion and the function returns E_PENTAGON so
that gridRing falls back to the correct algorithm.

Fixes uber#1241
@4mthxmas20

Copy link
Copy Markdown
Contributor Author

Note on the changelog entry: per CONTRIBUTING.md, the intended entry for the Unreleased → Fixed section is:

- Fixed `gridRing` silently returning incorrect results when the ring wraps around a pentagon without intersecting it; `gridRingUnsafe` now detects this pentagonal distortion and returns an error so that `gridRing` falls back to the correct algorithm (#1241)

I temporarily reverted the changelog file on this branch because the Unreleased section has moved on master since this branch was cut, and any entry added from this branch produces an add/add merge conflict there (this PR branch was created from an older fork master, and I cannot merge upstream master into the branch via the API due to OAuth workflow-scope restrictions on the fork). Once a maintainer updates this branch with the latest master (the "Update branch" button), I will happily re-add the entry conflict-free — or feel free to add it directly; maintainer edits are enabled.

@coveralls

coveralls commented Sep 30, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 99.72% (+0.001%) from 99.719% — 4mthxmas20:fix-gridring-pentagon-wrapped-ring into uber:master

@isaacbrodsky

Copy link
Copy Markdown
Collaborator

From #1241:

check that the accumulated rotations is back to its starting value at closure, not just the index, since it is the face-crossing bookkeeping that the enclosed pentagons perturb

It seems like this would be faster. Did you evaluate a solution that would use the rotations, or check which base cells or faces are being traversed?

You seem to have incorrect merge commit(s) in your branch which are causing Github to show unrelated changes (e.g. relating to integer overflow) in your PR. I'd suggest squash rebasing your branch.

@4mthxmas20

Copy link
Copy Markdown
Contributor Author

@isaacbrodsky Yes, I evaluated the rotations-based check before settling on the duplicate check. Using gridDiskDistancesSafe as ground truth over all res 0–2 cells for k = 1..25 (52,084 cases where gridRingUnsafe reported success, 884 of which were actually incorrect):

  • rotations != 0 at closure: 17,039 false positives, 60 false negatives
  • duplicate-cell check: 0 FP / 0 FN, also 0/0 on a second 106k-case corpus targeting all pentagon neighborhoods at res 2–4 and very large rings (k ≤ 60) that enclose multiple pentagons

The false positives are inherent rather than incidental: a correct ring that wraps a pentagon legitimately ends with nonzero accumulated rotations (the holonomy is real even when the traversal is undistorted), so about a third of correct rings wrapping pentagons would be needlessly pushed onto the BFS fallback — the fast path would mostly stop being fast exactly where it matters. And the 60 false negatives are distorted rings where the perturbation happened to cancel back to 0.

Since a correct ring consists of 6 * k distinct cells by construction, the duplicate check cannot false-positive, and I found no distorted ring that produces a duplicate-free output (the distortion mechanism perturbs face-crossing bookkeeping so the walk self-intersects and revisits a cell). Cost is O((6k)^2) pure integer comparisons with no allocation: measured k=10: 0.8→1.4µs, k=100: 8→62µs — still orders of magnitude cheaper than the fallback. I didn't implement face/base-cell traversal tracking; it would be O(k) but adds bookkeeping state to the hot loop, and given the measured overhead above it didn't seem necessary. Happy to explore it if you'd prefer.

On the branch history: agreed, the stray diff comes from updating file contents over an outdated fork master via the API. I'll clean up the branch so the PR shows only the fix.

Restore algos.c to base + only the gridRingUnsafe duplicate-cell check,
so the PR diff shows just this fix (per review feedback).
Restore algos.c to base + only the gridRingUnsafe duplicate-cell check,
so the PR diff shows just this fix (per review feedback).
Restore algos.c to base + only the gridRingUnsafe duplicate-cell check,
so the PR diff shows just this fix (per review feedback).
Restore algos.c to base + only the gridRingUnsafe duplicate-cell check,
so the PR diff shows just this fix (per review feedback).
@4mthxmas20

Copy link
Copy Markdown
Contributor Author

@isaacbrodsky Branch cleaned up — the PR diff now shows only this fix (3 files, +98: the gridRingUnsafe duplicate-cell check plus the two regression tests). The unrelated hunks are gone. (Since the fork's master predates the current upstream master and I can't create a real merge/rebase onto it via the API, I restored the branch contents to base + fix instead; the branch merges cleanly against current master, and a squash merge will collapse the intermediate commits.)

@isaacbrodsky

Copy link
Copy Markdown
Collaborator

What about checking whether the ring traverses base cells on too many different faces? Could that be done reliably with cheaper checks?

@4mthxmas20

Copy link
Copy Markdown
Contributor Author

@isaacbrodsky Good question — I haven't measured a face-count variant yet, and it's cheap to test, so I'll prototype it: track the distinct faces (or base cells) traversed during the walk, flag when the count exceeds a threshold, and evaluate it against the same ground truth (all res 0–2 cells for k = 1..25, plus the pentagon-neighborhood corpora, 158k+ cases), reporting false positives/negatives and timings versus the duplicate check.

Worth noting it's in the same family as the constant-cost guards tried on #1241 (rotation holonomy, step-reversal, corner-cell collision, pentagon-within-k via gridDistance), each of which either missed these cases or degenerated to always falling back. The specific risk for a face count: a correct ring can legitimately cross several faces at low res or large k without enclosing a pentagon, so the threshold has to separate "encloses a pentagon" from "merely wide" — which is exactly where the rotations check produced its 17k false positives. If the data comes back 0 FP / 0 FN with lower overhead, I'm happy to switch to it; otherwise I'll post the numbers here and we can weigh a hybrid (cheap pre-check, duplicate scan only when it trips) against just keeping the duplicate check, whose measured overhead (0.8→1.4µs at k=10, 8→62µs at k=100) is already far below the BFS fallback it gates.

This branch has not been deployed

No deployments
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.

gridRing silently returns wrong results when the ring encircles pentagons without touching one

3 participants