Skip to content

Commit 3e52c8d

Browse files
committed
Make Scene bilinear reduction halo-aware and single-stage
1 parent f5bd750 commit 3e52c8d

File tree

2 files changed

+143
-12
lines changed

2 files changed

+143
-12
lines changed

satpy/scene.py

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040

4141
LOG = logging.getLogger(__name__)
4242

43+
BILINEAR_REDUCTION_HALO = 1
44+
4345

4446
def _get_area_resolution(area):
4547
"""Attempt to retrieve resolution from AreaDefinition."""
@@ -60,6 +62,13 @@ def _aggregate_data_array(data_array, func, **coarsen_kwargs):
6062
return out
6163

6264

65+
def _expand_slice(slc: slice, size: int, padding: int):
66+
"""Expand a slice by a symmetric padding within array bounds."""
67+
start = 0 if slc.start is None else max(0, slc.start - padding)
68+
stop = size if slc.stop is None else min(size, slc.stop + padding)
69+
return slice(start, stop, slc.step)
70+
71+
6372
class DelayedGeneration(KeyError):
6473
"""Mark that a dataset can't be generated without further modification."""
6574

@@ -873,6 +882,7 @@ def _resampled_scene(self, new_scn, destination_area, reduce_data=True,
873882
new_datasets = {}
874883
datasets = list(new_scn._datasets.values())
875884
destination_area = self._get_finalized_destination_area(destination_area, new_scn)
885+
resampler_kwargs = self._get_resampler_kwargs(resample_kwargs)
876886

877887
resamplers = {}
878888
reductions = {}
@@ -896,8 +906,8 @@ def _resampled_scene(self, new_scn, destination_area, reduce_data=True,
896906
source_area = dataset.attrs["area"]
897907
dataset, source_area = self._reduce_data(dataset, source_area, destination_area,
898908
reduce_data, reductions, resample_kwargs)
899-
self._prepare_resampler(source_area, destination_area, resamplers, resample_kwargs)
900-
kwargs = resample_kwargs.copy()
909+
self._prepare_resampler(source_area, destination_area, resamplers, resampler_kwargs)
910+
kwargs = resampler_kwargs.copy()
901911
kwargs["resampler"] = resamplers[source_area]
902912
res = resample_dataset(dataset, destination_area, **kwargs)
903913
new_datasets[ds_id] = res
@@ -927,23 +937,48 @@ def _prepare_resampler(self, source_area, destination_area, resamplers, resample
927937
resamplers[source_area] = resampler
928938
self._resamplers[key] = resampler
929939

940+
@staticmethod
941+
def _uses_bilinear_resampler(resampler: Any):
942+
if resampler == "bilinear":
943+
return True
944+
try:
945+
from satpy.resample.kdtree import BilinearResampler
946+
except ImportError:
947+
return False
948+
if isinstance(resampler, BilinearResampler):
949+
return True
950+
if isinstance(resampler, type):
951+
return issubclass(resampler, BilinearResampler)
952+
return False
953+
954+
def _get_resampler_kwargs(self, resample_kwargs: dict[str, Any]):
955+
resampler_kwargs = resample_kwargs.copy()
956+
if self._uses_bilinear_resampler(resampler_kwargs.get("resampler")):
957+
resampler_kwargs["reduce_data"] = False
958+
return resampler_kwargs
959+
960+
def _get_area_slice_kwargs(self, resample_kwargs: dict[str, Any]):
961+
if resample_kwargs.get("resampler") == "gradient_search":
962+
return {"shape_divisible_by": resample_kwargs.get("shape_divisible_by", 2)}
963+
return {}
964+
965+
def _pad_reduce_data_slices(self, source_area, slice_x, slice_y, resample_kwargs):
966+
if self._uses_bilinear_resampler(resample_kwargs.get("resampler")):
967+
slice_x = _expand_slice(slice_x, source_area.width, BILINEAR_REDUCTION_HALO)
968+
slice_y = _expand_slice(slice_y, source_area.height, BILINEAR_REDUCTION_HALO)
969+
return slice_x, slice_y
970+
930971
def _reduce_data(self, dataset, source_area, destination_area, reduce_data, reductions, resample_kwargs):
931972
try:
932973
if reduce_data:
933974
key = source_area
934975
try:
935976
(slice_x, slice_y), source_area = reductions[key]
936977
except KeyError:
937-
if resample_kwargs.get("resampler") == "gradient_search":
938-
factor = resample_kwargs.get("shape_divisible_by", 2)
939-
else:
940-
factor = None
941-
try:
942-
slice_x, slice_y = source_area.get_area_slices(
943-
destination_area, shape_divisible_by=factor)
944-
except TypeError:
945-
slice_x, slice_y = source_area.get_area_slices(
946-
destination_area)
978+
slice_kwargs = self._get_area_slice_kwargs(resample_kwargs)
979+
slice_x, slice_y = source_area.get_area_slices(destination_area, **slice_kwargs)
980+
slice_x, slice_y = self._pad_reduce_data_slices(
981+
source_area, slice_x, slice_y, resample_kwargs)
947982
source_area = source_area[slice_y, slice_x]
948983
reductions[key] = (slice_x, slice_y), source_area
949984
dataset = self._slice_data(source_area, (slice_x, slice_y), dataset)

satpy/tests/scene_tests/test_resampling.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,47 @@ def _fake_resample_dataset_force_20x20(self, dataset, dest_area, **kwargs):
194194
attrs=attrs,
195195
)
196196

197+
@staticmethod
198+
def _geos_cross_projection_areas():
199+
from pyresample.geometry import AreaDefinition
200+
201+
source_area = AreaDefinition(
202+
"src",
203+
"src",
204+
"src",
205+
{
206+
"proj": "geos",
207+
"a": 6378169.0,
208+
"b": 6356583.8,
209+
"h": 35785831.0,
210+
"lon_0": 0.0,
211+
"units": "m",
212+
},
213+
3712,
214+
1392,
215+
(5568748.0, 5568748.0, -5568748.0, 1392187.0),
216+
)
217+
target_area = AreaDefinition(
218+
"dst",
219+
"dst",
220+
"dst",
221+
{"proj": "latlong", "datum": "WGS84"},
222+
768,
223+
768,
224+
(7.456086060029671, 43.98125198600542, 33.461915849571966, 59.24271064133026),
225+
)
226+
return source_area, target_area
227+
228+
@staticmethod
229+
def _geos_cross_projection_data(source_area):
230+
y = da.arange(source_area.height, chunks=256)[:, None]
231+
x = da.arange(source_area.width, chunks=256)[None, :]
232+
return xr.DataArray(
233+
(y * 10000 + x).astype(np.float64),
234+
dims=("y", "x"),
235+
attrs={"area": source_area, "name": "edge"},
236+
)
237+
197238
@mock.patch("satpy.resample.base.resample_dataset")
198239
@pytest.mark.parametrize("datasets", [
199240
None,
@@ -335,6 +376,61 @@ def test_resample_reduce_data_toggle(self, rs):
335376
assert get_area_slices.call_count == 2
336377
assert get_area_slices_big.call_count == 2
337378

379+
@mock.patch("satpy.resample.base.resample_dataset")
380+
def test_resample_bilinear_reduce_data_uses_halo_and_disables_internal_reduce_data(self, rs):
381+
"""Test bilinear Scene reduction pads the crop and avoids internal reduction."""
382+
from pyresample.geometry import AreaDefinition
383+
384+
rs.side_effect = self._fake_resample_dataset_force_20x20
385+
proj_str = ("+proj=lcc +datum=WGS84 +ellps=WGS84 "
386+
"+lon_0=-95. +lat_0=25 +lat_1=25 +units=m +no_defs")
387+
target_area = AreaDefinition("test", "test", "test", proj_str, 4, 4, (-1000., -1500., 1000., 1500.))
388+
area_def = AreaDefinition("test", "test", "test", proj_str, 5, 5, (-1000., -1500., 1000., 1500.))
389+
area_def.get_area_slices = mock.MagicMock(return_value=(slice(1, 3, None), slice(1, 3, None)))
390+
391+
scene = Scene(filenames=["fake1_1.txt"], reader="fake1")
392+
scene.load(["comp19"])
393+
scene["comp19"].attrs["area"] = area_def
394+
395+
scene.resample(target_area, resampler="bilinear", reduce_data=True)
396+
397+
dataset = rs.call_args.args[0]
398+
assert dataset.sizes["y"] == 4
399+
assert dataset.sizes["x"] == 4
400+
assert dataset.attrs["area"].width == 4
401+
assert dataset.attrs["area"].height == 4
402+
assert rs.call_args.kwargs["reduce_data"] is False
403+
404+
def test_resample_bilinear_reduce_data_halo_matches_full_source_on_edge_case(self):
405+
"""Test bilinear Scene reduction keeps edge pixels with a 1-pixel halo."""
406+
from pyresample.bilinear import XArrayBilinearResampler
407+
408+
source_area, target_area = self._geos_cross_projection_areas()
409+
scene = Scene()
410+
scene["edge"] = self._geos_cross_projection_data(source_area)
411+
412+
baseline = scene.resample(target_area, resampler="bilinear", reduce_data=False)["edge"].compute()
413+
reduced = scene.resample(target_area, resampler="bilinear", reduce_data=True)["edge"].compute()
414+
415+
slice_x, slice_y = source_area.get_area_slices(target_area)
416+
tight_data = scene["edge"].isel(x=slice_x, y=slice_y)
417+
tight_area = source_area[slice_y, slice_x]
418+
tight = XArrayBilinearResampler(
419+
tight_area,
420+
target_area,
421+
50000,
422+
reduce_data=False,
423+
).resample(tight_data).compute()
424+
425+
baseline_values = baseline.values
426+
reduced_values = reduced.values
427+
tight_values = tight.values
428+
finite_both = np.isfinite(reduced_values) & np.isfinite(baseline_values)
429+
max_abs_diff = np.max(np.abs(reduced_values[finite_both] - baseline_values[finite_both]))
430+
np.testing.assert_array_equal(np.isfinite(reduced_values), np.isfinite(baseline_values))
431+
assert max_abs_diff < 0.02
432+
assert np.count_nonzero(np.isfinite(baseline_values) & ~np.isfinite(tight_values)) == 4
433+
338434
def test_resample_ancillary(self):
339435
"""Test that the Scene reducing data does not affect final output."""
340436
from pyresample.geometry import AreaDefinition

0 commit comments

Comments
 (0)