geotiff: drop attrs['crs'] on rotated reads (#2122)#2125
Merged
Conversation
The ``open_geotiff`` docstring promises ``attrs['crs']`` is dropped on rotated reads opened with ``allow_rotated=True``, so downstream code that uses ``'crs' in da.attrs`` as the "this raster is georeferenced" signal does not treat a no-georef pixel grid as projected. Before this commit, ``_populate_attrs_from_geo_info`` only gated ``attrs['transform']`` on the ``has_georef`` flag and kept emitting ``crs`` / ``crs_wkt`` from the GeoKey block, so the documented contract failed in practice. The marker for "rotated read opted in via allow_rotated=True" is ``geo_info.transform.rotated_affine`` -- the geotag parser sets the rotated 6-tuple on that field when it sees a rotated ``ModelTransformationTag`` under the opt-in. Gate the CRS attrs on the absence of that marker. General no-georef reads (axis-aligned rasters that simply lack transform tags, e.g. arrays written with ``to_geotiff(..., crs=NNN)`` and no coords) still surface ``crs`` / ``crs_wkt`` because the CRS is meaningful even without an embedded transform; only the rotated case is misleading. Mirror the same gate on the eager and chunked VRT backends, keyed on the GDAL GeoTransform's ``b`` / ``d`` rotation terms. The VRT inline attrs build had the same flaw on both code paths. Tests cover numpy, dask, cupy (xfail until the GPU CPU-fallback forwards allow_rotated -- sibling finding), dask+cupy, VRT eager, and VRT chunked. Regression guards confirm axis-aligned reads still emit ``crs`` / ``crs_wkt`` / ``transform`` and that the writer's ``crs=`` kwarg path round-trips cleanly when no coords are supplied. Closes #2122.
CI failed with ModuleNotFoundError because the fast lane does not
install tifffile. Switch the two `import tifffile` sites in
`test_allow_rotated_no_crs_2122.py` to
`pytest.importorskip("tifffile")`, matching the convention used by
other geotiff tests that depend on tifffile for fixture writes.
Only affects the VRT and aligned-fixture tests; the
`_write_rotated_tiff_with_crs` helper has no tifffile dependency.
# Conflicts: # xrspatial/geotiff/_backends/vrt.py
Same fix as PR #2140: the three VRT tests in test_masked_nodata_attr_2092 import tifffile inside _write_int_vrt, but tifffile is not in the [tests] extras. Use pytest.importorskip to match the pattern in test_wkt_only_crs_warning_1768 and friends. Carries the fix on this branch so CI goes green without waiting on #2140 to merge.
Contributor
Author
PR Review: geotiff: drop attrs['crs'] on rotated reads (#2122)The fix closes the documented gap where Blockers (must fix before merge)None Suggestions (should fix, not blocking)
Nits (optional improvements)
What looks good
Checklist
|
Follow-up on review of PR #2125: both VRT entry points (eager and chunked) drop crs / crs_wkt / transform on rotated reads, but still emit float projected coords and skip the _xrspatial_no_georef marker. The eager non-VRT rotated path emits int64 pixel coords and stamps the marker, so a downstream consumer saw the two backends disagree on the same input. Compute _vrt_is_rotated before the coord block in the eager VRT path, then thread has_georef=not _vrt_is_rotated into _coords_from_pixel_geometry in both VRT paths. Stamp _NO_GEOREF_KEY=True on the rotated branch in both paths, matching the no-transform arm and the eager non-VRT path in _attrs.py. Extend test_vrt_eager_rotated_read_drops_crs and test_vrt_chunked_rotated_read_drops_crs to assert int64 x/y dtype and _NO_GEOREF_KEY=True so the parity gap is pinned.
Contributor
Author
Review follow-upPushed
Full geotiff suite still passes locally (664 / 1 xfailed). Waiting on CI. |
# Conflicts: # xrspatial/geotiff/_attrs.py
brendancol
added a commit
that referenced
this pull request
May 20, 2026
#2146 (nodata lifecycle) and #2150 (tier surface) had landed cleanly on main without touching _populate_attrs_from_geo_info, but #2125's "drop attrs['crs'] on rotated reads" introduced a _vrt_is_rotated gate on the VRT branches that our pre-merge dataclass-based VRT code did not honour. Resolved by: * _attrs.py: kept the GeoTIFFMetadata shim. The dataclass already produces the same attrs surface as the legacy field-by-field path. * _backends/vrt.py: kept the dataclass build on both VRT branches and threaded _vrt_is_rotated through to drop crs/crs_wkt and set has_georef=False on rotated reads (so metadata_to_attrs stamps the no-georef marker). Verified test_allow_rotated_no_crs_2122, test_geotiff_metadata_2139, test_nodata_lifecycle_attrs_2135, test_supported_features_tiers_2137, test_backend_parity_matrix, and the broader VRT/attrs/nodata suite (1309 tests) all pass.
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 #2122.
_populate_attrs_from_geo_infonow dropsattrs['crs']andattrs['crs_wkt']on rotated reads opened withallow_rotated=True, matching the public docstring contract. The gate keys ongeo_info.transform.rotated_affine(set by the geotag parser when a rotatedModelTransformationTagis consumed under the opt-in), so general no-georef reads still surface CRS attrs when the file has one declared but no transform tags.b/drotation terms.docs/source/user_guide/attrs_contract.rstand the_attrs.pymodule docstring to reflect the rotated-read exception.Backend coverage
allow_rotated(sibling finding tracked separately)Test plan
pytest xrspatial/geotiff/tests/test_allow_rotated_no_crs_2122.py: 7 pass + 1 xfail.pytest xrspatial/geotiff/tests/ -k "not gpu and not cuda": 3511 pass, 17 skipped, 1 xfailed (no regressions).da.attrs.get('crs') is Noneand'crs_wkt' not in da.attrs.crs/crs_wkt/transform.to_geotiff(arr, path, crs=32610)(no coords) still round-trips the CRS through a read.