Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed

### Fixed

- `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))

## [0.49.0] - 2026-04-01

Expand Down
36 changes: 25 additions & 11 deletions openeo/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pystac.extensions.eo
import pystac.extensions.item_assets

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

def resample_spatial(
self,
resolution: Union[float, Tuple[float, float], List[float]] = 0.0,
resolution: Union[float, Tuple[float, float], List[float], "Parameter"] = 0.0,
projection: Union[int, str, None] = None,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also allow projection to be a parameter here, while we're at it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, with the same logic that the metadata crs should become None, if projection is a Parameter

) -> CubeMetadata:
resolution = normalize_resample_resolution(resolution)
if isinstance(resolution, Parameter):
normalized_resolution = None
Comment thread
VincentVerelst marked this conversation as resolved.
Outdated
else:
normalized_resolution = normalize_resample_resolution(resolution)
if self._dimensions is None:
# Best-effort fallback to work with
dimensions = [
Expand All @@ -512,15 +516,25 @@ def resample_spatial(
spatial_indices = [i for i, d in enumerate(dimensions) if isinstance(d, SpatialDimension)]
if len(spatial_indices) != 2:
raise MetadataException(f"Expected two spatial dimensions but found {spatial_indices=}")
assert len(resolution) == 2
for i, r in zip(spatial_indices, resolution):
dim: SpatialDimension = dimensions[i]
dimensions[i] = SpatialDimension(
name=dim.name,
extent=dim.extent,
crs=projection or dim.crs,
step=r if r != 0.0 else dim.step,
)
if normalized_resolution is not None:
assert len(normalized_resolution) == 2
for i, r in zip(spatial_indices, normalized_resolution):
dim: SpatialDimension = dimensions[i]
dimensions[i] = SpatialDimension(
name=dim.name,
extent=dim.extent,
crs=projection or dim.crs,
step=r if r != 0.0 else dim.step,
)
elif projection:
for i in spatial_indices:
dim: SpatialDimension = dimensions[i]
dimensions[i] = SpatialDimension(
name=dim.name,
extent=dim.extent,
crs=projection,
step=dim.step,
Comment thread
VincentVerelst marked this conversation as resolved.
Outdated
)

return self._clone_and_update(dimensions=dimensions)

Expand Down
2 changes: 1 addition & 1 deletion openeo/rest/datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ def band(self, band: Union[str, int]) -> DataCube:
@openeo_process
def resample_spatial(
self,
resolution: Union[float, Tuple[float, float], List[float]] = 0.0,
resolution: Union[float, Tuple[float, float], List[float], Parameter] = 0.0,
projection: Union[int, str, None] = None,
method: str = "near",
align: str = "upper-left",
Expand Down
40 changes: 40 additions & 0 deletions tests/rest/datacube/test_datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,46 @@ def test_resample_spatial_no_metadata(s2cube_without_metadata):
]


def test_resample_spatial_parameter_resolution(s2cube):
"""A Parameter object passed as resolution must not crash and must appear in the process graph."""
param = Parameter.number("res", description="The spatial resolution.")
cube = s2cube.resample_spatial(resolution=param, projection=32631)
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
"resamplespatial1": {
"process_id": "resample_spatial",
"arguments": {
"data": {"from_node": "loadcollection1"},
"resolution": {"from_parameter": "res"},
"projection": 32631,
"method": "near",
"align": "upper-left",
},
}
}
assert cube.metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=None, crs=32631, step=None),
SpatialDimension(name="y", extent=None, crs=32631, step=None),
]


def test_resample_spatial_parameter_resolution_no_projection(s2cube):
"""A Parameter resolution with no concrete projection leaves step and crs unchanged."""
param = Parameter.number("res", description="The spatial resolution.")
cube = s2cube.resample_spatial(resolution=param)
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
"resamplespatial1": {
"process_id": "resample_spatial",
"arguments": {
"data": {"from_node": "loadcollection1"},
"resolution": {"from_parameter": "res"},
"projection": None,
"method": "near",
"align": "upper-left",
},
}
}


def test_resample_cube_spatial(s2cube):
cube1 = s2cube.resample_spatial(resolution=[2.0, 3.0], projection=4578)
cube2 = s2cube.resample_spatial(resolution=10, projection=32631)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pystac
import pytest

from openeo.api.process import Parameter
from openeo.metadata import (
_PYSTAC_1_9_EXTENSION_INTERFACE,
Band,
Expand Down Expand Up @@ -1279,6 +1280,35 @@ def test_metadata_resample_spatial(cube_metadata, kwargs, expected_x, expected_y
assert metadata.band_dimension == cube_metadata.band_dimension


@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
def test_metadata_resample_spatial_parameter_resolution_only(cube_metadata):
"""When resolution is a Parameter, step should remain unchanged and crs should stay as-is."""
param = Parameter.number("res", description="The spatial resolution.")
metadata = cube_metadata.resample_spatial(resolution=param)
assert isinstance(metadata, CubeMetadata)
# step must not change because the resolution is unknown at build time
Comment thread
VincentVerelst marked this conversation as resolved.
Outdated
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=4326, step=0.1),
SpatialDimension(name="y", extent=[49, 52], crs=4326, step=0.1),
]
assert metadata.temporal_dimension == cube_metadata.temporal_dimension
assert metadata.band_dimension == cube_metadata.band_dimension


@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
def test_metadata_resample_spatial_parameter_resolution_with_projection(cube_metadata):
"""When resolution is a Parameter but projection is concrete, crs should be updated."""
param = Parameter.number("res", description="The spatial resolution.")
metadata = cube_metadata.resample_spatial(resolution=param, projection=32631)
assert isinstance(metadata, CubeMetadata)
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=32631, step=0.1),
SpatialDimension(name="y", extent=[49, 52], crs=32631, step=0.1),
]
assert metadata.temporal_dimension == cube_metadata.temporal_dimension
assert metadata.band_dimension == cube_metadata.band_dimension


@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
def test_metadata_resample_cube_spatial(cube_metadata):
metadata1 = cube_metadata.resample_spatial(resolution=(11, 22), projection=32631)
Expand Down
Loading