diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb00fc83..5744ca954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add initial support for accessing [Federation Extension](https://github.com/Open-EO/openeo-api/tree/master/extensions/federation) related metadata ([#668](https://github.com/Open-EO/openeo-python-client/issues/668)) +- `sar_backscatter`: try to retrieve coefficient options from backend ([#693](https://github.com/Open-EO/openeo-python-client/issues/693)) ### Changed diff --git a/openeo/internal/processes/parse.py b/openeo/internal/processes/parse.py index 1e22ba6bc..d7fb5bec4 100644 --- a/openeo/internal/processes/parse.py +++ b/openeo/internal/processes/parse.py @@ -5,6 +5,7 @@ from __future__ import annotations +import copy import json import re import typing @@ -43,6 +44,23 @@ def is_geojson_schema(schema) -> bool: return any(is_geojson_schema(s) for s in self.schema) return False + def get_enum_options(self): + result = None + if isinstance(self.schema,list): + for item in self.schema: + if "enum" in item: + if result is None: + result = copy.deepcopy(item["enum"]) + else: + result += item["enum"] + elif "type" in item: + if item["type"] == "null": + result += [None] + elif isinstance(self.schema,dict): + if "enum" in self.schema: + result = self.schema["enum"] + return result + _NO_DEFAULT = object() @@ -127,6 +145,12 @@ def from_json_file(cls, path: Union[str, Path]) -> Process: with Path(path).open("r") as f: return cls.from_json(f.read()) + def get_parameter(self, name: str) -> Parameter: + for parameter in self.parameters: + if parameter.name == name: + return parameter + raise LookupError(f"Parameter {name!r} not found.") + def parse_all_from_dir(path: Union[str, Path], pattern="*.json") -> Iterator[Process]: """Parse all openEO process files in given directory""" diff --git a/openeo/rest/datacube.py b/openeo/rest/datacube.py index 5b6bc8013..342a64019 100644 --- a/openeo/rest/datacube.py +++ b/openeo/rest/datacube.py @@ -35,6 +35,7 @@ convert_callable_to_pgnode, get_parameter_names, ) +from openeo.internal.processes.parse import Process from openeo.internal.warnings import UserDeprecationWarning, deprecated, legacy_alias from openeo.metadata import ( Band, @@ -2724,15 +2725,15 @@ def ard_normalized_radar_backscatter( @openeo_process def sar_backscatter( - self, - coefficient: Union[str, None] = "gamma0-terrain", - elevation_model: Union[str, None] = None, - mask: bool = False, - contributing_area: bool = False, - local_incidence_angle: bool = False, - ellipsoid_incidence_angle: bool = False, - noise_removal: bool = True, - options: Optional[dict] = None + self, + coefficient: Union[str, None] = _UNSET, + elevation_model: Union[str, None] = None, + mask: bool = False, + contributing_area: bool = False, + local_incidence_angle: bool = False, + ellipsoid_incidence_angle: bool = False, + noise_removal: bool = True, + options: Optional[dict] = None, ) -> DataCube: """ Computes backscatter from SAR input. @@ -2767,9 +2768,26 @@ def sar_backscatter( .. versionadded:: 0.4.9 .. versionchanged:: 0.4.10 replace `orthorectify` and `rtc` arguments with `coefficient`. """ - coefficient_options = [ - "beta0", "sigma0-ellipsoid", "sigma0-terrain", "gamma0-ellipsoid", "gamma0-terrain", None - ] + try: + parameter = Process.from_dict(self.connection.describe_process("sar_backscatter")).get_parameter( + "coefficient" + ) + schema = parameter.schema + coefficient_options = schema.get_enum_options() + if coefficient == _UNSET: + coefficient = parameter.default + except Exception as e: + log.warning(f"Failed to extract coefficient options for process `sar_backscatter`: {e}") + coefficient_options = [ + "beta0", + "sigma0-ellipsoid", + "sigma0-terrain", + "gamma0-ellipsoid", + "gamma0-terrain", + None, + ] + if coefficient == _UNSET: + coefficient = "gamma0-terrain" if coefficient not in coefficient_options: raise OpenEoClientException("Invalid `sar_backscatter` coefficient {c!r}. Should be one of {o}".format( c=coefficient, o=coefficient_options diff --git a/tests/internal/processes/test_parse.py b/tests/internal/processes/test_parse.py index 48405e537..6f283b815 100644 --- a/tests/internal/processes/test_parse.py +++ b/tests/internal/processes/test_parse.py @@ -36,6 +36,49 @@ def test_schema_accepts_geojson(schema, expected): assert Schema([{"type": "number"}, schema]).accepts_geojson() == expected +@pytest.mark.parametrize( + ("schema", "expected"), + [ + ( + Schema( + { + "type": "string", + "enum": [ + "average", + "bilinear", + "cubic", + "cubicspline", + "lanczos", + "max", + "med", + "min", + "mode", + "near", + ], + }, + ), + ["average", "bilinear", "cubic", "cubicspline", "lanczos", "max", "med", "min", "mode", "near"], + ), + ( + Schema([{"type": "string", "enum": ["replicate", "reflect", "reflect_pixel", "wrap"]}, {"type": "null"}]), + ["replicate", "reflect", "reflect_pixel", "wrap", None], + ), + ( + Schema( + [ + {"type": "string", "enum": ["replicate", "reflect"]}, + {"type": "null"}, + {"type": "number", "enum": ["reflect_pixel", "wrap"]}, + ] + ), + ["replicate", "reflect", None, "reflect_pixel", "wrap"], + ), + ], +) +def test_get_enum_options(schema, expected): + assert schema.get_enum_options() == expected + + def test_parameter(): p = Parameter.from_dict({ "name": "foo", @@ -138,6 +181,51 @@ def test_process_from_json(): assert p.returns.schema.schema == {"type": ["number", "null"], "minimum": 0} +@pytest.mark.parametrize( + ("key", "expected"), [ + ("x", Parameter.from_dict({"name": "x", "description": "A number.", "schema": {"type": ["number", "null"]}})), + ("y", Parameter.from_dict({"name": "y", "description": "A number.", "schema": {"type": ["number", "null"]}})), + ] +) +def test_get_parameter(key, expected): + p = Process.from_dict({ + "id": "absolute", + "summary": "Absolute value", + "description": "Computes the absolute value of a real number.", + "categories": ["math"], + "parameters": [ + {"name": "x", "description": "A number.", "schema": {"type": ["number", "null"]}}, + {"name": "y", "description": "A number.", "schema": {"type": ["number", "null"]}}, + ], + "returns": { + "description": "The computed absolute value.", + "schema": {"type": ["number", "null"], "minimum": 0} + }, + "links": [{"rel": "about", "href": "http://example.com/abs.html"}], + }) + assert p.get_parameter(key) == expected + + +def test_get_parameter_error(): + p = Process.from_dict({ + "id": "absolute", + "summary": "Absolute value", + "description": "Computes the absolute value of a real number.", + "categories": ["math"], + "parameters": [ + {"name": "x", "description": "A number.", "schema": {"type": ["number", "null"]}}, + {"name": "y", "description": "A number.", "schema": {"type": ["number", "null"]}}, + ], + "returns": { + "description": "The computed absolute value.", + "schema": {"type": ["number", "null"], "minimum": 0} + }, + "links": [{"rel": "about", "href": "http://example.com/abs.html"}], + }) + with pytest.raises(LookupError): + p.get_parameter("z") + + def test_parse_remote_process_definition_minimal(requests_mock): url = "https://example.com/ndvi.json" requests_mock.get(url, json={"id": "ndvi"}) diff --git a/tests/rest/datacube/test_datacube100.py b/tests/rest/datacube/test_datacube100.py index f36ad770f..343a07b84 100644 --- a/tests/rest/datacube/test_datacube100.py +++ b/tests/rest/datacube/test_datacube100.py @@ -2828,6 +2828,53 @@ def test_sar_backscatter_coefficient_invalid(con100): cube.sar_backscatter(coefficient="unicorn") +def test_sar_backscatter_check_coefficient_backend(con100, requests_mock): + requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + processes = [ + { + "id": "sar_backscatter", + "description": "Computes backscatter from SAR input", + "summary": "Computes backscatter from SAR input", + "parameters": [ + { + "default": "gamma0-ellipsoid", + "description": "Select the radiometric correction coefficient.", + "name": "coefficient", + "schema": [ + { + "enum": [ + "beta0", + "sigma0-ellipsoid", + "sigma0-terrain", + "gamma0-ellipsoid", + "gamma0-terrain", + ], + "type": "string", + }, + ], + }, + ], + "returns": {"description": "incremented value", "schema": {"type": "integer"}}, + } + ] + requests_mock.get(API_URL + "/processes", json={"processes": processes}) + cube = con100.load_collection("S2").sar_backscatter() + assert _get_leaf_node(cube) == { + "process_id": "sar_backscatter", + "arguments": { + "data": {"from_node": "loadcollection1"}, + "coefficient": "gamma0-ellipsoid", + "elevation_model": None, + "mask": False, + "contributing_area": False, + "local_incidence_angle": False, + "ellipsoid_incidence_angle": False, + "noise_removal": True, + }, + "result": True, + } + + def test_datacube_from_process(con100): cube = con100.datacube_from_process("colorize", color="red", size=4) assert cube.flat_graph() == {