Skip to content

Commit cdcd491

Browse files
authored
Reference docs gap-fill in geotiff.rst (#2345 PR 2) — closes #2380 (#2387)
* Reference docs gap-fill in geotiff.rst (#2345 PR 2) Add the five sections the release-gate audit checklist promised but the reference page did not surface yet: * Nodata lifecycle (cross-links to user_guide.attrs_contract for the full lifecycle so the page does not duplicate that contract) * Rotated and sheared transforms (read posture, write posture, and the failure-closed combinations) * Remote-read safety limits with the full env-var quick reference and the regression test behind each knob * GPU support, explicitly tagged experimental, with the SUPPORTED_FEATURES tier each path reports and what users should and should not expect * Known unsupported combinations, as a matrix that names every combo and the regression test that locks it Each row points to the regression test that backs it. None of the new prose contradicts the existing tier table, SUPPORTED_FEATURES, or release_gate_geotiff.rst. Closes #2380. * Address review: fix mis-attributed test citations (#2380) Self-review on PR #2387 flagged four test functions cited against the wrong test file. Each lives in test_unsupported_features_2349.py, not test_vrt_unsupported_2370.py: * test_eager_writer_rejects_rotated_6tuple_transform * test_eager_writer_rejects_rotated_affine_attr * test_mixed_per_source_nodata_rejected * test_vrt_with_skewed_geotransform_rejected Also: * Replace the vague "issue #2214 regression suite" pointer on the degenerate-axis row with the actual test file (test_degenerate_pixel_size_2214.py). * Narrow the GPU-codec claim so it matches what the regression test actually locks: the GPU writer routes unsupported codecs through a CPU fallback inside write_geotiff_gpu, locked by test_gpu_writer_cpu_fallback_codecs_2026_05_12.py.
1 parent 1896d63 commit cdcd491

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

docs/source/reference/geotiff.rst

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,49 @@ GeoTIFF / COG
1515
checklist that lists every promised feature on this page, its tier,
1616
its one-line acceptance, and the regression test that locks it.
1717

18+
:ref:`user_guide.attrs_contract` -- the user-guide page that defines
19+
which attrs keys are canonical, which are aliases, and which are
20+
pass-through, and the round-trip guarantees that apply to each
21+
tier.
22+
23+
GPU support (experimental)
24+
==========================
25+
26+
The GPU read and write paths are tagged ``experimental`` in
27+
:data:`xrspatial.geotiff.SUPPORTED_FEATURES`. Both
28+
``SUPPORTED_FEATURES['reader.gpu']`` and
29+
``SUPPORTED_FEATURES['writer.gpu']`` report ``experimental``: the paths
30+
work and are covered by tests, but the surface can shift without a
31+
deprecation window. The GPU paths are not a release blocker -- a
32+
regression on a GPU row does not fail the build the way a regression
33+
on the stable CPU surface does.
34+
35+
What you can expect:
36+
37+
* GPU read and write produce the same pixels and the same canonical
38+
attrs as the CPU path on the supported codec subset. The eager and
39+
dask GPU readers are covered by
40+
``xrspatial/geotiff/tests/test_golden_corpus_gpu_1930.py`` and
41+
``xrspatial/geotiff/tests/test_golden_corpus_dask_gpu_1930.py``.
42+
* Integer and float nodata sentinels survive the GPU round-trip; see
43+
``xrspatial/geotiff/tests/test_gpu_nodata_1542.py``.
44+
* On GPU failure the reader emits
45+
:class:`xrspatial.geotiff.GeoTIFFFallbackWarning` and falls back to
46+
CPU unless ``on_gpu_failure='strict'`` or
47+
``XRSPATIAL_GEOTIFF_STRICT=1`` is set; see
48+
``xrspatial/geotiff/tests/test_gpu_strict_fallback_1516.py``.
49+
50+
What you should NOT rely on:
51+
52+
* GPU support for every codec on the CPU path. ``allow_experimental_codecs``
53+
does NOT widen the GPU codec set; on the GPU writer, codecs outside the
54+
GPU-supported set route through a CPU fallback inside
55+
``write_geotiff_gpu`` rather than executing on the GPU. Locked by
56+
``xrspatial/geotiff/tests/test_gpu_writer_cpu_fallback_codecs_2026_05_12.py``.
57+
* GPU promotion to ``stable`` inside this release cycle. See the GPU
58+
rows in :ref:`reference.geotiff_release_gate` for the current tier
59+
and the regression tests behind each row.
60+
1861
Stable COG contract
1962
===================
2063

@@ -58,6 +101,85 @@ the corresponding caveats:
58101
* HTTP / range COG (tracked separately; see the byte-budget contract in
59102
#2298).
60103

104+
Rotated and sheared transforms
105+
==============================
106+
107+
Read posture. ``open_geotiff`` rejects a file whose affine transform
108+
has non-zero rotation or shear coefficients by default. Pass
109+
``allow_rotated=True`` to opt in: the read then surfaces the rotated
110+
6-tuple on ``attrs['rotated_affine']`` and drops ``attrs['crs']`` so
111+
downstream math cannot silently mix a rotated grid with an
112+
axis-aligned CRS. The dropped-CRS rule is locked by
113+
``xrspatial/geotiff/tests/test_allow_rotated_crs_drop_2126.py``,
114+
``xrspatial/geotiff/tests/test_allow_rotated_no_crs_2122.py``, and
115+
``xrspatial/geotiff/tests/test_allow_rotated_geotiff_2115.py``. The
116+
HTTP dask path honours the same opt-in via
117+
``xrspatial/geotiff/tests/test_http_dask_allow_rotated_2130.py``.
118+
Without ``allow_rotated=True`` the read raises a typed error; see
119+
``xrspatial/geotiff/tests/test_rotated_typed_error_2267.py``.
120+
121+
Write posture. ``to_geotiff`` rejects a DataArray carrying
122+
``attrs['rotated_affine']`` unless the caller also passes
123+
``drop_rotation=True``. With the opt-in, the writer drops the rotated
124+
affine and writes an axis-aligned file from the coords. This is
125+
locked by ``xrspatial/geotiff/tests/test_to_geotiff_drop_rotation_2216.py``.
126+
A rotated or skewed 6-tuple supplied through ``attrs['transform']``
127+
or through a VRT source is also rejected; see
128+
``xrspatial/geotiff/tests/test_unsupported_features_2349.py``
129+
(``test_eager_writer_rejects_rotated_6tuple_transform`` and
130+
``test_vrt_with_skewed_geotransform_rejected``).
131+
132+
Failure-closed combinations. The following inputs raise rather than
133+
silently emit a mislabeled raster:
134+
135+
* Rotated read without ``allow_rotated=True`` -- raises across eager,
136+
dask, and windowed paths
137+
(``xrspatial/geotiff/tests/test_release_gate_negative_2341.py``).
138+
* Rotated write without ``drop_rotation=True`` -- raises ``ValueError``
139+
(``xrspatial/geotiff/tests/test_to_geotiff_drop_rotation_2216.py``).
140+
* Rotated or skewed source inside a VRT -- raises at parse
141+
(``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py``).
142+
143+
Nodata lifecycle
144+
================
145+
146+
This page summarises the read / write contract. The full lifecycle
147+
of every attrs key, including which keys are canonical, which are
148+
aliases, and which are pass-through, lives in
149+
:ref:`user_guide.attrs_contract`. Do not duplicate that page here;
150+
this section is the brief.
151+
152+
* Integer nodata. The on-disk sentinel survives the read bit-exact
153+
and is preserved on the next write. ``attrs['nodata']`` carries
154+
the sentinel as a Python ``int``. Out-of-range sentinels for the
155+
band dtype are rejected at write
156+
(``xrspatial/geotiff/tests/test_nodata_out_of_range_1581.py``).
157+
* Float nodata. The on-disk sentinel is recorded on
158+
``attrs['nodata']`` and surfaces as NaN in pixel data only when the
159+
read promotes via ``mask_nodata=True`` (the default for float
160+
outputs). With ``mask_nodata=False`` the raw float sentinel passes
161+
through, so downstream callers can branch on the exact value;
162+
``xrspatial/geotiff/tests/test_mask_nodata_kwarg_2052.py`` pins this
163+
split.
164+
* NaN nodata. A file that declares ``nodata=NaN`` is read with NaN in
165+
both ``attrs['nodata']`` and pixel data (NaN propagates either way).
166+
* ``attrs['masked_nodata']``. Every read sets a boolean lifecycle
167+
signal: ``True`` when the read produced NaN-masked output distinct
168+
from the on-disk sentinel, ``False`` when pixel data carries the
169+
raw sentinel. The signal is part of the canonical attrs contract;
170+
``xrspatial/geotiff/tests/test_masked_nodata_attr_2092.py`` pins
171+
the canonical form and
172+
``xrspatial/geotiff/tests/test_vrt_masked_nodata_attr_2159.py``
173+
covers the VRT mosaic case.
174+
* Mixed-band nodata. A VRT whose sources declare disagreeing per-band
175+
nodata sentinels raises ``MixedBandMetadataError`` by default. Pass
176+
``band_nodata='first'`` to opt back into the legacy flatten-to-band-0
177+
behaviour; see ``xrspatial/geotiff/tests/test_vrt_band_nodata_1598.py``.
178+
179+
The lifecycle is locked end-to-end by
180+
``xrspatial/geotiff/tests/test_nodata_lifecycle_attrs_2135.py`` and
181+
``xrspatial/geotiff/tests/test_nodata_lifecycle_parity_2211.py``.
182+
61183
Reading
62184
=======
63185
.. autosummary::
@@ -147,6 +269,49 @@ If you run an integration test against a local HTTP server (e.g.
147269
``XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS=1`` for the duration of the
148270
test.
149271

272+
Remote-read safety limits and env vars
273+
--------------------------------------
274+
275+
The reader applies a layered budget to every remote ``http://`` or
276+
``https://`` read so a single hostile file cannot exhaust memory or
277+
turn the process into a port scanner. The knobs are:
278+
279+
* ``max_cloud_bytes`` (kwarg) / ``XRSPATIAL_GEOTIFF_MAX_CLOUD_BYTES``
280+
(env). Per-call total byte budget for a remote read. The kwarg wins
281+
over the env var; the env var wins over the built-in default. Pass
282+
``max_cloud_bytes=None`` to disable the cap on a single call. Locked
283+
by ``xrspatial/geotiff/tests/test_max_cloud_bytes_dispatcher_silent_drop_2026_05_15.py``,
284+
``xrspatial/geotiff/tests/test_open_geotiff_max_cloud_bytes_annot_2106.py``,
285+
and ``xrspatial/geotiff/tests/test_http_read_all_bounded_2051.py``.
286+
* ``XRSPATIAL_COG_MAX_TILE_BYTES``. Per-tile / per-strip compressed
287+
byte cap (default 256 MiB). Locked by
288+
``xrspatial/geotiff/tests/test_local_tile_byte_cap_1664.py``,
289+
``xrspatial/geotiff/tests/test_cloud_read_byte_limit_1928.py``, and
290+
``xrspatial/geotiff/tests/test_gpu_tile_byte_cap_2026_05_18.py``.
291+
* ``XRSPATIAL_GEOTIFF_HTTP_CONNECT_TIMEOUT`` and
292+
``XRSPATIAL_GEOTIFF_HTTP_READ_TIMEOUT``. Per-request connect / read
293+
timeouts in seconds. Positive floats only; other values fall back
294+
to the defaults (10 s and 30 s). Range coalescing inside one read
295+
shares a single connection so the connect timeout applies once per
296+
host, not once per range.
297+
* ``XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS``. Set to ``1`` (or
298+
``true`` / ``yes``) to disable the private-host reject. Off by
299+
default; locked by
300+
``xrspatial/geotiff/tests/test_ssrf_hardening_1664.py``,
301+
``xrspatial/geotiff/tests/test_dns_rebinding_pin_issue_1846.py``,
302+
and ``xrspatial/geotiff/tests/test_uppercase_scheme_ssrf_2323.py``.
303+
* ``XRSPATIAL_VRT_ALLOWED_ROOTS``. Colon-separated list of additional
304+
directory roots that a VRT is allowed to reference. The default
305+
containment rule (sources must live under the VRT's directory) is
306+
locked by ``xrspatial/geotiff/tests/test_vrt_path_containment_1671.py``.
307+
* ``XRSPATIAL_GEOTIFF_STRICT``. Promotes the fallback warnings into
308+
raised exceptions, including the GPU-fallback path; see the next
309+
section.
310+
311+
The same byte budget applies to sidecar fetches, not just the parent
312+
file
313+
(``xrspatial/geotiff/tests/test_sidecar_max_cloud_bytes_2121.py``).
314+
150315
Strict mode (``XRSPATIAL_GEOTIFF_STRICT``)
151316
==========================================
152317

@@ -336,3 +501,70 @@ single-band and 3-band, one overview level, plus an auto-promotion row
336501
that drives the threshold via the IFD-overhead helper rather than
337502
allocating a multi-gigabyte buffer. Promotion to ``stable`` follows the
338503
same release-cycle soak rule as the rest of the COG surface.
504+
505+
Known unsupported combinations
506+
==============================
507+
508+
The combinations below fail closed today: they raise a typed error
509+
rather than emit a possibly-wrong raster. Each row names the
510+
regression test that locks the behaviour.
511+
512+
.. list-table::
513+
:header-rows: 1
514+
:widths: 35 65
515+
516+
* - Combination
517+
- Regression test
518+
* - ``to_geotiff(cog=True, tiled=False)``
519+
- ``xrspatial/geotiff/tests/test_cog_requires_tiled_2312.py``
520+
* - ``to_geotiff(cog=True, tile_size <= 0)``
521+
- ``xrspatial/geotiff/tests/test_cog_tile_size_hang_2311.py``
522+
* - Warped VRT
523+
(``<VRTDataset subClass="VRTWarpedDataset">`` or
524+
``<VRTRasterBand subClass="VRTWarpedRasterBand">``)
525+
- ``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py``,
526+
``xrspatial/geotiff/tests/test_vrt_capability_validator_2371.py``
527+
* - Nested VRT (a ``<SourceFilename>`` that resolves to a ``.vrt``)
528+
- ``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py``
529+
(``test_nested_vrt_source_raises``,
530+
``test_nested_vrt_open_geotiff_raises``)
531+
* - Mixed-CRS VRT (sources disagree on CRS without an opt-in)
532+
- ``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py``,
533+
``xrspatial/geotiff/tests/test_vrt_capability_validator_2371.py``
534+
* - Mixed per-band nodata across VRT sources (default
535+
``band_nodata=None``)
536+
- ``xrspatial/geotiff/tests/test_vrt_band_nodata_1598.py``,
537+
``xrspatial/geotiff/tests/test_unsupported_features_2349.py``
538+
(``test_mixed_per_source_nodata_rejected``)
539+
* - Rotated read without ``allow_rotated=True``
540+
- ``xrspatial/geotiff/tests/test_release_gate_negative_2341.py``,
541+
``xrspatial/geotiff/tests/test_rotated_typed_error_2267.py``
542+
* - Rotated write without ``drop_rotation=True``
543+
- ``xrspatial/geotiff/tests/test_to_geotiff_drop_rotation_2216.py``,
544+
``xrspatial/geotiff/tests/test_unsupported_features_2349.py``
545+
(``test_eager_writer_rejects_rotated_6tuple_transform``,
546+
``test_eager_writer_rejects_rotated_affine_attr``)
547+
* - Skewed VRT geotransform
548+
- ``xrspatial/geotiff/tests/test_unsupported_features_2349.py``
549+
(``test_vrt_with_skewed_geotransform_rejected``)
550+
* - Complex source / mask band / alpha band in a VRT
551+
- ``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py``,
552+
``xrspatial/geotiff/tests/test_vrt_capability_validator_2371.py``
553+
* - VRT source path escapes the VRT directory tree
554+
- ``xrspatial/geotiff/tests/test_vrt_path_containment_1671.py``
555+
* - 1xN / Nx1 write without ``attrs['transform']`` or
556+
``assume_square_pixels_for_degenerate_axis=True``
557+
- ``xrspatial/geotiff/tests/test_degenerate_pixel_size_2214.py``;
558+
see also "Degenerate-axis writes" above.
559+
* - HTTP read against a private / loopback / link-local host
560+
without ``XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS=1``
561+
- ``xrspatial/geotiff/tests/test_ssrf_hardening_1664.py``,
562+
``xrspatial/geotiff/tests/test_dns_rebinding_pin_issue_1846.py``
563+
* - Unsupported feature flags more broadly (codec, layout, and
564+
writer combos that ``SUPPORTED_FEATURES`` does not promise)
565+
- ``xrspatial/geotiff/tests/test_unsupported_features_2349.py``
566+
567+
This list is the prose mirror of the negative rows in
568+
:ref:`reference.geotiff_release_gate`. When a row gets promoted or
569+
removed, update both pages in the same PR so the docs and the runtime
570+
constant stay in sync.

0 commit comments

Comments
 (0)