Skip to content

Commit 70b57a0

Browse files
authored
Merge pull request #1003 from xcube-dev/forman-1001-resample_in_space_issues
Address issues with `resample_in_space()`
2 parents 6897616 + 0a70a8a commit 70b57a0

File tree

7 files changed

+186
-82
lines changed

7 files changed

+186
-82
lines changed

CHANGES.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,19 @@
5353
A new keyword argument `max_depth` defines the maximum subdirectory depths
5454
used to search for datasets in case `roots` is given. It defaults to `1`.
5555

56+
* The behaviour of function `resample_in_space()` of module
57+
`xcube.core.resampling` changed in this version. (#1001)
58+
1. If the resampling boils down to a plain affine transformation,
59+
the returned target dataset will now have the _same_ spatial coordinates
60+
as the provided target grid mapping passed by `target_gm`.
61+
2. In the case of up-sampling, we no longer recover `NaN` values by default
62+
as it may require considerable CPU overhead.
63+
To enforce the old behaviour, provide the `var_configs` keyword-argument
64+
and set `recover_nan` to `True` for desired variables.
65+
5666
* The class `MaskSet()` of module `xcube.core.maskset` now correctly recognises
5767
the variable attributes `flag_values`, `flag_masks`, `flag_meanings` when
58-
their values are lists (ESA CCI LC data encodes them as JSON arrays).
68+
their values are lists (ESA CCI LC data encodes them as JSON arrays). (#1002)
5969

6070

6171
### Incompatible API changes

test/core/resampling/test_affine.py

+16-12
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class AffineTransformDatasetTest(unittest.TestCase):
4646
def test_subset(self):
4747
target_gm = GridMapping.regular((3, 3), (50.0, 10.0), res, source_gm.crs)
4848
target_ds = affine_transform_dataset(
49-
source_ds, source_gm, target_gm, gm_name="crs"
49+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
5050
)
5151
self.assertIsInstance(target_ds, xr.Dataset)
5252
self.assertEqual(
@@ -66,7 +66,7 @@ def test_subset(self):
6666

6767
target_gm = GridMapping.regular((3, 3), (50.1, 10.1), res, source_gm.crs)
6868
target_ds = affine_transform_dataset(
69-
source_ds, source_gm, target_gm, gm_name="crs"
69+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
7070
)
7171
self.assertIsInstance(target_ds, xr.Dataset)
7272
self.assertEqual(
@@ -86,7 +86,7 @@ def test_subset(self):
8686

8787
target_gm = GridMapping.regular((3, 3), (50.05, 10.05), res, source_gm.crs)
8888
target_ds = affine_transform_dataset(
89-
source_ds, source_gm, target_gm, gm_name="crs"
89+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
9090
)
9191
self.assertIsInstance(target_ds, xr.Dataset)
9292
self.assertEqual(
@@ -102,13 +102,15 @@ def test_different_geographic_crses(self):
102102
expected = np.array([[1.25, 1.5, 0.75], [1.0, 1.25, 1.5], [1.75, 1.0, 1.25]])
103103

104104
target_gm = GridMapping.regular((3, 3), (50.05, 10.05), res, CRS_WGS84)
105-
target_ds = affine_transform_dataset(source_ds, source_gm, target_gm)
105+
target_ds = affine_transform_dataset(
106+
source_ds, source_gm=source_gm, target_gm=target_gm
107+
)
106108
self.assertEqual((3, 3), target_ds.refl.shape)
107109
np.testing.assert_almost_equal(target_ds.refl.values, expected)
108110

109111
target_gm = GridMapping.regular((3, 3), (50.05, 10.05), res, CRS_CRS84)
110112
target_ds = affine_transform_dataset(
111-
source_ds, source_gm, target_gm, gm_name="crs"
113+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
112114
)
113115
self.assertEqual((3, 3), target_ds.refl.shape)
114116
np.testing.assert_almost_equal(target_ds.refl.values, expected)
@@ -122,12 +124,14 @@ def test_different_geographic_crses(self):
122124
' must be equal, was "WGS 84" and'
123125
' "ETRS89-extended / LAEA Europe"',
124126
):
125-
affine_transform_dataset(source_ds, source_gm, target_gm)
127+
affine_transform_dataset(
128+
source_ds, source_gm=source_gm, target_gm=target_gm
129+
)
126130

127131
def test_downscale_x2(self):
128132
target_gm = GridMapping.regular((8, 6), (50, 10), 2 * res, source_gm.crs)
129133
target_ds = affine_transform_dataset(
130-
source_ds, source_gm, target_gm, gm_name="crs"
134+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
131135
)
132136
self.assertIsInstance(target_ds, xr.Dataset)
133137
self.assertEqual(
@@ -152,7 +156,7 @@ def test_downscale_x2(self):
152156
def test_downscale_x2_and_shift(self):
153157
target_gm = GridMapping.regular((8, 6), (49.8, 9.8), 2 * res, source_gm.crs)
154158
target_ds = affine_transform_dataset(
155-
source_ds, source_gm, target_gm, gm_name="crs"
159+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
156160
)
157161
self.assertIsInstance(target_ds, xr.Dataset)
158162
self.assertEqual(
@@ -177,7 +181,7 @@ def test_downscale_x2_and_shift(self):
177181
def test_upscale_x2(self):
178182
target_gm = GridMapping.regular((8, 6), (50, 10), res / 2, source_gm.crs)
179183
target_ds = affine_transform_dataset(
180-
source_ds, source_gm, target_gm, gm_name="crs"
184+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
181185
)
182186
self.assertIsInstance(target_ds, xr.Dataset)
183187
self.assertEqual(
@@ -202,7 +206,7 @@ def test_upscale_x2(self):
202206
def test_upscale_x2_and_shift(self):
203207
target_gm = GridMapping.regular((8, 6), (49.9, 9.95), res / 2, source_gm.crs)
204208
target_ds = affine_transform_dataset(
205-
source_ds, source_gm, target_gm, gm_name="crs"
209+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
206210
)
207211
self.assertIsInstance(target_ds, xr.Dataset)
208212
self.assertEqual(
@@ -227,7 +231,7 @@ def test_upscale_x2_and_shift(self):
227231
def test_shift(self):
228232
target_gm = GridMapping.regular((8, 6), (50.2, 10.1), res, source_gm.crs)
229233
target_ds = affine_transform_dataset(
230-
source_ds, source_gm, target_gm, gm_name="crs"
234+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
231235
)
232236
self.assertIsInstance(target_ds, xr.Dataset)
233237
self.assertEqual(
@@ -251,7 +255,7 @@ def test_shift(self):
251255

252256
target_gm = GridMapping.regular((8, 6), (49.8, 9.9), res, source_gm.crs)
253257
target_ds = affine_transform_dataset(
254-
source_ds, source_gm, target_gm, gm_name="crs"
258+
source_ds, source_gm=source_gm, target_gm=target_gm, gm_name="crs"
255259
)
256260
self.assertIsInstance(target_ds, xr.Dataset)
257261
self.assertEqual(

test/core/resampling/test_spatial.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ def test_affine_transform_dataset(self):
2525
)
2626

2727
target_ds = resample_in_space(
28-
source_ds, source_gm, target_gm, encode_cf=True, gm_name="crs"
28+
source_ds,
29+
source_gm=source_gm,
30+
target_gm=target_gm,
31+
encode_cf=True,
32+
gm_name="crs",
2933
)
3034

3135
self.assertIn("crs", target_ds)

xcube/core/resampling/affine.py

+51-20
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# https://opensource.org/licenses/MIT.
44

55
import math
6-
from typing import Union, Callable, Optional, Tuple, Any
6+
from typing import Union, Callable, Optional, Any
77
from collections.abc import Sequence, Mapping, Hashable
88

99
import numpy as np
@@ -14,17 +14,19 @@
1414
from xcube.core.gridmapping import GridMapping
1515
from xcube.core.gridmapping.helpers import AffineTransformMatrix
1616
from xcube.util.assertions import assert_true
17-
from .cf import maybe_encode_grid_mapping
17+
from .cf import complete_resampled_dataset
1818

1919
NDImage = Union[np.ndarray, da.Array]
2020
Aggregator = Callable[[NDImage], NDImage]
2121

2222

2323
def affine_transform_dataset(
24-
dataset: xr.Dataset,
25-
source_gm: GridMapping,
26-
target_gm: GridMapping,
27-
var_configs: Mapping[Hashable, Mapping[str, Any]] = None,
24+
source_ds: xr.Dataset,
25+
*,
26+
target_ds: Optional[xr.Dataset] = None,
27+
source_gm: Optional[GridMapping] = None,
28+
target_gm: Optional[GridMapping] = None,
29+
var_configs: Optional[Mapping[Hashable, Mapping[str, Any]]] = None,
2830
encode_cf: bool = True,
2931
gm_name: Optional[str] = None,
3032
reuse_coords: bool = False,
@@ -35,12 +37,22 @@ def affine_transform_dataset(
3537
*source_gm* and the CRS of *target_gm* are both geographic or equal.
3638
Otherwise, a ``ValueError`` will be raised.
3739
40+
New in 1.6: If *target_ds* is given, its coordinate
41+
variables are copied by reference into the returned
42+
dataset.
43+
3844
Args:
39-
dataset: The source dataset
40-
source_gm: Source grid mapping of *dataset*. Must be regular.
41-
Must have same CRS as *target_gm*.
42-
target_gm: Target grid mapping. Must be regular. Must have same
43-
CRS as *source_gm*.
45+
source_ds: The source dataset
46+
target_ds: An optional dataset that provides the
47+
target grid mapping, if *target_gm* is not provided.
48+
The coordinate variables of *target_dataset* are copied
49+
by reference into the returned dataset.
50+
source_gm: Optional source grid mapping of *dataset*.
51+
If not provided, computed from *source_ds*.
52+
Must be regular and must have same CRS as *target_gm*.
53+
target_gm: Optional target grid mapping. If not provided,
54+
computed from *target_ds* or source grid mapping.
55+
Must be regular and must have same CRS as source grid mapping.
4456
var_configs: Optional resampling configurations for individual
4557
variables.
4658
encode_cf: Whether to encode the target grid mapping into the
@@ -54,6 +66,18 @@ def affine_transform_dataset(
5466
Returns:
5567
The resampled target dataset.
5668
"""
69+
if source_gm is None:
70+
# No source grid mapping given, so do derive it from dataset
71+
source_gm = GridMapping.from_dataset(source_ds)
72+
73+
if target_gm is None:
74+
# No target grid mapping given, so do derive it
75+
# from target dataset or source grid mapping
76+
if target_ds is not None:
77+
target_gm = GridMapping.from_dataset(target_ds)
78+
else:
79+
target_gm = source_gm.to_regular()
80+
5781
# Are source and target both geographic grid mappings?
5882
both_geographic = source_gm.crs.is_geographic and target_gm.crs.is_geographic
5983
if not (both_geographic or source_gm.crs == target_gm.crs):
@@ -65,24 +89,28 @@ def affine_transform_dataset(
6589
GridMapping.assert_regular(source_gm, name="source_gm")
6690
GridMapping.assert_regular(target_gm, name="target_gm")
6791
resampled_dataset = resample_dataset(
68-
dataset=dataset,
92+
dataset=source_ds,
6993
matrix=target_gm.ij_transform_to(source_gm),
7094
size=target_gm.size,
7195
tile_size=target_gm.tile_size,
7296
xy_dim_names=source_gm.xy_dim_names,
7397
var_configs=var_configs,
7498
)
7599
has_bounds = any(
76-
dataset[var_name].attrs.get("bounds") for var_name in source_gm.xy_var_names
100+
source_ds[var_name].attrs.get("bounds") for var_name in source_gm.xy_var_names
77101
)
78102
new_coords = target_gm.to_coords(
79103
xy_var_names=source_gm.xy_var_names,
80104
xy_dim_names=source_gm.xy_dim_names,
81105
exclude_bounds=not has_bounds,
82106
reuse_coords=reuse_coords,
83107
)
84-
return maybe_encode_grid_mapping(
85-
encode_cf, resampled_dataset.assign_coords(new_coords), target_gm, gm_name
108+
return complete_resampled_dataset(
109+
encode_cf,
110+
resampled_dataset.assign_coords(new_coords),
111+
target_gm,
112+
gm_name,
113+
target_ds.coords if target_ds else None,
86114
)
87115

88116

@@ -127,7 +155,8 @@ def resample_dataset(
127155
else:
128156
spline_order = 1
129157
aggregator = np.nanmean
130-
recover_nan = True
158+
# forman: changed default from True to False (v1.6, 2024-06-05)
159+
recover_nan = False
131160
var_data = resample_ndimage(
132161
var.data,
133162
scale=(j_scale, i_scale),
@@ -260,7 +289,7 @@ def _transform_array(
260289
# Yes, then
261290
# 1. replace NaN by zero
262291
filled_im = da.where(mask, 0.0, image)
263-
# 2. transform the zeo-filled image
292+
# 2. transform the zero-filled image
264293
scaled_im = ndinterp.affine_transform(
265294
filled_im, matrix, **at_kwargs, cval=0.0
266295
)
@@ -306,17 +335,19 @@ def _normalize_image(im: NDImage) -> da.Array:
306335
return da.asarray(im)
307336

308337

309-
def _normalize_offset(offset: Optional[Sequence[float]], ndim: int) -> tuple[int, ...]:
338+
def _normalize_offset(
339+
offset: Optional[Sequence[float]], ndim: int
340+
) -> tuple[float, ...]:
310341
return _normalize_pair(offset, 0.0, ndim, "offset")
311342

312343

313-
def _normalize_scale(scale: Optional[Sequence[float]], ndim: int) -> tuple[int, ...]:
344+
def _normalize_scale(scale: Optional[Sequence[float]], ndim: int) -> tuple[float, ...]:
314345
return _normalize_pair(scale, 1.0, ndim, "scale")
315346

316347

317348
def _normalize_pair(
318349
pair: Optional[Sequence[float]], default: float, ndim: int, name: str
319-
) -> tuple[int, ...]:
350+
) -> tuple[float, ...]:
320351
if pair is None:
321352
pair = [default, default]
322353
elif isinstance(pair, (int, float)):

xcube/core/resampling/cf.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright (c) 2018-2024 by xcube team and contributors
22
# Permissions are hereby granted under the terms of the MIT License:
33
# https://opensource.org/licenses/MIT.
4-
4+
from collections.abc import Hashable, Mapping
55
from typing import Optional
66

77
import xarray as xr
@@ -82,12 +82,29 @@ def encode_grid_mapping(
8282
return ds_copy
8383

8484

85-
def maybe_encode_grid_mapping(
86-
encode_cf: bool, ds: xr.Dataset, gm: GridMapping, gm_name: Optional[str]
85+
def complete_resampled_dataset(
86+
encode_cf: bool,
87+
ds: xr.Dataset,
88+
gm: GridMapping,
89+
gm_name: Optional[str],
90+
coords: Optional[Mapping[Hashable, xr.DataArray]],
8791
) -> xr.Dataset:
8892
"""Internal helper."""
8993
if encode_cf:
90-
return encode_grid_mapping(
94+
ds = encode_grid_mapping(
9195
ds, gm, gm_name=gm_name, force=True if gm_name else None
9296
)
97+
if coords:
98+
compatible_coords = {
99+
k: v for k, v in coords.items() if is_var_compatible(v, ds)
100+
}
101+
ds = ds.assign_coords(compatible_coords)
93102
return ds
103+
104+
105+
def is_var_compatible(var: xr.DataArray, ds: xr.Dataset):
106+
"""Internal helper."""
107+
for d in var.dims:
108+
if d in ds.sizes and var.sizes[d] != ds.sizes[d]:
109+
return False
110+
return True

0 commit comments

Comments
 (0)