Skip to content

Commit

Permalink
Issue #690 track resample_spatial in cube metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
soxofaan committed Jan 23, 2025
1 parent d4f976c commit 564eed8
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 18 deletions.
42 changes: 39 additions & 3 deletions openeo/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from openeo.internal.jupyter import render_component
from openeo.util import Rfc3339, deep_get
from openeo.utils.normalize import normalize_resample_resolution

_log = logging.getLogger(__name__)

Expand All @@ -25,6 +26,8 @@ class DimensionAlreadyExistsException(MetadataException):


# TODO: make these dimension classes immutable data classes
# TODO: align better with STAC datacube extension
# TODO: align/adapt/integrate with pystac's datacube extension implementation?
class Dimension:
"""Base class for dimensions."""

Expand Down Expand Up @@ -58,6 +61,8 @@ def rename_labels(self, target, source) -> Dimension:


class SpatialDimension(Dimension):
# TODO: align better with STAC datacube extension: e.g. support "axis" (x or y)

DEFAULT_CRS = 4326

def __init__(
Expand Down Expand Up @@ -257,6 +262,10 @@ def __init__(self, dimensions: Optional[List[Dimension]] = None):
def __eq__(self, o: Any) -> bool:
return isinstance(o, type(self)) and self._dimensions == o._dimensions

def __str__(self) -> str:
bands = self.band_names if self.has_band_dimension() else "no bands dimension"
return f"CubeMetadata({bands} - {self.dimension_names()})"

def _clone_and_update(self, dimensions: Optional[List[Dimension]] = None, **kwargs) -> CubeMetadata:
"""Create a new instance (of same class) with copied/updated fields."""
cls = type(self)
Expand Down Expand Up @@ -411,9 +420,36 @@ def drop_dimension(self, name: str = None) -> CubeMetadata:
raise ValueError("No dimension named {n!r} (valid names: {ns!r})".format(n=name, ns=dimension_names))
return self._clone_and_update(dimensions=[d for d in self._dimensions if not d.name == name])

def __str__(self) -> str:
bands = self.band_names if self.has_band_dimension() else "no bands dimension"
return f"CubeMetadata({bands} - {self.dimension_names()})"
def resample_spatial(
self,
resolution: Union[int, float, Tuple[float, float], Tuple[int, int]] = 0.0,
projection: Union[int, str, None] = None,
):
resolution = normalize_resample_resolution(resolution)
if self._dimensions is None:
# Best-effort fallback to work with
dimensions = [
SpatialDimension(name="x", extent=[None, None]),
SpatialDimension(name="y", extent=[None, None]),
]
else:
# Make sure to work with a copy (to edit in-place)
dimensions = list(self._dimensions)

# Find and replace spatial dimensions
spatial_indixes = [i for i, d in enumerate(dimensions) if isinstance(d, SpatialDimension)]
if len(spatial_indixes) != 2:
raise MetadataException(f"Expected two spatial resolutions but found {spatial_indixes=}")
for i in spatial_indixes:
dim: SpatialDimension = dimensions[i]
dimensions[i] = SpatialDimension(
name=dim.name,
extent=dim.extent,
crs=projection or dim.crs,
step=resolution[i] if resolution[i] else dim.step,
)

return self._clone_and_update(dimensions=dimensions)


class CollectionMetadata(CubeMetadata):
Expand Down
28 changes: 19 additions & 9 deletions openeo/rest/datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,16 +749,26 @@ def band(self, band: Union[str, int]) -> DataCube:

@openeo_process
def resample_spatial(
self, resolution: Union[float, Tuple[float, float]], projection: Union[int, str] = None,
method: str = 'near', align: str = 'upper-left'
self,
resolution: Union[int, float, Tuple[float, float], Tuple[int, int]] = 0.0,
projection: Union[int, str, None] = None,
method: str = "near",
align: str = "upper-left",
) -> DataCube:
return self.process('resample_spatial', {
'data': THIS,
'resolution': resolution,
'projection': projection,
'method': method,
'align': align
})
metadata = (
self.metadata.resample_spatial(resolution=resolution, projection=projection) if self.metadata else None
)
return self.process(
process_id="resample_spatial",
arguments={
"data": THIS,
"resolution": resolution,
"projection": projection,
"method": method,
"align": align,
},
metadata=metadata,
)

def resample_cube_spatial(self, target: DataCube, method: str = "near") -> DataCube:
"""
Expand Down
Empty file added openeo/utils/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions openeo/utils/normalize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Tuple, Union


def normalize_resample_resolution(
resolution: Union[int, float, Tuple[float, float], Tuple[int, int]]
) -> Tuple[Union[int, float], Union[int, float]]:
"""Normalize a resolution value, as used in the `resample_spatial` process to a two-element tuple."""
if isinstance(resolution, (int, float)):
return (resolution, resolution)
elif (
isinstance(resolution, (list, tuple))
and len(resolution) == 2
and all(isinstance(r, (int, float)) for r in resolution)
):
return tuple(resolution)
raise ValueError(f"Invalid resolution {resolution!r}")
25 changes: 19 additions & 6 deletions tests/rest/datacube/test_datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from openeo import collection_property
from openeo.api.process import Parameter
from openeo.metadata import SpatialDimension
from openeo.rest import BandMathException, OpenEoClientException
from openeo.rest._testing import build_capabilities
from openeo.rest.connection import Connection
Expand Down Expand Up @@ -730,12 +731,24 @@ def test_apply_kernel(s2cube):


def test_resample_spatial(s2cube):
im = s2cube.resample_spatial(resolution=[2.0, 3.0], projection=4578)
graph = _get_leaf_node(im)
assert graph["process_id"] == "resample_spatial"
assert "data" in graph["arguments"]
assert graph["arguments"]["resolution"] == [2.0, 3.0]
assert graph["arguments"]["projection"] == 4578
cube = s2cube.resample_spatial(resolution=[2.0, 3.0], projection=4578)
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
"resamplespatial1": {
"process_id": "resample_spatial",
"arguments": {
"data": {"from_node": "loadcollection1"},
"resolution": [2.0, 3.0],
"projection": 4578,
"method": "near",
"align": "upper-left",
},
}
}

assert cube.metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=None, crs=4578, step=2.0),
SpatialDimension(name="y", extent=None, crs=4578, step=3.0),
]


def test_merge(s2cube, api_version, test_data):
Expand Down
35 changes: 35 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@
from openeo.testing.stac import StacDummyBuilder


@pytest.fixture
def xytb_cube_metadata() -> CubeMetadata:
"""Generic 4D cube (x, y, temporal, band) metadata"""
return CubeMetadata(
dimensions=[
SpatialDimension(name="x", extent=[2, 7], crs=4326, step=0.1),
SpatialDimension(name="y", extent=[49, 52], crs=4326, step=0.1),
TemporalDimension(name="t", extent=["2024-09-01", "2024-12-01"]),
BandDimension(name="bands", bands=[Band("B2"), Band("B3")]),
]
)


def test_metadata_get():
metadata = CollectionMetadata({"foo": "bar", "very": {"deeply": {"nested": {"path": {"to": "somewhere"}}}}})
assert metadata.get("foo") == "bar"
Expand Down Expand Up @@ -915,3 +928,25 @@ def test_metadata_from_stac_temporal_dimension(tmp_path, stac_dict, expected):
assert (dim.name, dim.extent) == expected
else:
assert not metadata.has_temporal_dimension()


@pytest.mark.parametrize(
["kwargs", "expected_x", "expected_y"],
[
({}, {"crs": 4326, "step": 0.1}, {"crs": 4326, "step": 0.1}),
({"resolution": 2}, {"crs": 4326, "step": 2}, {"crs": 4326, "step": 2}),
({"resolution": [0.5, 2]}, {"crs": 4326, "step": 0.5}, {"crs": 4326, "step": 2}),
({"projection": 32631}, {"crs": 32631, "step": 0.1}, {"crs": 32631, "step": 0.1}),
({"resolution": 10, "projection": 32631}, {"crs": 32631, "step": 10}, {"crs": 32631, "step": 10}),
({"resolution": [11, 22], "projection": 32631}, {"crs": 32631, "step": 11}, {"crs": 32631, "step": 22}),
],
)
def test_metadata_resample_spatial(xytb_cube_metadata, kwargs, expected_x, expected_y):
metadata = xytb_cube_metadata.resample_spatial(**kwargs)
assert isinstance(metadata, CubeMetadata)
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], **expected_x),
SpatialDimension(name="y", extent=[49, 52], **expected_y),
]
assert metadata.temporal_dimension == xytb_cube_metadata.temporal_dimension
assert metadata.band_dimension == xytb_cube_metadata.band_dimension
29 changes: 29 additions & 0 deletions tests/utills/test_nomalize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from openeo.utils.normalize import normalize_resample_resolution


@pytest.mark.parametrize(
["resolution", "expected"],
[
(1, (1, 1)),
(1.23, (1.23, 1.23)),
([1, 2], (1, 2)),
((1.23, 2.34), (1.23, 2.34)),
],
)
def test_normalize_resample_resolution(resolution, expected):
assert normalize_resample_resolution(resolution) == expected


@pytest.mark.parametrize(
"resolution",
[
"0123",
[1, 2, 3],
{"x": 2, "y": 5},
],
)
def test_normalize_resample_resolution(resolution):
with pytest.raises(ValueError, match="Invalid resolution"):
normalize_resample_resolution(resolution)

0 comments on commit 564eed8

Please sign in to comment.