anemoi-datasets 0.4.0__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 (51) 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 +3 -3
  5. anemoi/datasets/create/__init__.py +44 -17
  6. anemoi/datasets/create/check.py +6 -5
  7. anemoi/datasets/create/chunks.py +1 -1
  8. anemoi/datasets/create/config.py +5 -26
  9. anemoi/datasets/create/functions/filters/rename.py +9 -1
  10. anemoi/datasets/create/functions/filters/rotate_winds.py +10 -1
  11. anemoi/datasets/create/functions/sources/__init__.py +39 -0
  12. anemoi/datasets/create/functions/sources/accumulations.py +11 -41
  13. anemoi/datasets/create/functions/sources/constants.py +3 -0
  14. anemoi/datasets/create/functions/sources/grib.py +4 -0
  15. anemoi/datasets/create/functions/sources/hindcasts.py +32 -377
  16. anemoi/datasets/create/functions/sources/mars.py +53 -22
  17. anemoi/datasets/create/functions/sources/netcdf.py +2 -60
  18. anemoi/datasets/create/functions/sources/opendap.py +3 -2
  19. anemoi/datasets/create/functions/sources/xarray/__init__.py +73 -0
  20. anemoi/datasets/create/functions/sources/xarray/coordinates.py +234 -0
  21. anemoi/datasets/create/functions/sources/xarray/field.py +109 -0
  22. anemoi/datasets/create/functions/sources/xarray/fieldlist.py +171 -0
  23. anemoi/datasets/create/functions/sources/xarray/flavour.py +330 -0
  24. anemoi/datasets/create/functions/sources/xarray/grid.py +46 -0
  25. anemoi/datasets/create/functions/sources/xarray/metadata.py +161 -0
  26. anemoi/datasets/create/functions/sources/xarray/time.py +98 -0
  27. anemoi/datasets/create/functions/sources/xarray/variable.py +198 -0
  28. anemoi/datasets/create/functions/sources/xarray_kerchunk.py +42 -0
  29. anemoi/datasets/create/functions/sources/xarray_zarr.py +15 -0
  30. anemoi/datasets/create/functions/sources/zenodo.py +40 -0
  31. anemoi/datasets/create/input.py +290 -172
  32. anemoi/datasets/create/loaders.py +120 -71
  33. anemoi/datasets/create/patch.py +17 -14
  34. anemoi/datasets/create/persistent.py +1 -1
  35. anemoi/datasets/create/size.py +4 -5
  36. anemoi/datasets/create/statistics/__init__.py +49 -16
  37. anemoi/datasets/create/template.py +11 -61
  38. anemoi/datasets/create/trace.py +91 -0
  39. anemoi/datasets/create/utils.py +0 -48
  40. anemoi/datasets/create/zarr.py +24 -10
  41. anemoi/datasets/data/misc.py +9 -37
  42. anemoi/datasets/data/stores.py +29 -14
  43. anemoi/datasets/dates/__init__.py +7 -1
  44. anemoi/datasets/dates/groups.py +3 -0
  45. {anemoi_datasets-0.4.0.dist-info → anemoi_datasets-0.4.2.dist-info}/METADATA +18 -3
  46. anemoi_datasets-0.4.2.dist-info/RECORD +86 -0
  47. {anemoi_datasets-0.4.0.dist-info → anemoi_datasets-0.4.2.dist-info}/WHEEL +1 -1
  48. anemoi_datasets-0.4.0.dist-info/RECORD +0 -73
  49. {anemoi_datasets-0.4.0.dist-info → anemoi_datasets-0.4.2.dist-info}/LICENSE +0 -0
  50. {anemoi_datasets-0.4.0.dist-info → anemoi_datasets-0.4.2.dist-info}/entry_points.txt +0 -0
  51. {anemoi_datasets-0.4.0.dist-info → anemoi_datasets-0.4.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,171 @@
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 json
11
+ import logging
12
+
13
+ import yaml
14
+ from earthkit.data.core.fieldlist import FieldList
15
+
16
+ from .coordinates import is_scalar as is_scalar
17
+ from .field import EmptyFieldList
18
+ from .flavour import CoordinateGuesser
19
+ from .metadata import XArrayMetadata as XArrayMetadata
20
+ from .time import Time
21
+ from .variable import FilteredVariable
22
+ from .variable import Variable
23
+
24
+ LOG = logging.getLogger(__name__)
25
+
26
+
27
+ class XarrayFieldList(FieldList):
28
+ def __init__(self, ds, variables):
29
+ self.ds = ds
30
+ self.variables = variables.copy()
31
+ self.total_length = sum(v.length for v in variables)
32
+
33
+ def __repr__(self):
34
+ return f"XarrayFieldList({self.total_length})"
35
+
36
+ def __len__(self):
37
+ return self.total_length
38
+
39
+ def __getitem__(self, i):
40
+ k = i
41
+
42
+ if i < 0:
43
+ i = self.total_length + i
44
+
45
+ for v in self.variables:
46
+ if i < v.length:
47
+ return v[i]
48
+ i -= v.length
49
+
50
+ raise IndexError(k)
51
+
52
+ @classmethod
53
+ def from_xarray(cls, ds, flavour=None):
54
+ variables = []
55
+
56
+ if isinstance(flavour, str):
57
+ with open(flavour) as f:
58
+ if flavour.endswith(".yaml") or flavour.endswith(".yml"):
59
+ flavour = yaml.safe_load(f)
60
+ else:
61
+ flavour = json.load(f)
62
+
63
+ guess = CoordinateGuesser.from_flavour(ds, flavour)
64
+
65
+ skip = set()
66
+
67
+ def _skip_attr(v, attr_name):
68
+ attr_val = getattr(v, attr_name, "")
69
+ if isinstance(attr_val, str):
70
+ skip.update(attr_val.split(" "))
71
+
72
+ for name in ds.data_vars:
73
+ v = ds[name]
74
+ _skip_attr(v, "coordinates")
75
+ _skip_attr(v, "bounds")
76
+ _skip_attr(v, "grid_mapping")
77
+
78
+ # Select only geographical variables
79
+ for name in ds.data_vars:
80
+
81
+ if name in skip:
82
+ continue
83
+
84
+ v = ds[name]
85
+ coordinates = []
86
+
87
+ for coord in v.coords:
88
+
89
+ c = guess.guess(ds[coord], coord)
90
+ assert c, f"Could not guess coordinate for {coord}"
91
+ if coord not in v.dims:
92
+ c.is_dim = False
93
+ coordinates.append(c)
94
+
95
+ grid_coords = sum(1 for c in coordinates if c.is_grid and c.is_dim)
96
+ assert grid_coords <= 2
97
+
98
+ if grid_coords < 2:
99
+ continue
100
+
101
+ variables.append(
102
+ Variable(
103
+ ds=ds,
104
+ var=v,
105
+ coordinates=coordinates,
106
+ grid=guess.grid(coordinates),
107
+ time=Time.from_coordinates(coordinates),
108
+ metadata={},
109
+ )
110
+ )
111
+
112
+ return cls(ds, variables)
113
+
114
+ def sel(self, **kwargs):
115
+ """Override the FieldList's sel method
116
+
117
+ Returns
118
+ -------
119
+ FieldList
120
+ The new FieldList
121
+
122
+ The algorithm is as follows:
123
+ 1 - Use the kwargs to select the variables that match the selection (`param` or `variable`)
124
+ 2 - For each variable, use the remaining kwargs to select the coordinates (`level`, `number`, ...)
125
+ 3 - Some mars like keys, like `date`, `time`, `step` are not found in the coordinates,
126
+ but added to the metadata of the selected fields. A example is `step` that is added to the
127
+ metadata of the field. Step 2 may return a variable that contain all the fields that
128
+ verify at the same `valid_datetime`, with different base `date` and `time` and a different `step`.
129
+ So we get an extra chance to filter the fields by the metadata.
130
+ """
131
+
132
+ variables = []
133
+ count = 0
134
+
135
+ for v in self.variables:
136
+
137
+ v.update_metadata_mapping(kwargs)
138
+
139
+ # First, select matching variables
140
+ # This will consume 'param' or 'variable' from kwargs
141
+ # and return the rest
142
+ match, rest = v.match(**kwargs)
143
+
144
+ if match:
145
+ count += 1
146
+ missing = {}
147
+
148
+ # Select from the variable's coordinates (time, level, number, ....)
149
+ # This may return a new variable with a isel() slice of the selection
150
+ # or None if the selection is not possible. In this case, missing is updated
151
+ # with the values of kwargs (rest) that are not relevant for this variable
152
+ v = v.sel(missing, **rest)
153
+ if missing:
154
+ if v is not None:
155
+ # The remaining kwargs are passed used to create a FilteredVariable
156
+ # that will select 2D slices based on their metadata
157
+ v = FilteredVariable(v, **missing)
158
+ else:
159
+ LOG.warning(f"Variable {v} has missing coordinates: {missing}")
160
+
161
+ if v is not None:
162
+ variables.append(v)
163
+
164
+ if count == 0:
165
+ LOG.warning("No variable found for %s", kwargs)
166
+ LOG.warning("Variables: %s", sorted([v.name for v in self.variables]))
167
+
168
+ if not variables:
169
+ return EmptyFieldList()
170
+
171
+ return self.__class__(self.ds, variables)
@@ -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)