-
Notifications
You must be signed in to change notification settings - Fork 85
Expand file tree
/
Copy pathtest_vrt_source_nodata_zero_1655.py
More file actions
171 lines (151 loc) · 6.4 KB
/
test_vrt_source_nodata_zero_1655.py
File metadata and controls
171 lines (151 loc) · 6.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
"""Regression tests for issue #1655.
``read_vrt`` used to evaluate the per-source NODATA fallback as
``src.nodata or nodata``. Python treats ``0.0`` as falsy, so a
SimpleSource that declared ``<NODATA>0</NODATA>`` was silently replaced
with the band-level sentinel (or ``None`` when the band had none of its
own). Pixels equal to 0.0 in the source file survived as valid data and
biased every downstream NaN-aware aggregation.
The fix changes the fallback to an explicit ``is not None`` check so a
legitimate zero sentinel survives.
"""
from __future__ import annotations
import numpy as np
import pytest
from xrspatial.geotiff._geotags import GeoTransform
from xrspatial.geotiff._vrt import read_vrt
from xrspatial.geotiff._writer import write
def _write_source(tmp_path, arr, name='src_1655.tif'):
"""Write a small float32 GeoTIFF without a GDAL_NODATA tag."""
p = str(tmp_path / name)
write(
arr, p,
geo_transform=GeoTransform(
origin_x=0.0, origin_y=0.0,
pixel_width=1.0, pixel_height=-1.0,
),
crs_epsg=4326,
compression='none',
tiled=False,
)
return p
def _vrt_with_source_nodata(tmp_path, src_path, nodata_xml,
include_band_nodata=False,
width=4, height=3,
band_nodata='0.0'):
"""Write a single-band Float32 VRT with the supplied ``<NODATA>``
on its SimpleSource. ``include_band_nodata`` controls whether a
``<NoDataValue>`` is emitted on the band as well.
"""
band_nd_elem = (
f'<NoDataValue>{band_nodata}</NoDataValue>'
if include_band_nodata else '')
vrt_xml = (
f'<VRTDataset rasterXSize="{width}" rasterYSize="{height}">\n'
f' <SRS>EPSG:4326</SRS>\n'
f' <GeoTransform>0.0, 1.0, 0.0, 0.0, 0.0, -1.0</GeoTransform>\n'
f' <VRTRasterBand dataType="Float32" band="1">\n'
f' {band_nd_elem}\n'
f' <SimpleSource>\n'
f' <SourceFilename relativeToVRT="0">{src_path}</SourceFilename>\n'
f' <SourceBand>1</SourceBand>\n'
f' <SrcRect xOff="0" yOff="0" '
f'xSize="{width}" ySize="{height}"/>\n'
f' <DstRect xOff="0" yOff="0" '
f'xSize="{width}" ySize="{height}"/>\n'
f' <NODATA>{nodata_xml}</NODATA>\n'
f' </SimpleSource>\n'
f' </VRTRasterBand>\n'
f'</VRTDataset>\n'
)
vrt_path = str(tmp_path / 'src_zero_1655.vrt')
with open(vrt_path, 'w') as f:
f.write(vrt_xml)
return vrt_path
class TestVRTSourceNodataZero:
"""SimpleSource ``<NODATA>0</NODATA>`` must mask zeros to NaN."""
def test_source_nodata_zero_no_band_nodata(self, tmp_path):
"""SimpleSource NODATA=0 with no band-level fallback masks zeros."""
arr = np.array(
[[1.0, 0.0, 3.0, 0.0],
[4.0, 0.0, 6.0, 7.0],
[0.0, 8.0, 9.0, 10.0]],
dtype=np.float32,
)
src = _write_source(tmp_path, arr)
vrt = _vrt_with_source_nodata(tmp_path, src, '0.0')
result, _ = read_vrt(vrt)
assert int(np.isnan(result).sum()) == 4
def test_source_nodata_zero_integer_xml(self, tmp_path):
"""``<NODATA>0</NODATA>`` (integer literal) also masks zeros."""
arr = np.array(
[[1.0, 0.0, 3.0]],
dtype=np.float32,
)
src = _write_source(tmp_path, arr, name='int_xml.tif')
vrt = _vrt_with_source_nodata(
tmp_path, src, '0', width=3, height=1)
result, _ = read_vrt(vrt)
assert int(np.isnan(result).sum()) == 1
assert np.isnan(result[0, 1])
def test_source_nodata_nonzero_unchanged(self, tmp_path):
"""SimpleSource NODATA != 0 keeps masking behaviour."""
arr = np.array(
[[1.0, 0.0, 3.0, 0.0]],
dtype=np.float32,
)
src = _write_source(tmp_path, arr, name='nonzero.tif')
vrt = _vrt_with_source_nodata(
tmp_path, src, '1.0', width=4, height=1)
result, _ = read_vrt(vrt)
# Only the literal 1.0 at [0, 0] should be masked.
assert int(np.isnan(result).sum()) == 1
assert np.isnan(result[0, 0])
def test_band_nodata_zero_still_honoured(self, tmp_path):
"""Band-level ``<NoDataValue>0</NoDataValue>`` keeps working."""
arr = np.array(
[[1.0, 0.0, 3.0]],
dtype=np.float32,
)
src = _write_source(tmp_path, arr, name='band_zero.tif')
# Build a VRT where only the band carries nodata=0 (no NODATA
# on the SimpleSource).
vrt_xml = (
f'<VRTDataset rasterXSize="3" rasterYSize="1">\n'
f' <SRS>EPSG:4326</SRS>\n'
f' <GeoTransform>0.0, 1.0, 0.0, 0.0, 0.0, -1.0</GeoTransform>\n'
f' <VRTRasterBand dataType="Float32" band="1">\n'
f' <NoDataValue>0.0</NoDataValue>\n'
f' <SimpleSource>\n'
f' <SourceFilename relativeToVRT="0">{src}</SourceFilename>\n'
f' <SourceBand>1</SourceBand>\n'
f' <SrcRect xOff="0" yOff="0" xSize="3" ySize="1"/>\n'
f' <DstRect xOff="0" yOff="0" xSize="3" ySize="1"/>\n'
f' </SimpleSource>\n'
f' </VRTRasterBand>\n'
f'</VRTDataset>\n'
)
vrt = str(tmp_path / 'band_zero_1655.vrt')
with open(vrt, 'w') as f:
f.write(vrt_xml)
result, _ = read_vrt(vrt)
assert int(np.isnan(result).sum()) == 1
assert np.isnan(result[0, 1])
def test_source_nodata_zero_overrides_band(self, tmp_path):
"""SimpleSource NODATA=0 takes precedence over band NoDataValue=99."""
arr = np.array(
[[1.0, 0.0, 99.0]],
dtype=np.float32,
)
src = _write_source(tmp_path, arr, name='override.tif')
vrt = _vrt_with_source_nodata(
tmp_path, src, '0.0',
include_band_nodata=True, band_nodata='99.0',
width=3, height=1)
result, _ = read_vrt(vrt)
# The SimpleSource sentinel (0.0) wins over the band sentinel
# (99.0), so only the 0.0 cell becomes NaN. The 99.0 cell stays
# because the masking is per-source, applied at read time, and
# the band-level fallback never fires when src.nodata is set.
assert int(np.isnan(result).sum()) == 1
assert np.isnan(result[0, 1])
assert result[0, 2] == pytest.approx(99.0)