Skip to content

Commit b4c3497

Browse files
Support openeo.api.process.Parameter as resolution in DataCube.resample_spatial() #897
1 parent f97f0e7 commit b4c3497

5 files changed

Lines changed: 97 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
### Removed
2121

2222
### Fixed
23-
23+
- `DataCube.resample_spatial()` now supports `openeo.api.process.Parameter` objects for the `resolution` parameter. ([#897](https://github.com/Open-EO/openeo-python-client/issues/897))
2424

2525
## [0.49.0] - 2026-04-01
2626

openeo/metadata.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import pystac.extensions.eo
2020
import pystac.extensions.item_assets
2121

22+
from openeo.api.process import Parameter
2223
from openeo.internal.jupyter import render_component
2324
from openeo.util import Rfc3339, deep_get
2425
from openeo.utils.normalize import normalize_resample_resolution, unique
@@ -494,10 +495,13 @@ def drop_dimension(self, name: str = None) -> CubeMetadata:
494495

495496
def resample_spatial(
496497
self,
497-
resolution: Union[float, Tuple[float, float], List[float]] = 0.0,
498+
resolution: Union[float, Tuple[float, float], List[float], "Parameter"] = 0.0,
498499
projection: Union[int, str, None] = None,
499500
) -> CubeMetadata:
500-
resolution = normalize_resample_resolution(resolution)
501+
if isinstance(resolution, Parameter):
502+
normalized_resolution = None
503+
else:
504+
normalized_resolution = normalize_resample_resolution(resolution)
501505
if self._dimensions is None:
502506
# Best-effort fallback to work with
503507
dimensions = [
@@ -512,15 +516,25 @@ def resample_spatial(
512516
spatial_indices = [i for i, d in enumerate(dimensions) if isinstance(d, SpatialDimension)]
513517
if len(spatial_indices) != 2:
514518
raise MetadataException(f"Expected two spatial dimensions but found {spatial_indices=}")
515-
assert len(resolution) == 2
516-
for i, r in zip(spatial_indices, resolution):
517-
dim: SpatialDimension = dimensions[i]
518-
dimensions[i] = SpatialDimension(
519-
name=dim.name,
520-
extent=dim.extent,
521-
crs=projection or dim.crs,
522-
step=r if r != 0.0 else dim.step,
523-
)
519+
if normalized_resolution is not None:
520+
assert len(normalized_resolution) == 2
521+
for i, r in zip(spatial_indices, normalized_resolution):
522+
dim: SpatialDimension = dimensions[i]
523+
dimensions[i] = SpatialDimension(
524+
name=dim.name,
525+
extent=dim.extent,
526+
crs=projection or dim.crs,
527+
step=r if r != 0.0 else dim.step,
528+
)
529+
elif projection:
530+
for i in spatial_indices:
531+
dim: SpatialDimension = dimensions[i]
532+
dimensions[i] = SpatialDimension(
533+
name=dim.name,
534+
extent=dim.extent,
535+
crs=projection,
536+
step=dim.step,
537+
)
524538

525539
return self._clone_and_update(dimensions=dimensions)
526540

openeo/rest/datacube.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,7 @@ def band(self, band: Union[str, int]) -> DataCube:
809809
@openeo_process
810810
def resample_spatial(
811811
self,
812-
resolution: Union[float, Tuple[float, float], List[float]] = 0.0,
812+
resolution: Union[float, Tuple[float, float], List[float], Parameter] = 0.0,
813813
projection: Union[int, str, None] = None,
814814
method: str = "near",
815815
align: str = "upper-left",

tests/rest/datacube/test_datacube.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,46 @@ def test_resample_spatial_no_metadata(s2cube_without_metadata):
873873
]
874874

875875

876+
def test_resample_spatial_parameter_resolution(s2cube):
877+
"""A Parameter object passed as resolution must not crash and must appear in the process graph."""
878+
param = Parameter.number("res", description="The spatial resolution.")
879+
cube = s2cube.resample_spatial(resolution=param, projection=32631)
880+
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
881+
"resamplespatial1": {
882+
"process_id": "resample_spatial",
883+
"arguments": {
884+
"data": {"from_node": "loadcollection1"},
885+
"resolution": {"from_parameter": "res"},
886+
"projection": 32631,
887+
"method": "near",
888+
"align": "upper-left",
889+
},
890+
}
891+
}
892+
assert cube.metadata.spatial_dimensions == [
893+
SpatialDimension(name="x", extent=None, crs=32631, step=None),
894+
SpatialDimension(name="y", extent=None, crs=32631, step=None),
895+
]
896+
897+
898+
def test_resample_spatial_parameter_resolution_no_projection(s2cube):
899+
"""A Parameter resolution with no concrete projection leaves step and crs unchanged."""
900+
param = Parameter.number("res", description="The spatial resolution.")
901+
cube = s2cube.resample_spatial(resolution=param)
902+
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
903+
"resamplespatial1": {
904+
"process_id": "resample_spatial",
905+
"arguments": {
906+
"data": {"from_node": "loadcollection1"},
907+
"resolution": {"from_parameter": "res"},
908+
"projection": None,
909+
"method": "near",
910+
"align": "upper-left",
911+
},
912+
}
913+
}
914+
915+
876916
def test_resample_cube_spatial(s2cube):
877917
cube1 = s2cube.resample_spatial(resolution=[2.0, 3.0], projection=4578)
878918
cube2 = s2cube.resample_spatial(resolution=10, projection=32631)

tests/test_metadata.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pystac
99
import pytest
1010

11+
from openeo.api.process import Parameter
1112
from openeo.metadata import (
1213
_PYSTAC_1_9_EXTENSION_INTERFACE,
1314
Band,
@@ -1279,6 +1280,35 @@ def test_metadata_resample_spatial(cube_metadata, kwargs, expected_x, expected_y
12791280
assert metadata.band_dimension == cube_metadata.band_dimension
12801281

12811282

1283+
@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
1284+
def test_metadata_resample_spatial_parameter_resolution_only(cube_metadata):
1285+
"""When resolution is a Parameter, step should remain unchanged and crs should stay as-is."""
1286+
param = Parameter.number("res", description="The spatial resolution.")
1287+
metadata = cube_metadata.resample_spatial(resolution=param)
1288+
assert isinstance(metadata, CubeMetadata)
1289+
# step must not change because the resolution is unknown at build time
1290+
assert metadata.spatial_dimensions == [
1291+
SpatialDimension(name="x", extent=[2, 7], crs=4326, step=0.1),
1292+
SpatialDimension(name="y", extent=[49, 52], crs=4326, step=0.1),
1293+
]
1294+
assert metadata.temporal_dimension == cube_metadata.temporal_dimension
1295+
assert metadata.band_dimension == cube_metadata.band_dimension
1296+
1297+
1298+
@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
1299+
def test_metadata_resample_spatial_parameter_resolution_with_projection(cube_metadata):
1300+
"""When resolution is a Parameter but projection is concrete, crs should be updated."""
1301+
param = Parameter.number("res", description="The spatial resolution.")
1302+
metadata = cube_metadata.resample_spatial(resolution=param, projection=32631)
1303+
assert isinstance(metadata, CubeMetadata)
1304+
assert metadata.spatial_dimensions == [
1305+
SpatialDimension(name="x", extent=[2, 7], crs=32631, step=0.1),
1306+
SpatialDimension(name="y", extent=[49, 52], crs=32631, step=0.1),
1307+
]
1308+
assert metadata.temporal_dimension == cube_metadata.temporal_dimension
1309+
assert metadata.band_dimension == cube_metadata.band_dimension
1310+
1311+
12821312
@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
12831313
def test_metadata_resample_cube_spatial(cube_metadata):
12841314
metadata1 = cube_metadata.resample_spatial(resolution=(11, 22), projection=32631)

0 commit comments

Comments
 (0)