anemoi-datasets 0.3.10__py3-none-any.whl → 0.4.2__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.
- anemoi/datasets/_version.py +2 -2
- anemoi/datasets/commands/compare.py +59 -0
- anemoi/datasets/commands/create.py +84 -3
- anemoi/datasets/commands/inspect.py +9 -9
- anemoi/datasets/commands/scan.py +4 -4
- anemoi/datasets/compute/recentre.py +14 -9
- anemoi/datasets/create/__init__.py +44 -17
- anemoi/datasets/create/check.py +6 -5
- anemoi/datasets/create/chunks.py +1 -1
- anemoi/datasets/create/config.py +6 -27
- anemoi/datasets/create/functions/__init__.py +3 -3
- anemoi/datasets/create/functions/filters/empty.py +4 -4
- anemoi/datasets/create/functions/filters/rename.py +14 -6
- anemoi/datasets/create/functions/filters/rotate_winds.py +16 -60
- anemoi/datasets/create/functions/filters/unrotate_winds.py +14 -64
- anemoi/datasets/create/functions/sources/__init__.py +39 -0
- anemoi/datasets/create/functions/sources/accumulations.py +38 -56
- anemoi/datasets/create/functions/sources/constants.py +11 -4
- anemoi/datasets/create/functions/sources/empty.py +2 -2
- anemoi/datasets/create/functions/sources/forcings.py +3 -3
- anemoi/datasets/create/functions/sources/grib.py +8 -4
- anemoi/datasets/create/functions/sources/hindcasts.py +32 -364
- anemoi/datasets/create/functions/sources/mars.py +57 -26
- anemoi/datasets/create/functions/sources/netcdf.py +2 -60
- anemoi/datasets/create/functions/sources/opendap.py +3 -2
- anemoi/datasets/create/functions/sources/source.py +3 -3
- anemoi/datasets/create/functions/sources/tendencies.py +7 -7
- anemoi/datasets/create/functions/sources/xarray/__init__.py +73 -0
- anemoi/datasets/create/functions/sources/xarray/coordinates.py +234 -0
- anemoi/datasets/create/functions/sources/xarray/field.py +109 -0
- anemoi/datasets/create/functions/sources/xarray/fieldlist.py +171 -0
- anemoi/datasets/create/functions/sources/xarray/flavour.py +330 -0
- anemoi/datasets/create/functions/sources/xarray/grid.py +46 -0
- anemoi/datasets/create/functions/sources/xarray/metadata.py +161 -0
- anemoi/datasets/create/functions/sources/xarray/time.py +98 -0
- anemoi/datasets/create/functions/sources/xarray/variable.py +198 -0
- anemoi/datasets/create/functions/sources/xarray_kerchunk.py +42 -0
- anemoi/datasets/create/functions/sources/xarray_zarr.py +15 -0
- anemoi/datasets/create/functions/sources/zenodo.py +40 -0
- anemoi/datasets/create/input.py +309 -191
- anemoi/datasets/create/loaders.py +155 -77
- anemoi/datasets/create/patch.py +17 -14
- anemoi/datasets/create/persistent.py +1 -1
- anemoi/datasets/create/size.py +4 -5
- anemoi/datasets/create/statistics/__init__.py +51 -17
- anemoi/datasets/create/template.py +11 -61
- anemoi/datasets/create/trace.py +91 -0
- anemoi/datasets/create/utils.py +5 -52
- anemoi/datasets/create/zarr.py +24 -10
- anemoi/datasets/data/dataset.py +4 -4
- anemoi/datasets/data/misc.py +9 -37
- anemoi/datasets/data/stores.py +37 -14
- anemoi/datasets/dates/__init__.py +7 -1
- anemoi/datasets/dates/groups.py +3 -0
- {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/METADATA +24 -8
- anemoi_datasets-0.4.2.dist-info/RECORD +86 -0
- {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/WHEEL +1 -1
- anemoi_datasets-0.3.10.dist-info/RECORD +0 -73
- {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/LICENSE +0 -0
- {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/entry_points.txt +0 -0
- {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# (C) Copyright 2024 ECMWF.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
6
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
7
|
+
# nor does it submit to any jurisdiction.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .coordinates import DateCoordinate
|
|
12
|
+
from .coordinates import LatitudeCoordinate
|
|
13
|
+
from .coordinates import LevelCoordinate
|
|
14
|
+
from .coordinates import LongitudeCoordinate
|
|
15
|
+
from .coordinates import ScalarCoordinate
|
|
16
|
+
from .coordinates import StepCoordinate
|
|
17
|
+
from .coordinates import TimeCoordinate
|
|
18
|
+
from .coordinates import XCoordinate
|
|
19
|
+
from .coordinates import YCoordinate
|
|
20
|
+
from .grid import MeshedGrid
|
|
21
|
+
from .grid import UnstructuredGrid
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CoordinateGuesser:
|
|
25
|
+
|
|
26
|
+
def __init__(self, ds):
|
|
27
|
+
self.ds = ds
|
|
28
|
+
self._cache = {}
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_flavour(cls, ds, flavour):
|
|
32
|
+
if flavour is None:
|
|
33
|
+
return DefaultCoordinateGuesser(ds)
|
|
34
|
+
else:
|
|
35
|
+
return FlavourCoordinateGuesser(ds, flavour)
|
|
36
|
+
|
|
37
|
+
def guess(self, c, coord):
|
|
38
|
+
if coord not in self._cache:
|
|
39
|
+
self._cache[coord] = self._guess(c, coord)
|
|
40
|
+
return self._cache[coord]
|
|
41
|
+
|
|
42
|
+
def _guess(self, c, coord):
|
|
43
|
+
|
|
44
|
+
name = c.name
|
|
45
|
+
standard_name = getattr(c, "standard_name", "").lower()
|
|
46
|
+
axis = getattr(c, "axis", "")
|
|
47
|
+
long_name = getattr(c, "long_name", "").lower()
|
|
48
|
+
units = getattr(c, "units", "")
|
|
49
|
+
|
|
50
|
+
d = self._is_longitude(
|
|
51
|
+
c,
|
|
52
|
+
axis=axis,
|
|
53
|
+
name=name,
|
|
54
|
+
long_name=long_name,
|
|
55
|
+
standard_name=standard_name,
|
|
56
|
+
units=units,
|
|
57
|
+
)
|
|
58
|
+
if d is not None:
|
|
59
|
+
return d
|
|
60
|
+
|
|
61
|
+
d = self._is_latitude(
|
|
62
|
+
c,
|
|
63
|
+
axis=axis,
|
|
64
|
+
name=name,
|
|
65
|
+
long_name=long_name,
|
|
66
|
+
standard_name=standard_name,
|
|
67
|
+
units=units,
|
|
68
|
+
)
|
|
69
|
+
if d is not None:
|
|
70
|
+
return d
|
|
71
|
+
|
|
72
|
+
d = self._is_x(
|
|
73
|
+
c,
|
|
74
|
+
axis=axis,
|
|
75
|
+
name=name,
|
|
76
|
+
long_name=long_name,
|
|
77
|
+
standard_name=standard_name,
|
|
78
|
+
units=units,
|
|
79
|
+
)
|
|
80
|
+
if d is not None:
|
|
81
|
+
return d
|
|
82
|
+
|
|
83
|
+
d = self._is_y(
|
|
84
|
+
c,
|
|
85
|
+
axis=axis,
|
|
86
|
+
name=name,
|
|
87
|
+
long_name=long_name,
|
|
88
|
+
standard_name=standard_name,
|
|
89
|
+
units=units,
|
|
90
|
+
)
|
|
91
|
+
if d is not None:
|
|
92
|
+
return d
|
|
93
|
+
|
|
94
|
+
d = self._is_time(
|
|
95
|
+
c,
|
|
96
|
+
axis=axis,
|
|
97
|
+
name=name,
|
|
98
|
+
long_name=long_name,
|
|
99
|
+
standard_name=standard_name,
|
|
100
|
+
units=units,
|
|
101
|
+
)
|
|
102
|
+
if d is not None:
|
|
103
|
+
return d
|
|
104
|
+
|
|
105
|
+
d = self._is_step(
|
|
106
|
+
c,
|
|
107
|
+
axis=axis,
|
|
108
|
+
name=name,
|
|
109
|
+
long_name=long_name,
|
|
110
|
+
standard_name=standard_name,
|
|
111
|
+
units=units,
|
|
112
|
+
)
|
|
113
|
+
if d is not None:
|
|
114
|
+
return d
|
|
115
|
+
|
|
116
|
+
d = self._is_date(
|
|
117
|
+
c,
|
|
118
|
+
axis=axis,
|
|
119
|
+
name=name,
|
|
120
|
+
long_name=long_name,
|
|
121
|
+
standard_name=standard_name,
|
|
122
|
+
units=units,
|
|
123
|
+
)
|
|
124
|
+
if d is not None:
|
|
125
|
+
return d
|
|
126
|
+
|
|
127
|
+
d = self._is_level(
|
|
128
|
+
c,
|
|
129
|
+
axis=axis,
|
|
130
|
+
name=name,
|
|
131
|
+
long_name=long_name,
|
|
132
|
+
standard_name=standard_name,
|
|
133
|
+
units=units,
|
|
134
|
+
)
|
|
135
|
+
if d is not None:
|
|
136
|
+
return d
|
|
137
|
+
|
|
138
|
+
if c.shape in ((1,), tuple()):
|
|
139
|
+
return ScalarCoordinate(c)
|
|
140
|
+
|
|
141
|
+
raise NotImplementedError(
|
|
142
|
+
f"Coordinate {coord} not supported\n{axis=}, {name=},"
|
|
143
|
+
f" {long_name=}, {standard_name=}, units\n\n{c}\n\n{type(c.values)} {c.shape}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def grid(self, coordinates):
|
|
147
|
+
lat = [c for c in coordinates if c.is_lat]
|
|
148
|
+
lon = [c for c in coordinates if c.is_lon]
|
|
149
|
+
|
|
150
|
+
if len(lat) != 1:
|
|
151
|
+
raise NotImplementedError(f"Expected 1 latitude coordinate, got {len(lat)}")
|
|
152
|
+
|
|
153
|
+
if len(lon) != 1:
|
|
154
|
+
raise NotImplementedError(f"Expected 1 longitude coordinate, got {len(lon)}")
|
|
155
|
+
|
|
156
|
+
lat = lat[0]
|
|
157
|
+
lon = lon[0]
|
|
158
|
+
|
|
159
|
+
if (lat.name, lon.name) in self._cache:
|
|
160
|
+
return self._cache[(lat.name, lon.name)]
|
|
161
|
+
|
|
162
|
+
assert len(lat.variable.shape) == len(lon.variable.shape), (lat.variable.shape, lon.variable.shape)
|
|
163
|
+
if len(lat.variable.shape) == 1:
|
|
164
|
+
grid = MeshedGrid(lat, lon)
|
|
165
|
+
else:
|
|
166
|
+
grid = UnstructuredGrid(lat, lon)
|
|
167
|
+
|
|
168
|
+
self._cache[(lat.name, lon.name)] = grid
|
|
169
|
+
return grid
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class DefaultCoordinateGuesser(CoordinateGuesser):
|
|
173
|
+
def __init__(self, ds):
|
|
174
|
+
super().__init__(ds)
|
|
175
|
+
|
|
176
|
+
def _is_longitude(self, c, *, axis, name, long_name, standard_name, units):
|
|
177
|
+
if standard_name == "longitude":
|
|
178
|
+
return LongitudeCoordinate(c)
|
|
179
|
+
|
|
180
|
+
if long_name == "longitude" and units == "degrees_east":
|
|
181
|
+
return LongitudeCoordinate(c)
|
|
182
|
+
|
|
183
|
+
if name == "longitude": # WeatherBench
|
|
184
|
+
return LongitudeCoordinate(c)
|
|
185
|
+
|
|
186
|
+
def _is_latitude(self, c, *, axis, name, long_name, standard_name, units):
|
|
187
|
+
if standard_name == "latitude":
|
|
188
|
+
return LatitudeCoordinate(c)
|
|
189
|
+
|
|
190
|
+
if long_name == "latitude" and units == "degrees_north":
|
|
191
|
+
return LatitudeCoordinate(c)
|
|
192
|
+
|
|
193
|
+
if name == "latitude": # WeatherBench
|
|
194
|
+
return LatitudeCoordinate(c)
|
|
195
|
+
|
|
196
|
+
def _is_x(self, c, *, axis, name, long_name, standard_name, units):
|
|
197
|
+
if standard_name == "projection_x_coordinate":
|
|
198
|
+
return XCoordinate(c)
|
|
199
|
+
|
|
200
|
+
if name == "x":
|
|
201
|
+
return XCoordinate(c)
|
|
202
|
+
|
|
203
|
+
def _is_y(self, c, *, axis, name, long_name, standard_name, units):
|
|
204
|
+
if standard_name == "projection_y_coordinate":
|
|
205
|
+
return YCoordinate(c)
|
|
206
|
+
|
|
207
|
+
if name == "y":
|
|
208
|
+
return YCoordinate(c)
|
|
209
|
+
|
|
210
|
+
def _is_time(self, c, *, axis, name, long_name, standard_name, units):
|
|
211
|
+
if standard_name == "time":
|
|
212
|
+
return TimeCoordinate(c)
|
|
213
|
+
|
|
214
|
+
if name == "time":
|
|
215
|
+
return TimeCoordinate(c)
|
|
216
|
+
|
|
217
|
+
def _is_date(self, c, *, axis, name, long_name, standard_name, units):
|
|
218
|
+
if standard_name == "forecast_reference_time":
|
|
219
|
+
return DateCoordinate(c)
|
|
220
|
+
if name == "forecast_reference_time":
|
|
221
|
+
return DateCoordinate(c)
|
|
222
|
+
|
|
223
|
+
def _is_step(self, c, *, axis, name, long_name, standard_name, units):
|
|
224
|
+
if standard_name == "forecast_period":
|
|
225
|
+
return StepCoordinate(c)
|
|
226
|
+
|
|
227
|
+
if long_name == "time elapsed since the start of the forecast":
|
|
228
|
+
return StepCoordinate(c)
|
|
229
|
+
|
|
230
|
+
if name == "prediction_timedelta": # WeatherBench
|
|
231
|
+
return StepCoordinate(c)
|
|
232
|
+
|
|
233
|
+
def _is_level(self, c, *, axis, name, long_name, standard_name, units):
|
|
234
|
+
if standard_name == "atmosphere_hybrid_sigma_pressure_coordinate":
|
|
235
|
+
return LevelCoordinate(c, "ml")
|
|
236
|
+
|
|
237
|
+
if long_name == "height" and units == "m":
|
|
238
|
+
return LevelCoordinate(c, "height")
|
|
239
|
+
|
|
240
|
+
if standard_name == "air_pressure" and units == "hPa":
|
|
241
|
+
return LevelCoordinate(c, "pl")
|
|
242
|
+
|
|
243
|
+
if name == "level":
|
|
244
|
+
return LevelCoordinate(c, "pl")
|
|
245
|
+
|
|
246
|
+
if name == "vertical" and units == "hPa":
|
|
247
|
+
return LevelCoordinate(c, "pl")
|
|
248
|
+
|
|
249
|
+
if standard_name == "depth":
|
|
250
|
+
return LevelCoordinate(c, "depth")
|
|
251
|
+
|
|
252
|
+
if name == "pressure":
|
|
253
|
+
return LevelCoordinate(c, "pl")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class FlavourCoordinateGuesser(CoordinateGuesser):
|
|
257
|
+
def __init__(self, ds, flavour):
|
|
258
|
+
super().__init__(ds)
|
|
259
|
+
self.flavour = flavour
|
|
260
|
+
|
|
261
|
+
def _match(self, c, key, values):
|
|
262
|
+
|
|
263
|
+
if key not in self.flavour["rules"]:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
rules = self.flavour["rules"][key]
|
|
267
|
+
|
|
268
|
+
if not isinstance(rules, list):
|
|
269
|
+
rules = [rules]
|
|
270
|
+
|
|
271
|
+
for rule in rules:
|
|
272
|
+
ok = True
|
|
273
|
+
for k, v in rule.items():
|
|
274
|
+
if isinstance(v, str) and values.get(k) != v:
|
|
275
|
+
ok = False
|
|
276
|
+
if ok:
|
|
277
|
+
return rule
|
|
278
|
+
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
def _is_longitude(self, c, *, axis, name, long_name, standard_name, units):
|
|
282
|
+
if self._match(c, "longitude", locals()):
|
|
283
|
+
return LongitudeCoordinate(c)
|
|
284
|
+
|
|
285
|
+
def _is_latitude(self, c, *, axis, name, long_name, standard_name, units):
|
|
286
|
+
if self._match(c, "latitude", locals()):
|
|
287
|
+
return LatitudeCoordinate(c)
|
|
288
|
+
|
|
289
|
+
def _is_x(self, c, *, axis, name, long_name, standard_name, units):
|
|
290
|
+
if self._match(c, "x", locals()):
|
|
291
|
+
return XCoordinate(c)
|
|
292
|
+
|
|
293
|
+
def _is_y(self, c, *, axis, name, long_name, standard_name, units):
|
|
294
|
+
if self._match(c, "y", locals()):
|
|
295
|
+
return YCoordinate(c)
|
|
296
|
+
|
|
297
|
+
def _is_time(self, c, *, axis, name, long_name, standard_name, units):
|
|
298
|
+
if self._match(c, "time", locals()):
|
|
299
|
+
return TimeCoordinate(c)
|
|
300
|
+
|
|
301
|
+
def _is_step(self, c, *, axis, name, long_name, standard_name, units):
|
|
302
|
+
if self._match(c, "step", locals()):
|
|
303
|
+
return StepCoordinate(c)
|
|
304
|
+
|
|
305
|
+
def _is_date(self, c, *, axis, name, long_name, standard_name, units):
|
|
306
|
+
if self._match(c, "date", locals()):
|
|
307
|
+
return DateCoordinate(c)
|
|
308
|
+
|
|
309
|
+
def _is_level(self, c, *, axis, name, long_name, standard_name, units):
|
|
310
|
+
|
|
311
|
+
rule = self._match(c, "level", locals())
|
|
312
|
+
if rule:
|
|
313
|
+
# assert False, rule
|
|
314
|
+
return LevelCoordinate(
|
|
315
|
+
c,
|
|
316
|
+
self._levtype(
|
|
317
|
+
c,
|
|
318
|
+
axis=axis,
|
|
319
|
+
name=name,
|
|
320
|
+
long_name=long_name,
|
|
321
|
+
standard_name=standard_name,
|
|
322
|
+
units=units,
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _levtype(self, c, *, axis, name, long_name, standard_name, units):
|
|
327
|
+
if "levtype" in self.flavour:
|
|
328
|
+
return self.flavour["levtype"]
|
|
329
|
+
|
|
330
|
+
raise NotImplementedError(f"levtype for {c=}")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# (C) Copyright 2024 ECMWF.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
6
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
7
|
+
# nor does it submit to any jurisdiction.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Grid:
|
|
15
|
+
def __init__(self, lat, lon):
|
|
16
|
+
self.lat = lat
|
|
17
|
+
self.lon = lon
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def latitudes(self):
|
|
21
|
+
return self.grid_points()[0]
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def longitudes(self):
|
|
25
|
+
return self.grid_points()[1]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MeshedGrid(Grid):
|
|
29
|
+
_cache = None
|
|
30
|
+
|
|
31
|
+
def grid_points(self):
|
|
32
|
+
if self._cache is not None:
|
|
33
|
+
return self._cache
|
|
34
|
+
lat = self.lat.variable.values
|
|
35
|
+
lon = self.lon.variable.values
|
|
36
|
+
|
|
37
|
+
lat, lon = np.meshgrid(lat, lon)
|
|
38
|
+
self._cache = (lat.flatten(), lon.flatten())
|
|
39
|
+
return self._cache
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UnstructuredGrid(Grid):
|
|
43
|
+
def grid_points(self):
|
|
44
|
+
lat = self.lat.variable.values.flatten()
|
|
45
|
+
lon = self.lon.variable.values.flatten()
|
|
46
|
+
return lat, lon
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# (C) Copyright 2024 ECMWF.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
6
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
7
|
+
# nor does it submit to any jurisdiction.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from functools import cached_property
|
|
12
|
+
|
|
13
|
+
from earthkit.data.core.geography import Geography
|
|
14
|
+
from earthkit.data.core.metadata import RawMetadata
|
|
15
|
+
from earthkit.data.utils.dates import to_datetime
|
|
16
|
+
from earthkit.data.utils.projections import Projection
|
|
17
|
+
|
|
18
|
+
LOG = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MDMapping:
|
|
22
|
+
|
|
23
|
+
def __init__(self, mapping):
|
|
24
|
+
self.user_to_internal = mapping
|
|
25
|
+
|
|
26
|
+
def from_user(self, kwargs):
|
|
27
|
+
if isinstance(kwargs, str):
|
|
28
|
+
return self.user_to_internal.get(kwargs, kwargs)
|
|
29
|
+
return {self.user_to_internal.get(k, k): v for k, v in kwargs.items()}
|
|
30
|
+
|
|
31
|
+
def __len__(self):
|
|
32
|
+
return len(self.user_to_internal)
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return f"MDMapping({self.user_to_internal})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class XArrayMetadata(RawMetadata):
|
|
39
|
+
LS_KEYS = ["variable", "level", "valid_datetime", "units"]
|
|
40
|
+
NAMESPACES = ["default", "mars"]
|
|
41
|
+
MARS_KEYS = ["param", "step", "levelist", "levtype", "number", "date", "time"]
|
|
42
|
+
|
|
43
|
+
def __init__(self, field, mapping):
|
|
44
|
+
self._field = field
|
|
45
|
+
md = field._md.copy()
|
|
46
|
+
|
|
47
|
+
self._mapping = mapping
|
|
48
|
+
if mapping is None:
|
|
49
|
+
time_coord = [c for c in field.owner.coordinates if c.is_time]
|
|
50
|
+
if len(time_coord) == 1:
|
|
51
|
+
time_key = time_coord[0].name
|
|
52
|
+
else:
|
|
53
|
+
time_key = "time"
|
|
54
|
+
else:
|
|
55
|
+
time_key = mapping.from_user("valid_datetime")
|
|
56
|
+
self._time = to_datetime(md.pop(time_key))
|
|
57
|
+
self._field.owner.time.fill_time_metadata(self._time, md)
|
|
58
|
+
md["valid_datetime"] = self._time.isoformat()
|
|
59
|
+
|
|
60
|
+
super().__init__(md)
|
|
61
|
+
|
|
62
|
+
@cached_property
|
|
63
|
+
def geography(self):
|
|
64
|
+
return XArrayFieldGeography(self._field, self._field.owner.grid)
|
|
65
|
+
|
|
66
|
+
def as_namespace(self, namespace=None):
|
|
67
|
+
if not isinstance(namespace, str) and namespace is not None:
|
|
68
|
+
raise TypeError("namespace must be a str or None")
|
|
69
|
+
|
|
70
|
+
if namespace == "default" or namespace == "" or namespace is None:
|
|
71
|
+
return dict(self)
|
|
72
|
+
|
|
73
|
+
elif namespace == "mars":
|
|
74
|
+
return self._as_mars()
|
|
75
|
+
|
|
76
|
+
def _as_mars(self):
|
|
77
|
+
return dict(
|
|
78
|
+
param=self["variable"],
|
|
79
|
+
step=self["step"],
|
|
80
|
+
levelist=self["level"],
|
|
81
|
+
levtype=self["levtype"],
|
|
82
|
+
number=self["number"],
|
|
83
|
+
date=self["date"],
|
|
84
|
+
time=self["time"],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _base_datetime(self):
|
|
88
|
+
return self._field.forecast_reference_time
|
|
89
|
+
|
|
90
|
+
def _valid_datetime(self):
|
|
91
|
+
return self._time
|
|
92
|
+
|
|
93
|
+
def _get(self, key, **kwargs):
|
|
94
|
+
|
|
95
|
+
if key.startswith("mars."):
|
|
96
|
+
key = key[5:]
|
|
97
|
+
if key not in self.MARS_KEYS:
|
|
98
|
+
if kwargs.get("raise_on_missing", False):
|
|
99
|
+
raise KeyError(f"Invalid key '{key}' in namespace='mars'")
|
|
100
|
+
else:
|
|
101
|
+
return kwargs.get("default", None)
|
|
102
|
+
|
|
103
|
+
if self._mapping is not None:
|
|
104
|
+
key = self._mapping.from_user(key)
|
|
105
|
+
|
|
106
|
+
return super()._get(key, **kwargs)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class XArrayFieldGeography(Geography):
|
|
110
|
+
def __init__(self, field, grid):
|
|
111
|
+
self._field = field
|
|
112
|
+
self._grid = grid
|
|
113
|
+
|
|
114
|
+
def _unique_grid_id(self):
|
|
115
|
+
raise NotImplementedError()
|
|
116
|
+
|
|
117
|
+
def bounding_box(self):
|
|
118
|
+
raise NotImplementedError()
|
|
119
|
+
# return BoundingBox(north=self.north, south=self.south, east=self.east, west=self.west)
|
|
120
|
+
|
|
121
|
+
def gridspec(self):
|
|
122
|
+
raise NotImplementedError()
|
|
123
|
+
|
|
124
|
+
def latitudes(self, dtype=None):
|
|
125
|
+
result = self._grid.latitudes
|
|
126
|
+
if dtype is not None:
|
|
127
|
+
return result.astype(dtype)
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
def longitudes(self, dtype=None):
|
|
131
|
+
result = self._grid.longitudes
|
|
132
|
+
if dtype is not None:
|
|
133
|
+
return result.astype(dtype)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
def resolution(self):
|
|
137
|
+
# TODO: implement resolution
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def mars_grid(self):
|
|
142
|
+
# TODO: implement mars_grid
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def mars_area(self):
|
|
147
|
+
# TODO: code me
|
|
148
|
+
# return [self.north, self.west, self.south, self.east]
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def x(self, dtype=None):
|
|
152
|
+
raise NotImplementedError()
|
|
153
|
+
|
|
154
|
+
def y(self, dtype=None):
|
|
155
|
+
raise NotImplementedError()
|
|
156
|
+
|
|
157
|
+
def shape(self):
|
|
158
|
+
return self._field.shape
|
|
159
|
+
|
|
160
|
+
def projection(self):
|
|
161
|
+
return Projection.from_cf_grid_mapping(**self._field.grid_mapping)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# (C) Copyright 2024 ECMWF.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
6
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
7
|
+
# nor does it submit to any jurisdiction.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Time:
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_coordinates(cls, coordinates):
|
|
17
|
+
time_coordinate = [c for c in coordinates if c.is_time]
|
|
18
|
+
step_coordinate = [c for c in coordinates if c.is_step]
|
|
19
|
+
date_coordinate = [c for c in coordinates if c.is_date]
|
|
20
|
+
|
|
21
|
+
if len(date_coordinate) == 0 and len(time_coordinate) == 1 and len(step_coordinate) == 1:
|
|
22
|
+
return ForecasstFromValidTimeAndStep(step_coordinate[0])
|
|
23
|
+
|
|
24
|
+
if len(date_coordinate) == 0 and len(time_coordinate) == 1 and len(step_coordinate) == 0:
|
|
25
|
+
return Analysis()
|
|
26
|
+
|
|
27
|
+
if len(date_coordinate) == 0 and len(time_coordinate) == 0 and len(step_coordinate) == 0:
|
|
28
|
+
return Constant()
|
|
29
|
+
|
|
30
|
+
if len(date_coordinate) == 1 and len(time_coordinate) == 1 and len(step_coordinate) == 0:
|
|
31
|
+
return ForecastFromValidTimeAndBaseTime(date_coordinate[0])
|
|
32
|
+
|
|
33
|
+
if len(date_coordinate) == 1 and len(time_coordinate) == 0 and len(step_coordinate) == 1:
|
|
34
|
+
return ForecastFromBaseTimeAndDate(date_coordinate[0], step_coordinate[0])
|
|
35
|
+
|
|
36
|
+
raise NotImplementedError(f"{date_coordinate=} {time_coordinate=} {step_coordinate=}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Constant(Time):
|
|
40
|
+
|
|
41
|
+
def fill_time_metadata(self, time, metadata):
|
|
42
|
+
metadata["date"] = time.strftime("%Y%m%d")
|
|
43
|
+
metadata["time"] = time.strftime("%H%M")
|
|
44
|
+
metadata["step"] = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Analysis(Time):
|
|
48
|
+
|
|
49
|
+
def fill_time_metadata(self, time, metadata):
|
|
50
|
+
metadata["date"] = time.strftime("%Y%m%d")
|
|
51
|
+
metadata["time"] = time.strftime("%H%M")
|
|
52
|
+
metadata["step"] = 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ForecasstFromValidTimeAndStep(Time):
|
|
56
|
+
def __init__(self, step_coordinate):
|
|
57
|
+
self.step_name = step_coordinate.variable.name
|
|
58
|
+
|
|
59
|
+
def fill_time_metadata(self, time, metadata):
|
|
60
|
+
step = metadata.pop(self.step_name)
|
|
61
|
+
assert isinstance(step, datetime.timedelta)
|
|
62
|
+
base = time - step
|
|
63
|
+
|
|
64
|
+
hours = step.total_seconds() / 3600
|
|
65
|
+
assert int(hours) == hours
|
|
66
|
+
|
|
67
|
+
metadata["date"] = base.strftime("%Y%m%d")
|
|
68
|
+
metadata["time"] = base.strftime("%H%M")
|
|
69
|
+
metadata["step"] = int(hours)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ForecastFromValidTimeAndBaseTime(Time):
|
|
73
|
+
def __init__(self, date_coordinate):
|
|
74
|
+
self.date_coordinate = date_coordinate
|
|
75
|
+
|
|
76
|
+
def fill_time_metadata(self, time, metadata):
|
|
77
|
+
|
|
78
|
+
step = time - self.date_coordinate
|
|
79
|
+
|
|
80
|
+
hours = step.total_seconds() / 3600
|
|
81
|
+
assert int(hours) == hours
|
|
82
|
+
|
|
83
|
+
metadata["date"] = self.date_coordinate.single_value.strftime("%Y%m%d")
|
|
84
|
+
metadata["time"] = self.date_coordinate.single_value.strftime("%H%M")
|
|
85
|
+
metadata["step"] = int(hours)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ForecastFromBaseTimeAndDate(Time):
|
|
89
|
+
def __init__(self, date_coordinate, step_coordinate):
|
|
90
|
+
self.date_coordinate = date_coordinate
|
|
91
|
+
self.step_coordinate = step_coordinate
|
|
92
|
+
|
|
93
|
+
def fill_time_metadata(self, time, metadata):
|
|
94
|
+
metadata["date"] = time.strftime("%Y%m%d")
|
|
95
|
+
metadata["time"] = time.strftime("%H%M")
|
|
96
|
+
hours = metadata[self.step_coordinate.name].total_seconds() / 3600
|
|
97
|
+
assert int(hours) == hours
|
|
98
|
+
metadata["step"] = int(hours)
|