Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
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` and `projection` parameter. ([#897](https://github.com/Open-EO/openeo-python-client/issues/897))
Comment thread
VincentVerelst marked this conversation as resolved.
Outdated

## [0.49.0] - 2026-04-01

Expand Down
18 changes: 12 additions & 6 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,14 @@ def drop_dimension(self, name: str = None) -> CubeMetadata:

def resample_spatial(
self,
resolution: Union[float, Tuple[float, float], List[float]] = 0.0,
projection: Union[int, str, None] = None,
resolution: Union[float, Tuple[float, float], List[float], Parameter] = 0.0,
projection: Union[int, str, Parameter, None] = None,
) -> CubeMetadata:
resolution = normalize_resample_resolution(resolution)
if isinstance(resolution, Parameter):
normalized_resolution = None, None
else:
normalized_resolution = normalize_resample_resolution(resolution)

if self._dimensions is None:
# Best-effort fallback to work with
dimensions = [
Expand All @@ -512,13 +517,14 @@ 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):

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,
crs=None if isinstance(projection, Parameter) else projection or dim.crs,
step=r if r != 0.0 else dim.step,
)

Expand Down
4 changes: 2 additions & 2 deletions openeo/rest/datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,8 +809,8 @@ def band(self, band: Union[str, int]) -> DataCube:
@openeo_process
def resample_spatial(
self,
resolution: Union[float, Tuple[float, float], List[float]] = 0.0,
projection: Union[int, str, None] = None,
resolution: Union[float, Tuple[float, float], List[float], Parameter] = 0.0,
projection: Union[int, str, Parameter, None] = None,
method: str = "near",
align: str = "upper-left",
) -> DataCube:
Expand Down
85 changes: 85 additions & 0 deletions tests/rest/datacube/test_datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,91 @@ 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_spatial_parameter_projection(s2cube):
"""A Parameter object passed as projection must appear in the process graph; metadata crs should be None."""
proj_param = Parameter.integer("proj", description="The target projection.")
cube = s2cube.resample_spatial(resolution=10, projection=proj_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": 10,
"projection": {"from_parameter": "proj"},
"method": "near",
"align": "upper-left",
},
}
}
assert cube.metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=None, crs=None, step=10),
SpatialDimension(name="y", extent=None, crs=None, step=10),
]


def test_resample_spatial_parameter_resolution_and_projection(s2cube):
"""When both resolution and projection are Parameters, step and crs should both be None."""
res_param = Parameter.number("res", description="The spatial resolution.")
proj_param = Parameter.integer("proj", description="The target projection.")
cube = s2cube.resample_spatial(resolution=res_param, projection=proj_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": {"from_parameter": "proj"},
"method": "near",
"align": "upper-left",
},
}
}
assert cube.metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=None, crs=None, step=None),
SpatialDimension(name="y", extent=None, crs=None, step=None),
]


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
59 changes: 59 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,64 @@ 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 be set to None after parametrized resample_spatial.
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=4326, step=None),
SpatialDimension(name="y", extent=[49, 52], crs=4326, step=None),
]
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=None),
SpatialDimension(name="y", extent=[49, 52], crs=32631, step=None),
]
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_projection_only(cube_metadata):
"""When projection is a Parameter, crs should be set to None."""
param = Parameter.integer("proj", description="The target projection.")
metadata = cube_metadata.resample_spatial(resolution=10, projection=param)
assert isinstance(metadata, CubeMetadata)
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=None, step=10),
SpatialDimension(name="y", extent=[49, 52], crs=None, step=10),
]
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_and_projection(cube_metadata):
"""When both resolution and projection are Parameters, step and crs should both be None."""
res_param = Parameter.number("res", description="The spatial resolution.")
proj_param = Parameter.integer("proj", description="The target projection.")
metadata = cube_metadata.resample_spatial(resolution=res_param, projection=proj_param)
assert isinstance(metadata, CubeMetadata)
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=None, step=None),
SpatialDimension(name="y", extent=[49, 52], crs=None, step=None),
]
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