anemoi-datasets 0.5.26__py3-none-any.whl → 0.5.27__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 (105) hide show
  1. anemoi/datasets/__init__.py +1 -2
  2. anemoi/datasets/_version.py +16 -3
  3. anemoi/datasets/commands/check.py +1 -1
  4. anemoi/datasets/commands/copy.py +1 -2
  5. anemoi/datasets/commands/create.py +1 -1
  6. anemoi/datasets/commands/inspect.py +27 -35
  7. anemoi/datasets/commands/validate.py +59 -0
  8. anemoi/datasets/compute/recentre.py +3 -6
  9. anemoi/datasets/create/__init__.py +22 -25
  10. anemoi/datasets/create/check.py +10 -12
  11. anemoi/datasets/create/chunks.py +1 -2
  12. anemoi/datasets/create/config.py +3 -6
  13. anemoi/datasets/create/filter.py +1 -2
  14. anemoi/datasets/create/input/__init__.py +1 -2
  15. anemoi/datasets/create/input/action.py +3 -5
  16. anemoi/datasets/create/input/concat.py +5 -8
  17. anemoi/datasets/create/input/context.py +3 -6
  18. anemoi/datasets/create/input/data_sources.py +5 -8
  19. anemoi/datasets/create/input/empty.py +1 -2
  20. anemoi/datasets/create/input/filter.py +2 -3
  21. anemoi/datasets/create/input/function.py +1 -2
  22. anemoi/datasets/create/input/join.py +4 -5
  23. anemoi/datasets/create/input/misc.py +4 -6
  24. anemoi/datasets/create/input/repeated_dates.py +13 -18
  25. anemoi/datasets/create/input/result.py +29 -33
  26. anemoi/datasets/create/input/step.py +4 -8
  27. anemoi/datasets/create/input/template.py +3 -4
  28. anemoi/datasets/create/input/trace.py +1 -1
  29. anemoi/datasets/create/patch.py +1 -2
  30. anemoi/datasets/create/persistent.py +3 -5
  31. anemoi/datasets/create/size.py +1 -3
  32. anemoi/datasets/create/sources/accumulations.py +47 -52
  33. anemoi/datasets/create/sources/accumulations2.py +4 -8
  34. anemoi/datasets/create/sources/constants.py +1 -3
  35. anemoi/datasets/create/sources/empty.py +1 -2
  36. anemoi/datasets/create/sources/fdb.py +133 -0
  37. anemoi/datasets/create/sources/forcings.py +1 -2
  38. anemoi/datasets/create/sources/grib.py +6 -10
  39. anemoi/datasets/create/sources/grib_index.py +13 -15
  40. anemoi/datasets/create/sources/hindcasts.py +2 -5
  41. anemoi/datasets/create/sources/legacy.py +1 -1
  42. anemoi/datasets/create/sources/mars.py +17 -21
  43. anemoi/datasets/create/sources/netcdf.py +1 -2
  44. anemoi/datasets/create/sources/opendap.py +1 -3
  45. anemoi/datasets/create/sources/patterns.py +4 -6
  46. anemoi/datasets/create/sources/recentre.py +8 -11
  47. anemoi/datasets/create/sources/source.py +3 -6
  48. anemoi/datasets/create/sources/tendencies.py +2 -5
  49. anemoi/datasets/create/sources/xarray.py +4 -6
  50. anemoi/datasets/create/sources/xarray_support/__init__.py +12 -13
  51. anemoi/datasets/create/sources/xarray_support/coordinates.py +8 -12
  52. anemoi/datasets/create/sources/xarray_support/field.py +16 -12
  53. anemoi/datasets/create/sources/xarray_support/fieldlist.py +11 -15
  54. anemoi/datasets/create/sources/xarray_support/flavour.py +42 -42
  55. anemoi/datasets/create/sources/xarray_support/grid.py +15 -9
  56. anemoi/datasets/create/sources/xarray_support/metadata.py +19 -128
  57. anemoi/datasets/create/sources/xarray_support/patch.py +4 -6
  58. anemoi/datasets/create/sources/xarray_support/time.py +10 -13
  59. anemoi/datasets/create/sources/xarray_support/variable.py +21 -21
  60. anemoi/datasets/create/sources/xarray_zarr.py +1 -2
  61. anemoi/datasets/create/sources/zenodo.py +3 -5
  62. anemoi/datasets/create/statistics/__init__.py +3 -6
  63. anemoi/datasets/create/testing.py +4 -0
  64. anemoi/datasets/create/typing.py +1 -2
  65. anemoi/datasets/create/utils.py +1 -2
  66. anemoi/datasets/create/zarr.py +7 -2
  67. anemoi/datasets/data/__init__.py +15 -6
  68. anemoi/datasets/data/complement.py +7 -12
  69. anemoi/datasets/data/concat.py +5 -8
  70. anemoi/datasets/data/dataset.py +42 -47
  71. anemoi/datasets/data/debug.py +7 -9
  72. anemoi/datasets/data/ensemble.py +4 -6
  73. anemoi/datasets/data/fill_missing.py +7 -10
  74. anemoi/datasets/data/forwards.py +22 -26
  75. anemoi/datasets/data/grids.py +12 -16
  76. anemoi/datasets/data/indexing.py +9 -12
  77. anemoi/datasets/data/interpolate.py +7 -15
  78. anemoi/datasets/data/join.py +8 -12
  79. anemoi/datasets/data/masked.py +6 -11
  80. anemoi/datasets/data/merge.py +5 -9
  81. anemoi/datasets/data/misc.py +41 -45
  82. anemoi/datasets/data/missing.py +11 -16
  83. anemoi/datasets/data/observations/__init__.py +8 -14
  84. anemoi/datasets/data/padded.py +3 -5
  85. anemoi/datasets/data/records/backends/__init__.py +2 -2
  86. anemoi/datasets/data/rescale.py +5 -12
  87. anemoi/datasets/data/select.py +13 -16
  88. anemoi/datasets/data/statistics.py +4 -7
  89. anemoi/datasets/data/stores.py +16 -21
  90. anemoi/datasets/data/subset.py +8 -11
  91. anemoi/datasets/data/unchecked.py +7 -11
  92. anemoi/datasets/data/xy.py +25 -21
  93. anemoi/datasets/dates/__init__.py +13 -18
  94. anemoi/datasets/dates/groups.py +7 -10
  95. anemoi/datasets/grids.py +5 -9
  96. anemoi/datasets/testing.py +93 -7
  97. anemoi/datasets/validate.py +598 -0
  98. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.27.dist-info}/METADATA +4 -4
  99. anemoi_datasets-0.5.27.dist-info/RECORD +134 -0
  100. anemoi/datasets/utils/__init__.py +0 -8
  101. anemoi_datasets-0.5.26.dist-info/RECORD +0 -131
  102. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.27.dist-info}/WHEEL +0 -0
  103. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.27.dist-info}/entry_points.txt +0 -0
  104. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.27.dist-info}/licenses/LICENSE +0 -0
  105. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.27.dist-info}/top_level.txt +0 -0
