Skip to content

Reject non-monotonic 1D coords in proximity/allocation/direction#2875

Merged
brendancol merged 3 commits into
mainfrom
issue-2851
Jun 3, 2026
Merged

Reject non-monotonic 1D coords in proximity/allocation/direction#2875
brendancol merged 3 commits into
mainfrom
issue-2851

Conversation

@brendancol
Copy link
Copy Markdown
Contributor

Closes #2851

What

proximity, allocation, and direction assume monotonic 1D x and y coordinates but never enforced it. Several internal steps rely on that assumption:

  • max_possible_distance is taken from the coordinate endpoints only.
  • The dask map_overlap halo and the NumPy line-sweep treat array adjacency as spatial adjacency.
  • The tiled KDTree convergence check lower-bounds the out-of-region distance with chunk-boundary coordinate gaps (_min_boundary_distance).

None of those hold for a non-monotonic axis, so such input was accepted silently and produced wrong proximity/allocation/direction versus the brute-force NumPy result.

Non-monotonic coords are outside the module's contract, so this rejects them up front rather than trying to make the tiled path provably correct on data the rest of the module already cannot handle.

Change

  • Validate both 1D coordinate axes once in the shared _process entrypoint and raise a ValueError naming the non-monotonic axis. A single-element axis is still allowed (no order to violate).
  • Document the monotonic-coordinate requirement in the three public docstrings and on the proximity reference page.

Backends

The check lives in _process, the single chokepoint for all three functions, so it applies to numpy, cupy, dask+numpy, and dask+cupy alike.

Test plan

  • Non-monotonic x and y each raise ValueError for all three functions across all four backends (GPU/dask+cupy skip when unavailable).
  • Descending (monotonic) axis still produces the reference proximity.
  • Single-element axis is accepted.
  • Full test_proximity.py suite passes (309 tests).

The endpoint-based max distance, the dask map_overlap halo, the NumPy
line-sweep, and the tiled KDTree convergence check all assume monotonic
1D coordinates. Non-monotonic coords were accepted silently and returned
wrong results. Validate both axes once in the shared _process entrypoint
and raise a clear ValueError naming the offending axis. A single-element
axis is still allowed. Document the requirement in the docstrings and
reference page, and add cross-backend tests for the raised error, the
descending-axis accept path, and the single-element accept path.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label Jun 2, 2026
Copy link
Copy Markdown
Contributor Author

@brendancol brendancol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Reject non-monotonic 1D coords in proximity/allocation/direction

Blockers (must fix before merge)

None.

Suggestions (should fix, not blocking)

  • proximity.py:271-277. The check also rejects equal adjacent coordinates (a duplicate axis value makes both ascending and descending false) and coords that contain NaN. Both are reasonable to reject, but the error always says "non-monotonic", which is a bit off for the duplicate/NaN case. Not worth a code change unless you want a more specific message. Flagging it so it is a conscious call.

Nits (optional improvements)

  • test_proximity.py:1700. test_single_element_axis_allowed covers numpy and dask+numpy but not cupy. The single-element path runs before dispatch so it is backend-agnostic and this is fine, but a cupy parametrization would match the other two new tests.

What looks good

  • The validation lives in _process, the single chokepoint for all three public functions, so one check covers all four backends. Right place for it: it runs after dask coords are computed to numpy and before any backend-specific path.
  • Length-1 axes are explicitly allowed, matching the existing _halo_depth single-coordinate handling.
  • Tests cover both axes, all three functions, all four backends (GPU skipped when unavailable), plus the descending-accept and single-element-accept paths.
  • Docstrings and the reference page document the new contract.

Checklist

  • Reject is the defensible stance (the whole module assumes monotonic coords)
  • All backends consistent (check runs pre-dispatch)
  • NaN handling correct (NaN coords rejected, NaN raster data unaffected)
  • Edge cases covered (single-element axis, descending axis)
  • No premature materialization (coords already computed upstream)
  • Benchmark not needed; README matrix not applicable; docstrings accurate

#2851)

- Spell out that duplicate and NaN coordinate values also fail the strict
  monotonic check, so the message is accurate for those cases.
- Parametrize test_single_element_axis_allowed over cupy and dask+cupy to
  match the other new tests.
Copy link
Copy Markdown
Contributor Author

@brendancol brendancol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review (after 5c0cd23)

Both findings from the first pass are addressed:

  • Suggestion (error message): the message now spells out that duplicate and NaN coordinate values also fail the strict-monotonic check, so it reads accurately for those cases. proximity.py:278-283.
  • Nit (test coverage): test_single_element_axis_allowed is now parametrized over cupy and dask+cupy, matching the other two new tests. test_proximity.py:1700.

No new findings. No blockers. Ready to merge once CI is green.

@brendancol
Copy link
Copy Markdown
Contributor Author

CI note: the run (ubuntu-latest, 3.14) fast-lane job fails on test_polygonize.py::TestSimplifyHelpers::test_visvalingam_whyatt_scales_subquadratic, a wall-clock timing assertion (ratio < 3.0) unrelated to this PR. It failed three runs in a row with ratios 3.47x, 3.43x, 3.46x, all just over the 3.0x threshold. The test lives on main and this branch touches only xrspatial/proximity.py, its tests, and the proximity reference page, so the failure is a pre-existing timing flake from slow runners, not a regression here. Every other check passes: run (3.12), label, and the sibling run (ubuntu-latest, 3.14) workflow. The full test_proximity.py suite passes locally (311 tests).

# Conflicts:
#	xrspatial/tests/test_proximity.py
@brendancol brendancol merged commit 549ae9e into main Jun 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

proximity/allocation/direction silently accept non-monotonic 1D coords and return wrong results

1 participant