openeo-gfmap 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  """Feature extractor functionalities. Such as a base class to assist the
2
2
  implementation of feature extractors of a UDF.
3
3
  """
4
+
4
5
  import functools
5
6
  import inspect
6
7
  import logging
@@ -32,6 +33,8 @@ class FeatureExtractor(ABC):
32
33
  """
33
34
 
34
35
  def __init__(self) -> None:
36
+ self._epsg = None
37
+
35
38
  logging.basicConfig(level=logging.INFO)
36
39
  self.logger = logging.getLogger(self.__class__.__name__)
37
40
 
@@ -88,6 +91,10 @@ class FeatureExtractor(ABC):
88
91
  """Returns the EPSG code of the datacube."""
89
92
  return self._epsg
90
93
 
94
+ @epsg.setter
95
+ def epsg(self, value: int):
96
+ self._epsg = value
97
+
91
98
  def dependencies(self) -> list:
92
99
  """Returns the additional dependencies such as wheels or zip files.
93
100
  Dependencies should be returned as a list of string, which will set-up at the top of the
@@ -204,6 +211,7 @@ class PatchFeatureExtractor(FeatureExtractor):
204
211
  arr.loc[dict(bands=s1_bands_to_select)] = data_to_rescale
205
212
  return arr
206
213
 
214
+ # TODO to remove the fixed transpose as it contributes to unclear code.
207
215
  def _execute(self, cube: XarrayDataCube, parameters: dict) -> XarrayDataCube:
208
216
  arr = cube.get_array().transpose("bands", "t", "y", "x")
209
217
  arr = self._common_preparations(arr, parameters)
@@ -371,6 +379,7 @@ def apply_feature_extractor_local(
371
379
  )
372
380
 
373
381
  feature_extractor = feature_extractor_class()
382
+ feature_extractor._parameters = parameters
374
383
  output_labels = feature_extractor.output_labels()
375
384
  dependencies = feature_extractor.dependencies()
376
385
 
@@ -7,15 +7,27 @@ component.
7
7
 
8
8
  import logging
9
9
 
10
- from .fetching import CollectionFetcher, FetchType
11
- from .s1 import build_sentinel1_grd_extractor
12
- from .s2 import build_sentinel2_l2a_extractor
13
-
14
10
  _log = logging.getLogger(__name__)
11
+ _log.setLevel(logging.INFO)
12
+
13
+ ch = logging.StreamHandler()
14
+ ch.setLevel(logging.INFO)
15
+
16
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
17
+ ch.setFormatter(formatter)
18
+
19
+ _log.addHandler(ch)
15
20
 
16
21
  __all__ = [
17
22
  "build_sentinel2_l2a_extractor",
18
23
  "CollectionFetcher",
19
24
  "FetchType",
20
25
  "build_sentinel1_grd_extractor",
26
+ "build_generic_extractor",
27
+ "build_generic_extractor_stac",
21
28
  ]