@@ -10,10 +10,6 @@
10
10
  import datetime
11
11
  import logging
12
12
  from typing import Any
13
- from typing import Dict
14
- from typing import List
15
- from typing import Optional
16
- from typing import Union
17
13
 
18
14
  import earthkit.data as ekd
19
15
  import xarray as xr
@@ -27,7 +23,7 @@ from .fieldlist import XarrayFieldList
27
23
  LOG = logging.getLogger(__name__)
28
24
 
29
25
 
30
- def check(what: str, ds: xr.Dataset, paths: List[str], **kwargs: Any) -> None:
26
+ def check(what: str, ds: xr.Dataset, paths: list[str], **kwargs: Any) -> None:
31
27
  """Checks if the dataset has the expected number of fields.
32
28
 
33
29
  Parameters
@@ -53,12 +49,12 @@ def check(what: str, ds: xr.Dataset, paths: List[str], **kwargs: Any) -> None:
53
49
  def load_one(
54
50
  emoji: str,
55
51
  context: Any,
56
- dates: List[str],
57
- dataset: Union[str, xr.Dataset],
52
+ dates: list[str],
53
+ dataset: str | xr.Dataset,
58
54
  *,
59
- options: Optional[Dict[str, Any]] = None,
60
- flavour: Optional[str] = None,
61
- patch: Optional[Any] = None,
55
+ options: dict[str, Any] | None = None,
56
+ flavour: str | None = None,
57
+ patch: Any | None = None,
62
58
  **kwargs: Any,
63
59
  ) -> ekd.FieldList:
64
60
  """Loads a single dataset.
