|
| 1 | +import pytest |
1 | 2 | import numpy as np |
| 3 | +import torch |
2 | 4 | import geopandas as gpd |
3 | 5 | import rasterio as rio |
4 | 6 | from shapely.geometry import box |
@@ -129,3 +131,90 @@ def test_full_coverage_polygon(self, sample_rasterio_src): |
129 | 131 | gdf = gpd.GeoDataFrame(geometry=[full_poly], crs=sample_rasterio_src.crs) |
130 | 132 | result = rasterize_vector(gdf, profile) |
131 | 133 | assert result.sum() == 100 * 100 |
| 134 | + |
| 135 | + |
| 136 | +class TestExportToDiskStreaming: |
| 137 | + """The debug path hands over a list of layers instead of a stacked array.""" |
| 138 | + |
| 139 | + def test_writes_each_layer_as_its_own_band(self, sample_geotiff, tmp_dir): |
| 140 | + layers = [ |
| 141 | + torch.full((100, 100), 3, dtype=torch.uint8), |
| 142 | + torch.ones((100, 100), dtype=torch.bool), |
| 143 | + ] |
| 144 | + export_path = tmp_dir / "streamed.tif" |
| 145 | + export_to_disk( |
| 146 | + array=layers, |
| 147 | + export_path=export_path, |
| 148 | + source_path=sample_geotiff, |
| 149 | + layer_names=["counts", "flags"], |
| 150 | + ) |
| 151 | + with rio.open(export_path) as src: |
| 152 | + assert src.count == 2 |
| 153 | + assert src.dtypes == ("float32", "float32") |
| 154 | + assert src.descriptions == ("counts", "flags") |
| 155 | + assert np.all(src.read(1) == 3.0) |
| 156 | + assert np.all(src.read(2) == 1.0) |
| 157 | + |
| 158 | + def test_releases_each_layer_as_it_is_written(self, sample_geotiff, tmp_dir): |
| 159 | + """The memory contract: a written layer must not still be referenced. |
| 160 | +
|
| 161 | + Holding all 14 debug layers as float32 plus a stacked copy is what cost |
| 162 | + 12.6 GB on a full Sentinel-2 tile. Streaming only helps if each source |
| 163 | + is actually dropped, which callers rely on. |
| 164 | + """ |
| 165 | + layers = [torch.ones((50, 50)), torch.zeros((50, 50))] |
| 166 | + export_to_disk( |
| 167 | + array=layers, |
| 168 | + export_path=tmp_dir / "released.tif", |
| 169 | + source_path=sample_geotiff, |
| 170 | + layer_names=["a", "b"], |
| 171 | + ) |
| 172 | + assert layers == [None, None] |
| 173 | + |
| 174 | + def test_none_layer_is_written_as_zeros(self, sample_geotiff, tmp_dir): |
| 175 | + layers = [torch.ones((100, 100)), None] |
| 176 | + export_path = tmp_dir / "with_none.tif" |
| 177 | + export_to_disk( |
| 178 | + array=layers, |
| 179 | + export_path=export_path, |
| 180 | + source_path=sample_geotiff, |
| 181 | + layer_names=["present", "missing"], |
| 182 | + ) |
| 183 | + with rio.open(export_path) as src: |
| 184 | + assert np.all(src.read(2) == 0.0) |
| 185 | + |
| 186 | + def test_uses_band_interleave_and_tiles(self, sample_geotiff, tmp_dir): |
| 187 | + """Band-at-a-time writes only compress well with BAND interleave.""" |
| 188 | + export_path = tmp_dir / "layout.tif" |
| 189 | + export_to_disk( |
| 190 | + array=[torch.ones((100, 100)), torch.zeros((100, 100))], |
| 191 | + export_path=export_path, |
| 192 | + source_path=sample_geotiff, |
| 193 | + layer_names=["a", "b"], |
| 194 | + ) |
| 195 | + with rio.open(export_path) as src: |
| 196 | + assert src.profile["tiled"] is True |
| 197 | + assert src.interleaving.name.lower() == "band" |
| 198 | + |
| 199 | + def test_all_none_is_rejected(self, sample_geotiff, tmp_dir): |
| 200 | + with pytest.raises(ValueError): |
| 201 | + export_to_disk( |
| 202 | + array=[None, None], |
| 203 | + export_path=tmp_dir / "empty.tif", |
| 204 | + source_path=sample_geotiff, |
| 205 | + layer_names=["a", "b"], |
| 206 | + ) |
| 207 | + |
| 208 | + def test_stacked_array_path_is_unchanged(self, sample_geotiff, tmp_dir): |
| 209 | + """The single-band mask still goes through as a numpy array.""" |
| 210 | + array = np.ones((1, 100, 100), dtype=np.uint8) |
| 211 | + export_path = tmp_dir / "mask.tif" |
| 212 | + export_to_disk( |
| 213 | + array=array, |
| 214 | + export_path=export_path, |
| 215 | + source_path=sample_geotiff, |
| 216 | + layer_names=["Water predictions"], |
| 217 | + ) |
| 218 | + with rio.open(export_path) as src: |
| 219 | + assert src.count == 1 |
| 220 | + assert src.dtypes == ("uint8",) |
0 commit comments