29
+
30
+ from .fetching import CollectionFetcher, FetchType # noqa: E402
31
+ from .generic import build_generic_extractor, build_generic_extractor_stac # noqa: E402
32
+ from .s1 import build_sentinel1_grd_extractor # noqa: E402
33
+ from .s2 import build_sentinel2_l2a_extractor # noqa: E402
@@ -108,6 +108,7 @@ def _load_collection_hybrid(
108
108
  return cube
109
109
 
110
110
 
111
+ # TODO; deprecated?
111
112
  def _load_collection(
112
113
  connection: openeo.Connection,
113
114
  bands: list,
@@ -1,11 +1,10 @@
1
1
  """ Generic extraction of features, supporting VITO backend.
2
2
  """
3
3
 
4
- from functools import partial
5
- from typing import Callable
4
+ from typing import Callable, Optional
6
5
 
7
6
  import openeo
8
- from geojson import GeoJSON
7
+ from openeo.rest import OpenEoApiError
9
8
 
10
9
  from openeo_gfmap.backend import Backend, BackendContext
11
10
  from openeo_gfmap.fetching import CollectionFetcher, FetchType, _log
@@ -29,15 +28,32 @@ BASE_WEATHER_MAPPING = {
29
28
  "vapour-pressure": "AGERA5-VAPOUR",
30
29
  "wind-speed": "AGERA5-WIND",
31
30
  }
31
+ AGERA5_STAC_MAPPING = {
32
+ "dewpoint_temperature_mean": "AGERA5-DEWTEMP",
33
+ "total_precipitation": "AGERA5-PRECIP",
34
+ "solar_radiation_flux": "AGERA5-SOLRAD",
35
+ "2m_temperature_max": "AGERA5-TMAX",
36
+ "2m_temperature_mean": "AGERA5-TMEAN",
37
+ "2m_temperature_min": "AGERA5-TMIN",
38
+ "vapour_pressure": "AGERA5-VAPOUR",
39
+ "wind_speed": "AGERA5-WIND",
40
+ }
41
+ KNOWN_UNTEMPORAL_COLLECTIONS = ["COPERNICUS_30"]
42
+
43
+ AGERA5_TERRASCOPE_STAC = "https://stac.openeo.vito.be/collections/agera5_daily"
44
+
32
45
 
46
+ def _get_generic_fetcher(
47
+ collection_name: str, fetch_type: FetchType, backend: Backend, is_stac: bool
48
+ ) -> Callable:
49
+ band_mapping: Optional[dict] = None
33
50
 
34
- def _get_generic_fetcher(collection_name: str, fetch_type: FetchType) -> Callable:
35
51
  if collection_name == "COPERNICUS_30":
36
- BASE_MAPPING = BASE_DEM_MAPPING
52
+ band_mapping = BASE_DEM_MAPPING
37
53
  elif collection_name == "AGERA5":
38
- BASE_MAPPING = BASE_WEATHER_MAPPING
39
- else:
40
- raise Exception("Please choose a valid collection.")
54
+ band_mapping = BASE_WEATHER_MAPPING
55
+ elif is_stac and (AGERA5_TERRASCOPE_STAC in collection_name):
56
+ band_mapping = AGERA5_STAC_MAPPING
41
57
 
42
58
  def generic_default_fetcher(
43
59
  connection: openeo.Connection,
@@ -46,43 +62,58 @@ def _get_generic_fetcher(collection_name: str, fetch_type: FetchType) -> Callabl
46
62
  bands: list,
47
63
  **params,
48
64
  ) -> openeo.DataCube:
49
- bands = convert_band_names(bands, BASE_MAPPING)
65
+ if band_mapping is not None:
66
+ bands = convert_band_names(bands, band_mapping)
50
67
 
51
- if (collection_name == "COPERNICUS_30") and (temporal_extent is not None):
68
+ if (collection_name in KNOWN_UNTEMPORAL_COLLECTIONS) and (
69
+ temporal_extent is not None
70
+ ):
52
71
  _log.warning(
53
- "User set-up non None temporal extent for DEM collection. Ignoring it."
72
+ "Ignoring the temporal extent provided by the user as the collection %s is known to be untemporal.",
73
+ collection_name,
54
74
  )
55
75
  temporal_extent = None
56
76
 
57
- cube = _load_collection(
58
- connection,
59
- bands,
60
- collection_name,
61
- spatial_extent,
62
- temporal_extent,
63
- fetch_type,
64
- **params,
65
- )
77
+ try:
78
+ cube = _load_collection(
79
+ connection,
80
+ bands,
81
+ collection_name,
82
+ spatial_extent,
83
+ temporal_extent,
84
+ fetch_type,
85
+ is_stac=is_stac,
86
+ **params,
87
+ )
88
+ except OpenEoApiError as e:
89
+ if "CollectionNotFound" in str(e):
90
+ raise ValueError(
91
+ f"Collection {collection_name} not found in the selected backend {backend.value}."
92
+ ) from e
93
+ raise e
66
94
 
67
- # Apply if the collection is a GeoJSON Feature collection
68
- if isinstance(spatial_extent, GeoJSON):
69
- cube = cube.filter_spatial(spatial_extent)
95
+ # # Apply if the collection is a GeoJSON Feature collection
96
+ # if isinstance(spatial_extent, GeoJSON):
97
+ # cube = cube.filter_spatial(spatial_extent)
70
98
 
71
99
  return cube
72
100
 
73
101
  return generic_default_fetcher
74
102
 
75
103
 
76
- def _get_generic_processor(collection_name: str, fetch_type: FetchType) -> Callable:
104
+ def _get_generic_processor(
105
+ collection_name: str, fetch_type: FetchType, is_stac: bool
106
+ ) -> Callable:
77
107
  """Builds the preprocessing function from the collection name as it stored
78
108
  in the target backend.
79
109
  """
110
+ band_mapping: Optional[dict] = None
80
111
  if collection_name == "COPERNICUS_30":
81
- BASE_MAPPING = BASE_DEM_MAPPING
112
+ band_mapping = BASE_DEM_MAPPING
82
113
  elif collection_name == "AGERA5":
83
- BASE_MAPPING = BASE_WEATHER_MAPPING
84
- else:
85
- raise Exception("Please choose a valid collection.")
114
+ band_mapping = BASE_WEATHER_MAPPING
115
+ elif is_stac and (AGERA5_TERRASCOPE_STAC in collection_name):
116
+ band_mapping = AGERA5_STAC_MAPPING
86
117
 
87
118
  def generic_default_processor(cube: openeo.DataCube, **params):
88
119
  """Default collection preprocessing method for generic datasets.
@@ -100,51 +131,14 @@ def _get_generic_processor(collection_name: str, fetch_type: FetchType) -> Calla
100
131
  if collection_name == "COPERNICUS_30":
101
132
  cube = cube.min_time()
102
133
 
103
- cube = rename_bands(cube, BASE_MAPPING)
134
+ if band_mapping is not None:
135
+ cube = rename_bands(cube, band_mapping)
104
136
 
105
137
  return cube
106
138
 
107
139
  return generic_default_processor
108
140
 
109
141
 
110
- OTHER_BACKEND_MAP = {
111
- "AGERA5": {
112
- Backend.TERRASCOPE: {
113
- "fetch": partial(_get_generic_fetcher, collection_name="AGERA5"),
114
- "preprocessor": partial(_get_generic_processor, collection_name="AGERA5"),
115
- },
116
- Backend.CDSE: {
117
- "fetch": partial(_get_generic_fetcher, collection_name="AGERA5"),
118
- "preprocessor": partial(_get_generic_processor, collection_name="AGERA5"),
119
- },
120
- Backend.FED: {
121
- "fetch": partial(_get_generic_fetcher, collection_name="AGERA5"),
122
- "preprocessor": partial(_get_generic_processor, collection_name="AGERA5"),
123
- },
124
- },
125
- "COPERNICUS_30": {
126
- Backend.TERRASCOPE: {
127
- "fetch": partial(_get_generic_fetcher, collection_name="COPERNICUS_30"),
128
- "preprocessor": partial(
129
- _get_generic_processor, collection_name="COPERNICUS_30"
130
- ),
131
- },
132
- Backend.CDSE: {
133
- "fetch": partial(_get_generic_fetcher, collection_name="COPERNICUS_30"),
134
- "preprocessor": partial(
135
- _get_generic_processor, collection_name="COPERNICUS_30"
136
- ),
137
- },
138
- Backend.FED: {
139
- "fetch": partial(_get_generic_fetcher, collection_name="COPERNICUS_30"),
140
- "preprocessor": partial(
141
- _get_generic_processor, collection_name="COPERNICUS_30"
142
- ),
143
- },
144
- },
145
- }
146
-
147
-
148
142
  def build_generic_extractor(
149
143
  backend_context: BackendContext,
150
144
  bands: list,
@@ -152,14 +146,28 @@ def build_generic_extractor(
152
146
  collection_name: str,
153
147
  **params,
154
148
  ) -> CollectionFetcher:
155
- """Creates a generic extractor adapted to the given backend. Currently only tested with VITO backend"""
156
- backend_functions = OTHER_BACKEND_MAP.get(collection_name).get(
157
- backend_context.backend
149
+ """Creates a generic extractor adapted to the given backend. Provides band mappings for known
150
+ collections, such as AGERA5 available on Terrascope/FED and Copernicus 30m DEM in all backends.
151
+ """
152
+ fetcher = _get_generic_fetcher(
153
+ collection_name, fetch_type, backend_context.backend, False
158
154
  )
155
+ preprocessor = _get_generic_processor(collection_name, fetch_type, False)
159
156
 
160
- fetcher, preprocessor = (
161
- backend_functions["fetch"](fetch_type=fetch_type),
162
- backend_functions["preprocessor"](fetch_type=fetch_type),
157
+ return CollectionFetcher(backend_context, bands, fetcher, preprocessor, **params)
158
+
159
+
160
+ def build_generic_extractor_stac(
161
+ backend_context: BackendContext,
162
+ bands: list,
163
+ fetch_type: FetchType,
164
+ collection_url: str,
165
+ **params,
166
+ ) -> CollectionFetcher:
167
+ """Creates a generic extractor adapted to the given backend. Currently only tested with VITO backend"""
168
+ fetcher = _get_generic_fetcher(
169
+ collection_url, fetch_type, backend_context.backend, True
163
170
  )
171
+ preprocessor = _get_generic_processor(collection_url, fetch_type, True)
164
172
 
165
173
  return CollectionFetcher(backend_context, bands, fetcher, preprocessor, **params)
@@ -67,8 +67,6 @@ def _get_s1_grd_default_fetcher(
67
67
  """
68
68
  bands = convert_band_names(bands, BASE_SENTINEL1_GRD_MAPPING)
69
69
 
70
- load_collection_parameters = params.get("load_collection", {})
71
-
72
70
  cube = _load_collection(
73
71
  connection,
74
72
  bands,
@@ -76,7 +74,7 @@ def _get_s1_grd_default_fetcher(
76
74
  spatial_extent,
77
75
  temporal_extent,
78
76
  fetch_type,
79
- **load_collection_parameters,
77
+ **params,
80
78
  )
81
79
 
82
80
  if fetch_type is not FetchType.POINT and isinstance(spatial_extent, GeoJSON):
@@ -113,6 +113,7 @@ def _get_s2_l2a_default_fetcher(
113
113
  return s2_l2a_fetch_default
114
114
 
115
115
 
116
+ # TODO deprecated?
116
117
  def _get_s2_l2a_element84_fetcher(
117
118
  collection_name: str, fetch_type: FetchType
118
119
  ) -> Callable:
@@ -1,6 +1,7 @@
1
1
  """Inference functionalities. Such as a base class to assist the implementation
2
2
  of inference models on an UDF.
3
3
  """
4
+
4
5
  import functools
5
6
  import inspect
6
7
  import logging
@@ -74,8 +75,9 @@ class ModelInference(ABC):
74
75
 
75
76
  return abs_path
76
77
 
78
+ @classmethod
77
79
  @functools.lru_cache(maxsize=6)
78
- def load_ort_session(self, model_url: str):
80
+ def load_ort_session(cls, model_url: str):
79
81
  """Loads an onnx session from a publicly available URL. The URL must be a direct
80
82
  download link to the ONNX session file.
81
83
  The `lru_cache` decorator avoids loading multiple time the model within the same worker.
@@ -180,7 +182,7 @@ class ONNXModelInference(ModelInference):
180
182
  raise ValueError("The model_url must be defined in the parameters.")
181
183
 
182
184
  # Load the model and the input_name parameters
183
- session = self.load_ort_session(self._parameters.get("model_url"))
185
+ session = ModelInference.load_ort_session(self._parameters.get("model_url"))
184
186
 
185
187
  input_name = self._parameters.get("input_name")
186
188
  if input_name is None:
@@ -329,6 +331,7 @@ def apply_model_inference_local(
329
331
  )
330
332
 
331
333
  model_inference = model_inference_class()
334
+ model_inference._parameters = parameters
332
335
  output_labels = model_inference.output_labels()
333
336
  dependencies = model_inference.dependencies()
334
337