@@ -97,7 +93,10 @@ def load_one(
97
93
  # If the dataset is a zarr store, we need to use the zarr engine
98
94
  options["engine"] = "zarr"
99
95
 
100
- data = xr.open_dataset(dataset, **options)
96
+ if isinstance(dataset, xr.Dataset):
97
+ data = dataset
98
+ else:
99
+ data = xr.open_dataset(dataset, **options)
101
100
 
102
101
  fs = XarrayFieldList.from_xarray(data, flavour=flavour, patch=patch)
103
102
 
@@ -124,7 +123,7 @@ def load_one(
124
123
  return result
125
124
 
126
125
 
127
- def load_many(emoji: str, context: Any, dates: List[datetime.datetime], pattern: str, **kwargs: Any) -> ekd.FieldList:
126
+ def load_many(emoji: str, context: Any, dates: list[datetime.datetime], pattern: str, **kwargs: Any) -> ekd.FieldList:
128
127
  """Loads multiple datasets.
129
128
 
130
129
  Parameters
@@ -154,7 +153,7 @@ def load_many(emoji: str, context: Any, dates: List[datetime.datetime], pattern:
154
153
 
155
154
 
156
155
  @legacy_source("xarray")
157
- def execute(context: Any, dates: List[str], url: str, *args: Any, **kwargs: Any) -> ekd.FieldList:
156
+ def execute(context: Any, dates: list[str], url: str, *args: Any, **kwargs: Any) -> ekd.FieldList:
158
157
  """Executes the loading of datasets.
159
158
 
160
159
  Parameters
@@ -13,10 +13,6 @@ from __future__ import annotations
13
13
  import datetime
14
14
  import logging
15
15
  from typing import Any
16
- from typing import Dict
17
- from typing import Optional
18
- from typing import Tuple
19
- from typing import Union
20
16
 
21
17
  import numpy as np
22
18
  import xarray as xr
@@ -107,7 +103,7 @@ class Coordinate:
107
103
  """
108
104
  self.variable = variable
109
105
  self.scalar = is_scalar(variable)
110
- self.kwargs: Dict[str, Any] = {} # Used when creating a new coordinate (reduced method)
106
+ self.kwargs: dict[str, Any] = {} # Used when creating a new coordinate (reduced method)
111
107
 
112
108
  def __len__(self) -> int:
113
109
  """Get the length of the coordinate.
@@ -127,7 +123,7 @@ class Coordinate:
127
123
  str
128
124
  The string representation of the coordinate.
129
125
  """
130
- return "%s[name=%s,values=%s,shape=%s]" % (
126
+ return "{}[name={},values={},shape={}]".format(
131
127
  self.__class__.__name__,
132
128
  self.variable.name,
133
129
  self.variable.values if self.scalar else len(self),
@@ -152,7 +148,7 @@ class Coordinate:
152
148
  **self.kwargs,
153
149
  )
154
150
 
155
- def index(self, value: Union[Any, list, tuple]) -> Optional[Union[int, list]]:
151
+ def index(self, value: Any | list | tuple) -> int | list | None:
156
152
  """Return the index of the value in the coordinate.
157
153
 
158
154
  Parameters
@@ -172,7 +168,7 @@ class Coordinate:
172
168
  return self._index_multiple(value)
173
169
  return self._index_single(value)
174
170
 
175
- def _index_single(self, value: Any) -> Optional[int]:
171
+ def _index_single(self, value: Any) -> int | None:
176
172
  """Return the index of a single value in the coordinate.
177
173
 
178
174
  Parameters
@@ -205,7 +201,7 @@ class Coordinate:
205
201
 
206
202
  return None
207
203
 
208
- def _index_multiple(self, value: list) -> Optional[list]:
204
+ def _index_multiple(self, value: list) -> list | None:
209
205
  """Return the indices of multiple values in the coordinate.
210
206
 
211
207
  Parameters
@@ -275,7 +271,7 @@ class TimeCoordinate(Coordinate):
275
271
  is_time = True
276
272
  mars_names = ("valid_datetime",)
277
273
 
278
- def index(self, time: datetime.datetime) -> Optional[int]:
274
+ def index(self, time: datetime.datetime) -> int | None:
279
275
  """Return the index of the time in the coordinate.
280
276
 
281
277
  Parameters
@@ -297,7 +293,7 @@ class DateCoordinate(Coordinate):
297
293
  is_date = True
298
294
  mars_names = ("date",)
299
295
 
300
- def index(self, date: datetime.datetime) -> Optional[int]:
296
+ def index(self, date: datetime.datetime) -> int | None:
301
297
  """Return the index of the date in the coordinate.
302
298
 
303
299
  Parameters
@@ -436,7 +432,7 @@ class ScalarCoordinate(Coordinate):
436
432
  is_grid = False
437
433
 
438
434
  @property
439
- def mars_names(self) -> Tuple[str, ...]:
435
+ def mars_names(self) -> tuple[str, ...]:
440
436
  """Get the MARS names for the coordinate."""
441
437
  return (self.variable.name,)
442
438
 
@@ -12,9 +12,6 @@ import datetime
12
12
  import logging
13
13
  from functools import cached_property
14
14
  from typing import Any
15
- from typing import Dict
16
- from typing import Optional
17
- from typing import Tuple
18
15
 
19
16
  from earthkit.data import Field
20
17
  from earthkit.data.core.fieldlist import math
@@ -80,12 +77,21 @@ class XArrayField(Field):
80
77
  # Copy the metadata from the owner
81
78
  self._md = owner._metadata.copy()
82
79
 
80
+ aliases = {}
83
81
  for coord_name, coord_value in self.selection.coords.items():
84
82
  if is_scalar(coord_value):
85
83
  # Extract the single value from the scalar dimension
86
84
  # and store it in the metadata
87
85
  coordinate = owner.by_name[coord_name]
88
- self._md[coord_name] = coordinate.normalise(extract_single_value(coord_value))
86
+ normalised = coordinate.normalise(extract_single_value(coord_value))
87
+ self._md[coord_name] = normalised
88
+ for alias in coordinate.mars_names:
89
+ aliases[alias] = normalised
90
+
91
+ # Add metadata aliases (e.g. levelist == level) only if they are not already present
92
+ for alias, value in aliases.items():
93
+ if alias not in self._md:
94
+ self._md[alias] = value
89
95
 
90
96
  # By now, the only dimensions should be latitude and longitude
91
97
  self._shape = tuple(list(self.selection.shape)[-2:])
@@ -93,13 +99,11 @@ class XArrayField(Field):
93
99
  raise ValueError(f"Invalid shape for selection {self._shape=}, {self.selection.shape=} {self.selection=}")
94
100
 
95
101
  @property
96
- def shape(self) -> Tuple[int, int]:
102
+ def shape(self) -> tuple[int, int]:
97
103
  """Return the shape of the field."""
98
104
  return self._shape
99
105
 
100
- def to_numpy(
101
- self, flatten: bool = False, dtype: Optional[type] = None, index: Optional[int] = None
102
- ) -> NDArray[Any]:
106
+ def to_numpy(self, flatten: bool = False, dtype: type | None = None, index: int | None = None) -> NDArray[Any]:
103
107
  """Convert the selection to a numpy array.
104
108
 
105
109
  Returns
@@ -137,7 +141,7 @@ class XArrayField(Field):
137
141
  """Return the grid points of the field."""
138
142
  return self.owner.grid_points()
139
143
 
140
- def to_latlon(self, flatten: bool = True) -> Dict[str, Any]:
144
+ def to_latlon(self, flatten: bool = True) -> dict[str, Any]:
141
145
  """Convert the selection to latitude and longitude coordinates.
142
146
 
143
147
  Returns
@@ -154,7 +158,7 @@ class XArrayField(Field):
154
158
  return dict(lat=self.latitudes, lon=self.longitudes)
155
159
 
156
160
  @property
157
- def resolution(self) -> Optional[Any]:
161
+ def resolution(self) -> Any | None:
158
162
  """Return the resolution of the field."""
159
163
  return None
160
164
 
@@ -185,9 +189,9 @@ class XArrayField(Field):
185
189
 
186
190
  def __repr__(self) -> str:
187
191
  """Return a string representation of the field."""
188
- return repr(self._metadata)
192
+ return f"XArrayField({self._metadata})"
189
193
 
190
- def _values(self, dtype: Optional[type] = None) -> Any:
194
+ def _values(self, dtype: type | None = None) -> Any:
191
195
  """Return the values of the selection.
192
196
 
193
197
  Returns
@@ -11,10 +11,6 @@
11
11
  import json
12
12
  import logging
13
13
  from typing import Any
14
- from typing import Dict
15
- from typing import List
16
- from typing import Optional
17
- from typing import Union
18
14
 
19
15
  import xarray as xr
20
16
  import yaml
@@ -33,7 +29,7 @@ LOG = logging.getLogger(__name__)
33
29
  class XarrayFieldList(FieldList):
34
30
  """A class to represent a list of fields from an xarray Dataset."""
35
31
 
36
- def __init__(self, ds: xr.Dataset, variables: List[Variable]) -> None:
32
+ def __init__(self, ds: xr.Dataset, variables: list[Variable]) -> None:
37
33
  """Initialize the XarrayFieldList.
38
34
 
39
35
  Parameters
@@ -44,7 +40,7 @@ class XarrayFieldList(FieldList):
44
40
  The list of variables.
45
41
  """
46
42
  self.ds: xr.Dataset = ds
47
- self.variables: List[Variable] = variables.copy()
43
+ self.variables: list[Variable] = variables.copy()
48
44
  self.total_length: int = sum(v.length for v in variables)
49
45
 
50
46
  def __repr__(self) -> str:
@@ -90,8 +86,8 @@ class XarrayFieldList(FieldList):
90
86
  cls,
91
87
  ds: xr.Dataset,
92
88
  *,
93
- flavour: Optional[Union[str, Dict[str, Any]]] = None,
94
- patch: Optional[Dict[str, Any]] = None,
89
+ flavour: str | dict[str, Any] | None = None,
90
+ patch: dict[str, Any] | None = None,
95
91
  ) -> "XarrayFieldList":
96
92
  """Create an XarrayFieldList from an xarray Dataset.
97
93
 
@@ -112,7 +108,7 @@ class XarrayFieldList(FieldList):
112
108
  if patch is not None:
113
109
  ds = patch_dataset(ds, patch)
114
110
 
115
- variables: List[Variable] = []
111
+ variables: list[Variable] = []
116
112
 
117
113
  if isinstance(flavour, str):
118
114
  with open(flavour) as f:
@@ -121,9 +117,9 @@ class XarrayFieldList(FieldList):
121
117
  else:
122
118
  flavour = json.load(f)
123
119
 
124
- if isinstance(flavour, Dict):
125
- flavour_coords: List[str] = [coords["name"] for coords in flavour["rules"].values()]
126
- ds_dims: List[str] = [dim for dim in ds._dims]
120
+ if isinstance(flavour, dict):
121
+ flavour_coords: list[str] = [coords["name"] for coords in flavour["rules"].values()]
122
+ ds_dims: list[str] = [dim for dim in ds._dims]
127
123
  for dim in ds_dims:
128
124
  if dim in flavour_coords and dim not in ds._coord_names:
129
125
  ds = ds.assign_coords({dim: ds[dim]})
@@ -154,7 +150,7 @@ class XarrayFieldList(FieldList):
154
150
  continue
155
151
 
156
152
  variable = ds[name]
157
- coordinates: List[Any] = []
153
+ coordinates: list[Any] = []
158
154
 
159
155
  for coord in variable.coords:
160
156
 
@@ -210,7 +206,7 @@ class XarrayFieldList(FieldList):
210
206
  So we get an extra chance to filter the fields by the metadata.
211
207
  """
212
208
 
213
- variables: List[Variable] = []
209
+ variables: list[Variable] = []
214
210
  count: int = 0
215
211
 
216
212
  for v in self.variables:
@@ -223,7 +219,7 @@ class XarrayFieldList(FieldList):
223
219
 
224
220
  if match:
225
221
  count += 1
226
- missing: Dict[str, Any] = {}
222
+ missing: dict[str, Any] = {}
227
223
 
228
224
  # Select from the variable's coordinates (time, level, number, ....)
229
225
  # This may return a new variable with a isel() slice of the selection
@@ -11,11 +11,8 @@
11
11
  import logging
12
12
  from abc import ABC
13
13
  from abc import abstractmethod
14
+ from collections.abc import Hashable
14
15
  from typing import Any
15
- from typing import Dict
16
- from typing import Hashable
17
- from typing import Optional
18
- from typing import Tuple
19
16
 
20
17
  import xarray as xr
21
18
  from anemoi.utils.config import DotDict
@@ -61,11 +58,11 @@ class CoordinateGuesser(ABC):
61
58
  The dataset to guess coordinates from.
62
59
  """
63
60
  self.ds = ds
64
- self._coordinate_cache: Dict[Hashable, Coordinate] = {}
65
- self._grid_cache: Dict[Hashable, Grid] = {}
61
+ self._coordinate_cache: dict[Hashable, Coordinate] = {}
62
+ self._grid_cache: dict[Hashable, Grid] = {}
66
63
 
67
64
  @classmethod
68
- def from_flavour(cls, ds: xr.Dataset, flavour: Optional[Dict[str, Any]]) -> "CoordinateGuesser":
65
+ def from_flavour(cls, ds: xr.Dataset, flavour: dict[str, Any] | None) -> "CoordinateGuesser":
69
66
  """Creates a CoordinateGuesser from a flavour.
70
67
 
71
68
  Parameters
@@ -133,7 +130,7 @@ class CoordinateGuesser(ABC):
133
130
  units=units,
134
131
  )
135
132
 
136
- d: Optional[Coordinate] = None
133
+ d: Coordinate | None = None
137
134
 
138
135
  d = self._is_point(coordinate, attributes)
139
136
  if d is not None:
@@ -214,7 +211,7 @@ class CoordinateGuesser(ABC):
214
211
 
215
212
  raise NotImplementedError(f"Cannot establish grid {coordinates}")
216
213
 
217
- def _check_dims(self, variable: Any, x_or_lon: Any, y_or_lat: Any) -> Tuple[Any, bool]:
214
+ def _check_dims(self, variable: Any, x_or_lon: Any, y_or_lat: Any) -> tuple[Any, bool]:
218
215
  """Checks the dimensions of the variable against the coordinates.
219
216
 
220
217
  Parameters
@@ -362,7 +359,7 @@ class CoordinateGuesser(ABC):
362
359
  grid_mapping = self.ds.attrs["crs"]
363
360
  LOG.warning(f"Using CRS {grid_mapping} from global attributes")
364
361
 
365
- grid: Optional[Grid] = None
362
+ grid: Grid | None = None
366
363
  if grid_mapping is not None:
367
364
 
368
365
  grid_mapping = dict(self.ds[grid_mapping].attrs)
@@ -380,7 +377,7 @@ class CoordinateGuesser(ABC):
380
377
  raise NotImplementedError(f"Unstructured grid {x.name} {y.name}")
381
378
 
382
379
  @abstractmethod
383
- def _is_longitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LongitudeCoordinate]:
380
+ def _is_longitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LongitudeCoordinate | None:
384
381
  """Checks if the coordinate is a longitude.
385
382
 
386
383
  Parameters
@@ -398,11 +395,11 @@ class CoordinateGuesser(ABC):
398
395
  pass
399
396
 
400
397
  @abstractmethod
401
- def _is_point(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[PointCoordinate]:
398
+ def _is_point(self, c: xr.DataArray, attributes: CoordinateAttributes) -> PointCoordinate | None:
402
399
  pass
403
400
 
404
401
  @abstractmethod
405
- def _is_latitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LatitudeCoordinate]:
402
+ def _is_latitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LatitudeCoordinate | None:
406
403
  """Checks if the coordinate is a latitude.
407
404
 
408
405
  Parameters
@@ -420,7 +417,7 @@ class CoordinateGuesser(ABC):
420
417
  pass
421
418
 
422
419
  @abstractmethod
423
- def _is_x(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[XCoordinate]:
420
+ def _is_x(self, c: xr.DataArray, attributes: CoordinateAttributes) -> XCoordinate | None:
424
421
  """Checks if the coordinate is an x coordinate.
425
422
 
426
423
  Parameters
@@ -438,7 +435,7 @@ class CoordinateGuesser(ABC):
438
435
  pass
439
436
 
440
437
  @abstractmethod
441
- def _is_y(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[YCoordinate]:
438
+ def _is_y(self, c: xr.DataArray, attributes: CoordinateAttributes) -> YCoordinate | None:
442
439
  """Checks if the coordinate is a y coordinate.
443
440
 
444
441
  Parameters
@@ -456,7 +453,7 @@ class CoordinateGuesser(ABC):
456
453
  pass
457
454
 
458
455
  @abstractmethod
459
- def _is_time(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[TimeCoordinate]:
456
+ def _is_time(self, c: xr.DataArray, attributes: CoordinateAttributes) -> TimeCoordinate | None:
460
457
  """Checks if the coordinate is a time coordinate.
461
458
 
462
459
  Parameters
@@ -474,7 +471,7 @@ class CoordinateGuesser(ABC):
474
471
  pass
475
472
 
476
473
  @abstractmethod
477
- def _is_date(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[DateCoordinate]:
474
+ def _is_date(self, c: xr.DataArray, attributes: CoordinateAttributes) -> DateCoordinate | None:
478
475
  """Checks if the coordinate is a date coordinate.
479
476
 
480
477
  Parameters
@@ -492,7 +489,7 @@ class CoordinateGuesser(ABC):
492
489
  pass
493
490
 
494
491
  @abstractmethod
495
- def _is_step(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[StepCoordinate]:
492
+ def _is_step(self, c: xr.DataArray, attributes: CoordinateAttributes) -> StepCoordinate | None:
496
493
  """Checks if the coordinate is a step coordinate.
497
494
 
498
495
  Parameters
@@ -510,7 +507,7 @@ class CoordinateGuesser(ABC):
510
507
  pass
511
508
 
512
509
  @abstractmethod
513
- def _is_level(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LevelCoordinate]:
510
+ def _is_level(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LevelCoordinate | None:
514
511
  """Checks if the coordinate is a level coordinate.
515
512
 
516
513
  Parameters
@@ -528,7 +525,7 @@ class CoordinateGuesser(ABC):
528
525
  pass
529
526
 
530
527
  @abstractmethod
531
- def _is_number(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[EnsembleCoordinate]:
528
+ def _is_number(self, c: xr.DataArray, attributes: CoordinateAttributes) -> EnsembleCoordinate | None:
532
529
  """Checks if the coordinate is an ensemble coordinate.
533
530
 
534
531
  Parameters
@@ -559,7 +556,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
559
556
  """
560
557
  super().__init__(ds)
561
558
 
562
- def _is_point(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[PointCoordinate]:
559
+ def _is_point(self, c: xr.DataArray, attributes: CoordinateAttributes) -> PointCoordinate | None:
563
560
  if attributes.standard_name in ["cell", "station", "poi", "point"]:
564
561
  return PointCoordinate(c)
565
562
 
@@ -568,7 +565,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
568
565
 
569
566
  return None
570
567
 
571
- def _is_longitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LongitudeCoordinate]:
568
+ def _is_longitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LongitudeCoordinate | None:
572
569
  """Checks if the coordinate is a longitude.
573
570
 
574
571
  Parameters
@@ -597,7 +594,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
597
594
 
598
595
  return None
599
596
 
600
- def _is_latitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LatitudeCoordinate]:
597
+ def _is_latitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LatitudeCoordinate | None:
601
598
  """Checks if the coordinate is a latitude.
602
599
 
603
600
  Parameters
@@ -626,7 +623,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
626
623
 
627
624
  return None
628
625
 
629
- def _is_x(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[XCoordinate]:
626
+ def _is_x(self, c: xr.DataArray, attributes: CoordinateAttributes) -> XCoordinate | None:
630
627
  """Checks if the coordinate is an x coordinate.
631
628
 
632
629
  Parameters
@@ -649,7 +646,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
649
646
 
650
647
  return None
651
648
 
652
- def _is_y(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[YCoordinate]:
649
+ def _is_y(self, c: xr.DataArray, attributes: CoordinateAttributes) -> YCoordinate | None:
653
650
  """Checks if the coordinate is a y coordinate.
654
651
 
655
652
  Parameters
@@ -672,7 +669,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
672
669
 
673
670
  return None
674
671
 
675
- def _is_time(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[TimeCoordinate]:
672
+ def _is_time(self, c: xr.DataArray, attributes: CoordinateAttributes) -> TimeCoordinate | None:
676
673
  """Checks if the coordinate is a time coordinate.
677
674
 
678
675
  Parameters
@@ -695,7 +692,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
695
692
 
696
693
  return None
697
694
 
698
- def _is_date(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[DateCoordinate]:
695
+ def _is_date(self, c: xr.DataArray, attributes: CoordinateAttributes) -> DateCoordinate | None:
699
696
  """Checks if the coordinate is a date coordinate.
700
697
 
701
698
  Parameters
@@ -718,7 +715,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
718
715
 
719
716
  return None
720
717
 
721
- def _is_step(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[StepCoordinate]:
718
+ def _is_step(self, c: xr.DataArray, attributes: CoordinateAttributes) -> StepCoordinate | None:
722
719
  """Checks if the coordinate is a step coordinate.
723
720
 
724
721
  Parameters
@@ -744,7 +741,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
744
741
 
745
742
  return None
746
743
 
747
- def _is_level(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LevelCoordinate]:
744
+ def _is_level(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LevelCoordinate | None:
748
745
  """Checks if the coordinate is a level coordinate.
749
746
 
750
747
  Parameters
@@ -762,6 +759,9 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
762
759
  if attributes.standard_name == "atmosphere_hybrid_sigma_pressure_coordinate":
763
760
  return LevelCoordinate(c, "ml")
764
761
 
762
+ if attributes.standard_name == "model_level_number":
763
+ return LevelCoordinate(c, "ml")
764
+
765
765
  if attributes.long_name == "height" and attributes.units == "m":
766
766
  return LevelCoordinate(c, "height")
767
767
 
@@ -782,7 +782,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
782
782
 
783
783
  return None
784
784
 
785
- def _is_number(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[EnsembleCoordinate]:
785
+ def _is_number(self, c: xr.DataArray, attributes: CoordinateAttributes) -> EnsembleCoordinate | None:
786
786
  """Checks if the coordinate is an ensemble coordinate.
787
787
 
788
788
  Parameters
@@ -806,7 +806,7 @@ class DefaultCoordinateGuesser(CoordinateGuesser):
806
806
  class FlavourCoordinateGuesser(CoordinateGuesser):
807
807
  """Implementation of CoordinateGuesser that uses a flavour for guessing."""
808
808
 
809
- def __init__(self, ds: xr.Dataset, flavour: Dict[str, Any]) -> None:
809
+ def __init__(self, ds: xr.Dataset, flavour: dict[str, Any]) -> None:
810
810
  """Initializes the FlavourCoordinateGuesser.
811
811
 
812
812
  Parameters
@@ -819,7 +819,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
819
819
  super().__init__(ds)
820
820
  self.flavour = flavour
821
821
 
822
- def _match(self, c: xr.DataArray, key: str, attributes: CoordinateAttributes) -> Optional[Dict[str, Any]]:
822
+ def _match(self, c: xr.DataArray, key: str, attributes: CoordinateAttributes) -> dict[str, Any] | None:
823
823
  """Matches the coordinate against the flavour rules.
824
824
 
825
825
  Parameters
@@ -854,7 +854,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
854
854
 
855
855
  return None
856
856
 
857
- def _is_longitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LongitudeCoordinate]:
857
+ def _is_longitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LongitudeCoordinate | None:
858
858
  """Checks if the coordinate is a longitude using the flavour rules.
859
859
 
860
860
  Parameters
@@ -874,7 +874,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
874
874
 
875
875
  return None
876
876
 
877
- def _is_latitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LatitudeCoordinate]:
877
+ def _is_latitude(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LatitudeCoordinate | None:
878
878
  """Checks if the coordinate is a latitude using the flavour rules.
879
879
 
880
880
  Parameters
@@ -894,7 +894,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
894
894
 
895
895
  return None
896
896
 
897
- def _is_x(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[XCoordinate]:
897
+ def _is_x(self, c: xr.DataArray, attributes: CoordinateAttributes) -> XCoordinate | None:
898
898
  """Checks if the coordinate is an x coordinate using the flavour rules.
899
899
 
900
900
  Parameters
@@ -914,7 +914,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
914
914
 
915
915
  return None
916
916
 
917
- def _is_y(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[YCoordinate]:
917
+ def _is_y(self, c: xr.DataArray, attributes: CoordinateAttributes) -> YCoordinate | None:
918
918
  """Checks if the coordinate is a y coordinate using the flavour rules.
919
919
 
920
920
  Parameters
@@ -934,7 +934,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
934
934
 
935
935
  return None
936
936
 
937
- def _is_time(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[TimeCoordinate]:
937
+ def _is_time(self, c: xr.DataArray, attributes: CoordinateAttributes) -> TimeCoordinate | None:
938
938
  """Checks if the coordinate is a time coordinate using the flavour rules.
939
939
 
940
940
  Parameters
@@ -954,7 +954,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
954
954
 
955
955
  return None
956
956
 
957
- def _is_step(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[StepCoordinate]:
957
+ def _is_step(self, c: xr.DataArray, attributes: CoordinateAttributes) -> StepCoordinate | None:
958
958
  """Checks if the coordinate is a step coordinate using the flavour rules.
959
959
 
960
960
  Parameters
@@ -974,7 +974,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
974
974
 
975
975
  return None
976
976
 
977
- def _is_date(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[DateCoordinate]:
977
+ def _is_date(self, c: xr.DataArray, attributes: CoordinateAttributes) -> DateCoordinate | None:
978
978
  """Checks if the coordinate is a date coordinate using the flavour rules.
979
979
 
980
980
  Parameters
@@ -994,7 +994,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
994
994
 
995
995
  return None
996
996
 
997
- def _is_level(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[LevelCoordinate]:
997
+ def _is_level(self, c: xr.DataArray, attributes: CoordinateAttributes) -> LevelCoordinate | None:
998
998
  """Checks if the coordinate is a level coordinate using the flavour rules.
999
999
 
1000
1000
  Parameters
@@ -1039,7 +1039,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
1039
1039
 
1040
1040
  raise NotImplementedError(f"levtype for {c=}")
1041
1041
 
1042
- def _is_number(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[EnsembleCoordinate]:
1042
+ def _is_number(self, c: xr.DataArray, attributes: CoordinateAttributes) -> EnsembleCoordinate | None:
1043
1043
  """Checks if the coordinate is an ensemble coordinate using the flavour rules.
1044
1044
 
1045
1045
  Parameters
@@ -1059,7 +1059,7 @@ class FlavourCoordinateGuesser(CoordinateGuesser):
1059
1059
 
1060
1060
  return None
1061
1061
 
1062
- def _is_point(self, c: xr.DataArray, attributes: CoordinateAttributes) -> Optional[PointCoordinate]:
1062
+ def _is_point(self, c: xr.DataArray, attributes: CoordinateAttributes) -> PointCoordinate | None:
1063
1063
  """Checks if the coordinate is a point coordinate using the flavour rules.
1064
1064
 
1065
1065
  Parameters