reproject: unit-aware bounds_policy="auto" blow-up detection#2600
Merged
Conversation
The old span-ratio heuristic compared source span in source CRS units (degrees for EPSG:4326) against output span in target CRS units (metres for EPSG:3857). The unit mismatch made the > 50x threshold fire on ordinary geographic-to-projected reprojections, silently trimming 70-106 km per side on a (-10, -10, 10, 10) bbox. Replace it with a unit-agnostic check: ratio of max absolute projected coordinate to median absolute projected coordinate. Benign reprojections stay near 1-2; real singularities push the ratio past 10 (Mercator at the poles after clamp) or higher (polar-stereographic opposite pole projects to ~1e23). Also flag the non-finite fraction of raw, unclamped edge samples so the auto clamp branch does not mask true singularities. Adds four regression tests under TestBoundsPolicy covering the bug case (numpy and dask backends), warning silence on benign input, and that the polar-stereographic pathological case still trips.
brendancol
commented
May 28, 2026
brendancol
left a comment
Contributor
Author
There was a problem hiding this comment.
PR Review
Blockers
None.
Suggestions
-
xrspatial/reproject/_grid.py:271-294. The non-finite probe duplicates the edge-sampling pattern from lines 215-226, just with the unclamped source coordinates. A small helper would keep the two samplers in sync ifn_edgeis ever tuned. Optional given the surgical-changes rule, but flagging it.
Nits
-
xrspatial/reproject/_grid.py:299. Thenonfinite_frac > 0.05threshold is reasonable but unexplained. A one-line comment ("5% means edges hit a singularity; benign cases yield 0%") would help future maintainers understand the choice. -
xrspatial/reproject/_grid.py:295-298. The magnitude-threshold comment mentions "ratio ~1e16" for the opposite-pole case. Worth also noting that the post-clamp Mercator-global case has a ratio around 5 (caught only by the non-finite probe), so the reader sees why both signals are needed.
What looks good
- The bug is reproducible before the fix: 106 km x crop, 70 km y crop on (-10, -10, 10, 10) into EPSG:3857.
- Two-signal design cleanly separates the pathological cases. The magnitude ratio catches finite-but-huge singularities like polar-stereographic on the opposite pole. The non-finite probe catches Mercator at the poles after clamp would otherwise have hidden the issue.
- Tests cover the regression case, the silent-no-warning case, the polar-stereographic pathological case, and a dask-backed regression.
- CHANGELOG and docs both updated.
- All 13 existing TestBoundsPolicy tests pass; full reproject suite of 354 tests passes.
- The non-finite probe is gated on
bounds_policy == "auto" and source_crs.is_geographicso the extra pyproj call only fires when needed.
Checklist
- Algorithm matches the issue's suggested approach
- Backends consistent
- NaN handling correct
- Edge cases covered
- Dask test added
- No premature materialization
- Benchmark not required
- README not applicable
- Docstrings updated
…2582) - Extract _edge_samples() helper. The non-finite probe was duplicating the same 4-edge sampling pattern as the main edge+interior sampler; consolidating keeps the two callers in sync if n_edge is ever tuned. - Add a comment explaining why the auto_blowup signal combines two heuristics and why the magnitude case alone is insufficient (global 4326 -> 3857 has ratio ~5 after the clamp, well under the 10x threshold; the non-finite probe is what trips that case). - Add a comment explaining the 5% non-finite threshold: benign cases yield 0%, real singularities yield 15%+, the floor tolerates one or two stray inf points from numerical noise.
brendancol
commented
May 28, 2026
brendancol
left a comment
Contributor
Author
There was a problem hiding this comment.
Follow-up review
Addressed all three findings from the previous review.
Fixed
- Suggestion (
_grid.py:271-294): extracted_edge_samples()helper; both the main edge sampler and the non-finite probe now share the same edge construction. - Nit (
_grid.py:299): added a comment explaining why 5% is the non-finite threshold (benign cases yield 0%, real singularities yield 15%+, the floor absorbs numerical noise). - Nit (
_grid.py:295-298): expanded the magnitude-threshold comment so the reader sees why both signals are necessary. The post-clamp global Mercator case has ratio ~5, well under 10x; the non-finite probe is what catches that case.
All 17 TestBoundsPolicy tests still pass; full reproject suite of 358 tests passes.
# Conflicts: # CHANGELOG.md
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 #2582. The
bounds_policy="auto"heuristic compared source span (in source CRS units) to output span (in target CRS units), so the > 50x ratio fired on almost every geographic-to-projected reprojection and silently cropped valid edge data via the 2/98 percentile fallback. For a (-10, -10, 10, 10) EPSG:4326 bbox going to EPSG:3857, that was 70-106 km per side.Replaced the unit-mismatched ratio with two unit-agnostic signals:
Verification
The reproduction case from the issue:
Global EPSG:4326 -> EPSG:3857 still trips the percentile fallback. The pathological EPSG:4326 -> EPSG:3413 case (south pole into a north polar stereographic) still trips.
Backend coverage
Test plan