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.
Files changed (61) hide show
  1. anemoi/datasets/_version.py +2 -2
  2. anemoi/datasets/commands/compare.py +59 -0
  3. anemoi/datasets/commands/create.py +84 -3
  4. anemoi/datasets/commands/inspect.py +9 -9
  5. anemoi/datasets/commands/scan.py +4 -4
  6. anemoi/datasets/compute/recentre.py +14 -9
  7. anemoi/datasets/create/__init__.py +44 -17
  8. anemoi/datasets/create/check.py +6 -5
  9. anemoi/datasets/create/chunks.py +1 -1
  10. anemoi/datasets/create/config.py +6 -27
  11. anemoi/datasets/create/functions/__init__.py +3 -3
  12. anemoi/datasets/create/functions/filters/empty.py +4 -4
  13. anemoi/datasets/create/functions/filters/rename.py +14 -6
  14. anemoi/datasets/create/functions/filters/rotate_winds.py +16 -60
  15. anemoi/datasets/create/functions/filters/unrotate_winds.py +14 -64
  16. anemoi/datasets/create/functions/sources/__init__.py +39 -0
  17. anemoi/datasets/create/functions/sources/accumulations.py +38 -56
  18. anemoi/datasets/create/functions/sources/constants.py +11 -4
  19. anemoi/datasets/create/functions/sources/empty.py +2 -2
  20. anemoi/datasets/create/functions/sources/forcings.py +3 -3
  21. anemoi/datasets/create/functions/sources/grib.py +8 -4
  22. anemoi/datasets/create/functions/sources/hindcasts.py +32 -364
  23. anemoi/datasets/create/functions/sources/mars.py +57 -26
  24. anemoi/datasets/create/functions/sources/netcdf.py +2 -60
  25. anemoi/datasets/create/functions/sources/opendap.py +3 -2
  26. anemoi/datasets/create/functions/sources/source.py +3 -3
  27. anemoi/datasets/create/functions/sources/tendencies.py +7 -7
  28. anemoi/datasets/create/functions/sources/xarray/__init__.py +73 -0
  29. anemoi/datasets/create/functions/sources/xarray/coordinates.py +234 -0
  30. anemoi/datasets/create/functions/sources/xarray/field.py +109 -0
  31. anemoi/datasets/create/functions/sources/xarray/fieldlist.py +171 -0
  32. anemoi/datasets/create/functions/sources/xarray/flavour.py +330 -0
  33. anemoi/datasets/create/functions/sources/xarray/grid.py +46 -0
  34. anemoi/datasets/create/functions/sources/xarray/metadata.py +161 -0
  35. anemoi/datasets/create/functions/sources/xarray/time.py +98 -0
  36. anemoi/datasets/create/functions/sources/xarray/variable.py +198 -0
  37. anemoi/datasets/create/functions/sources/xarray_kerchunk.py +42 -0
  38. anemoi/datasets/create/functions/sources/xarray_zarr.py +15 -0
  39. anemoi/datasets/create/functions/sources/zenodo.py +40 -0
  40. anemoi/datasets/create/input.py +309 -191
  41. anemoi/datasets/create/loaders.py +155 -77
  42. anemoi/datasets/create/patch.py +17 -14
  43. anemoi/datasets/create/persistent.py +1 -1
  44. anemoi/datasets/create/size.py +4 -5
  45. anemoi/datasets/create/statistics/__init__.py +51 -17
  46. anemoi/datasets/create/template.py +11 -61
  47. anemoi/datasets/create/trace.py +91 -0
  48. anemoi/datasets/create/utils.py +5 -52
  49. anemoi/datasets/create/zarr.py +24 -10
  50. anemoi/datasets/data/dataset.py +4 -4
  51. anemoi/datasets/data/misc.py +9 -37
  52. anemoi/datasets/data/stores.py +37 -14
  53. anemoi/datasets/dates/__init__.py +7 -1
  54. anemoi/datasets/dates/groups.py +3 -0
  55. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/METADATA +24 -8
  56. anemoi_datasets-0.4.2.dist-info/RECORD +86 -0
  57. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/WHEEL +1 -1
  58. anemoi_datasets-0.3.10.dist-info/RECORD +0 -73
  59. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/LICENSE +0 -0
  60. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/entry_points.txt +0 -0
  61. {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)