anemoi-datasets 0.5.15__py3-none-any.whl → 0.5.17__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 (155) hide show
  1. anemoi/datasets/__init__.py +4 -1
  2. anemoi/datasets/__main__.py +12 -2
  3. anemoi/datasets/_version.py +9 -4
  4. anemoi/datasets/commands/cleanup.py +17 -2
  5. anemoi/datasets/commands/compare.py +18 -2
  6. anemoi/datasets/commands/copy.py +196 -14
  7. anemoi/datasets/commands/create.py +50 -7
  8. anemoi/datasets/commands/finalise-additions.py +17 -2
  9. anemoi/datasets/commands/finalise.py +17 -2
  10. anemoi/datasets/commands/init-additions.py +17 -2
  11. anemoi/datasets/commands/init.py +16 -2
  12. anemoi/datasets/commands/inspect.py +283 -62
  13. anemoi/datasets/commands/load-additions.py +16 -2
  14. anemoi/datasets/commands/load.py +16 -2
  15. anemoi/datasets/commands/patch.py +17 -2
  16. anemoi/datasets/commands/publish.py +17 -2
  17. anemoi/datasets/commands/scan.py +31 -3
  18. anemoi/datasets/compute/recentre.py +47 -11
  19. anemoi/datasets/create/__init__.py +612 -85
  20. anemoi/datasets/create/check.py +142 -20
  21. anemoi/datasets/create/chunks.py +64 -4
  22. anemoi/datasets/create/config.py +185 -21
  23. anemoi/datasets/create/filter.py +50 -0
  24. anemoi/datasets/create/filters/__init__.py +33 -0
  25. anemoi/datasets/create/filters/empty.py +37 -0
  26. anemoi/datasets/create/filters/legacy.py +93 -0
  27. anemoi/datasets/create/filters/noop.py +37 -0
  28. anemoi/datasets/create/filters/orog_to_z.py +58 -0
  29. anemoi/datasets/create/{functions/filters → filters}/pressure_level_relative_humidity_to_specific_humidity.py +33 -10
  30. anemoi/datasets/create/{functions/filters → filters}/pressure_level_specific_humidity_to_relative_humidity.py +32 -8
  31. anemoi/datasets/create/filters/rename.py +205 -0
  32. anemoi/datasets/create/{functions/filters → filters}/rotate_winds.py +43 -28
  33. anemoi/datasets/create/{functions/filters → filters}/single_level_dewpoint_to_relative_humidity.py +32 -9
  34. anemoi/datasets/create/{functions/filters → filters}/single_level_relative_humidity_to_dewpoint.py +33 -9
  35. anemoi/datasets/create/{functions/filters → filters}/single_level_relative_humidity_to_specific_humidity.py +55 -7
  36. anemoi/datasets/create/{functions/filters → filters}/single_level_specific_humidity_to_relative_humidity.py +98 -37
  37. anemoi/datasets/create/filters/speeddir_to_uv.py +95 -0
  38. anemoi/datasets/create/{functions/filters → filters}/sum.py +24 -27
  39. anemoi/datasets/create/filters/transform.py +53 -0
  40. anemoi/datasets/create/{functions/filters → filters}/unrotate_winds.py +27 -18
  41. anemoi/datasets/create/filters/uv_to_speeddir.py +94 -0
  42. anemoi/datasets/create/{functions/filters → filters}/wz_to_w.py +51 -33
  43. anemoi/datasets/create/input/__init__.py +76 -5
  44. anemoi/datasets/create/input/action.py +149 -13
  45. anemoi/datasets/create/input/concat.py +81 -10
  46. anemoi/datasets/create/input/context.py +39 -4
  47. anemoi/datasets/create/input/data_sources.py +72 -6
  48. anemoi/datasets/create/input/empty.py +21 -3
  49. anemoi/datasets/create/input/filter.py +60 -12
  50. anemoi/datasets/create/input/function.py +154 -37
  51. anemoi/datasets/create/input/join.py +86 -14
  52. anemoi/datasets/create/input/misc.py +67 -17
  53. anemoi/datasets/create/input/pipe.py +33 -6
  54. anemoi/datasets/create/input/repeated_dates.py +189 -41
  55. anemoi/datasets/create/input/result.py +202 -87
  56. anemoi/datasets/create/input/step.py +119 -22
  57. anemoi/datasets/create/input/template.py +100 -13
  58. anemoi/datasets/create/input/trace.py +62 -7
  59. anemoi/datasets/create/patch.py +52 -4
  60. anemoi/datasets/create/persistent.py +134 -17
  61. anemoi/datasets/create/size.py +15 -1
  62. anemoi/datasets/create/source.py +51 -0
  63. anemoi/datasets/create/sources/__init__.py +36 -0
  64. anemoi/datasets/create/{functions/sources → sources}/accumulations.py +296 -30
  65. anemoi/datasets/create/{functions/sources → sources}/constants.py +27 -2
  66. anemoi/datasets/create/{functions/sources → sources}/eccc_fstd.py +7 -3
  67. anemoi/datasets/create/sources/empty.py +37 -0
  68. anemoi/datasets/create/{functions/sources → sources}/forcings.py +25 -1
  69. anemoi/datasets/create/sources/grib.py +297 -0
  70. anemoi/datasets/create/{functions/sources → sources}/hindcasts.py +38 -4
  71. anemoi/datasets/create/sources/legacy.py +93 -0
  72. anemoi/datasets/create/{functions/sources → sources}/mars.py +168 -20
  73. anemoi/datasets/create/sources/netcdf.py +42 -0
  74. anemoi/datasets/create/sources/opendap.py +43 -0
  75. anemoi/datasets/create/{functions/sources/__init__.py → sources/patterns.py} +35 -4
  76. anemoi/datasets/create/sources/recentre.py +150 -0
  77. anemoi/datasets/create/{functions/sources → sources}/source.py +27 -5
  78. anemoi/datasets/create/{functions/sources → sources}/tendencies.py +64 -7
  79. anemoi/datasets/create/sources/xarray.py +92 -0
  80. anemoi/datasets/create/sources/xarray_kerchunk.py +36 -0
  81. anemoi/datasets/create/sources/xarray_support/README.md +1 -0
  82. anemoi/datasets/create/{functions/sources/xarray → sources/xarray_support}/__init__.py +109 -8
  83. anemoi/datasets/create/sources/xarray_support/coordinates.py +442 -0
  84. anemoi/datasets/create/{functions/sources/xarray → sources/xarray_support}/field.py +94 -16
  85. anemoi/datasets/create/{functions/sources/xarray → sources/xarray_support}/fieldlist.py +90 -25
  86. anemoi/datasets/create/sources/xarray_support/flavour.py +1036 -0
  87. anemoi/datasets/create/{functions/sources/xarray → sources/xarray_support}/grid.py +92 -31
  88. anemoi/datasets/create/sources/xarray_support/metadata.py +395 -0
  89. anemoi/datasets/create/sources/xarray_support/patch.py +91 -0
  90. anemoi/datasets/create/sources/xarray_support/time.py +391 -0
  91. anemoi/datasets/create/sources/xarray_support/variable.py +331 -0
  92. anemoi/datasets/create/sources/xarray_zarr.py +41 -0
  93. anemoi/datasets/create/{functions/sources → sources}/zenodo.py +34 -5
  94. anemoi/datasets/create/statistics/__init__.py +233 -44
  95. anemoi/datasets/create/statistics/summary.py +52 -6
  96. anemoi/datasets/create/testing.py +76 -0
  97. anemoi/datasets/create/{functions/filters/noop.py → typing.py} +6 -3
  98. anemoi/datasets/create/utils.py +97 -6
  99. anemoi/datasets/create/writer.py +26 -4
  100. anemoi/datasets/create/zarr.py +170 -23
  101. anemoi/datasets/data/__init__.py +51 -4
  102. anemoi/datasets/data/complement.py +191 -40
  103. anemoi/datasets/data/concat.py +141 -16
  104. anemoi/datasets/data/dataset.py +552 -61
  105. anemoi/datasets/data/debug.py +197 -26
  106. anemoi/datasets/data/ensemble.py +93 -8
  107. anemoi/datasets/data/fill_missing.py +165 -18
  108. anemoi/datasets/data/forwards.py +428 -56
  109. anemoi/datasets/data/grids.py +323 -97
  110. anemoi/datasets/data/indexing.py +112 -19
  111. anemoi/datasets/data/interpolate.py +92 -12
  112. anemoi/datasets/data/join.py +158 -19
  113. anemoi/datasets/data/masked.py +129 -15
  114. anemoi/datasets/data/merge.py +137 -23
  115. anemoi/datasets/data/misc.py +172 -16
  116. anemoi/datasets/data/missing.py +233 -29
  117. anemoi/datasets/data/rescale.py +111 -10
  118. anemoi/datasets/data/select.py +168 -26
  119. anemoi/datasets/data/statistics.py +67 -6
  120. anemoi/datasets/data/stores.py +149 -64
  121. anemoi/datasets/data/subset.py +159 -25
  122. anemoi/datasets/data/unchecked.py +168 -57
  123. anemoi/datasets/data/xy.py +168 -25
  124. anemoi/datasets/dates/__init__.py +191 -16
  125. anemoi/datasets/dates/groups.py +189 -47
  126. anemoi/datasets/grids.py +270 -31
  127. anemoi/datasets/testing.py +28 -1
  128. {anemoi_datasets-0.5.15.dist-info → anemoi_datasets-0.5.17.dist-info}/METADATA +10 -7
  129. anemoi_datasets-0.5.17.dist-info/RECORD +137 -0
  130. {anemoi_datasets-0.5.15.dist-info → anemoi_datasets-0.5.17.dist-info}/WHEEL +1 -1
  131. {anemoi_datasets-0.5.15.dist-info → anemoi_datasets-0.5.17.dist-info/licenses}/LICENSE +1 -1
  132. anemoi/datasets/create/functions/__init__.py +0 -66
  133. anemoi/datasets/create/functions/filters/__init__.py +0 -9
  134. anemoi/datasets/create/functions/filters/empty.py +0 -17
  135. anemoi/datasets/create/functions/filters/orog_to_z.py +0 -58
  136. anemoi/datasets/create/functions/filters/rename.py +0 -79
  137. anemoi/datasets/create/functions/filters/speeddir_to_uv.py +0 -78
  138. anemoi/datasets/create/functions/filters/uv_to_speeddir.py +0 -56
  139. anemoi/datasets/create/functions/sources/empty.py +0 -15
  140. anemoi/datasets/create/functions/sources/grib.py +0 -150
  141. anemoi/datasets/create/functions/sources/netcdf.py +0 -15
  142. anemoi/datasets/create/functions/sources/opendap.py +0 -15
  143. anemoi/datasets/create/functions/sources/recentre.py +0 -60
  144. anemoi/datasets/create/functions/sources/xarray/coordinates.py +0 -255
  145. anemoi/datasets/create/functions/sources/xarray/flavour.py +0 -472
  146. anemoi/datasets/create/functions/sources/xarray/metadata.py +0 -148
  147. anemoi/datasets/create/functions/sources/xarray/patch.py +0 -44
  148. anemoi/datasets/create/functions/sources/xarray/time.py +0 -177
  149. anemoi/datasets/create/functions/sources/xarray/variable.py +0 -188
  150. anemoi/datasets/create/functions/sources/xarray_kerchunk.py +0 -42
  151. anemoi/datasets/create/functions/sources/xarray_zarr.py +0 -15
  152. anemoi/datasets/utils/fields.py +0 -47
  153. anemoi_datasets-0.5.15.dist-info/RECORD +0 -129
  154. {anemoi_datasets-0.5.15.dist-info → anemoi_datasets-0.5.17.dist-info}/entry_points.txt +0 -0
  155. {anemoi_datasets-0.5.15.dist-info → anemoi_datasets-0.5.17.dist-info}/top_level.txt +0 -0
@@ -7,19 +7,41 @@
7
7
  # granted to it by virtue of its status as an intergovernmental organisation
8
8
  # nor does it submit to any jurisdiction.
9
9
 
10
+ import datetime
10
11
  import logging
11
-
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
+
18
+ import earthkit.data as ekd
19
+ import xarray as xr
12
20
  from earthkit.data.core.fieldlist import MultiFieldList
13
21
 
22
+ from anemoi.datasets.create.sources.patterns import iterate_patterns
14
23
  from anemoi.datasets.data.stores import name_to_zarr_store
15
24
 
16
- from .. import iterate_patterns
25
+ from ..legacy import legacy_source
17
26
  from .fieldlist import XarrayFieldList
18
27
 
19
28
  LOG = logging.getLogger(__name__)
20
29
 
21
30
 
22
- def check(what, ds, paths, **kwargs):
31
+ def check(what: str, ds: xr.Dataset, paths: List[str], **kwargs: Any) -> None:
32
+ """Checks if the dataset has the expected number of fields.
33
+
34
+ Parameters
35
+ ----------
36
+ what : str
37
+ Description of what is being checked.
38
+ ds : xr.Dataset
39
+ The dataset to check.
40
+ paths : List[str]
41
+ List of paths.
42
+ **kwargs : Any
43
+ Additional keyword arguments.
44
+ """
23
45
  count = 1
24
46
  for k, v in kwargs.items():
25
47
  if isinstance(v, (tuple, list)):
@@ -29,8 +51,43 @@ def check(what, ds, paths, **kwargs):
29
51
  raise ValueError(f"Expected {count} fields, got {len(ds)} (kwargs={kwargs}, {what}s={paths})")
30
52
 
31
53
 
32
- def load_one(emoji, context, dates, dataset, *, options={}, flavour=None, patch=None, **kwargs):
33
- import xarray as xr
54
+ def load_one(
55
+ emoji: str,
56
+ context: Any,
57
+ dates: List[str],
58
+ dataset: Union[str, xr.Dataset],
59
+ *,
60
+ options: Optional[Dict[str, Any]] = None,
61
+ flavour: Optional[str] = None,
62
+ patch: Optional[Any] = None,
63
+ **kwargs: Any,
64
+ ) -> ekd.FieldList:
65
+ """Loads a single dataset.
66
+
67
+ Parameters
68
+ ----------
69
+ emoji : str
70
+ Emoji for tracing.
71
+ context : Any
72
+ Context object.
73
+ dates : List[str]
74
+ List of dates.
75
+ dataset : Union[str, xr.Dataset]
76
+ The dataset to load.
77
+ options : Dict[str, Any], optional
78
+ Additional options for loading the dataset.
79
+ flavour : Optional[str], optional
80
+ Flavour of the dataset.
81
+ patch : Optional[Any], optional
82
+ Patch for the dataset.
83
+ **kwargs : Any
84
+ Additional keyword arguments.
85
+
86
+ Returns
87
+ -------
88
+ MultiFieldList
89
+ The loaded dataset.
90
+ """
34
91
 
35
92
  """
36
93
  We manage the S3 client ourselve, bypassing fsspec and s3fs layers, because sometimes something on the stack
@@ -41,6 +98,9 @@ def load_one(emoji, context, dates, dataset, *, options={}, flavour=None, patch=
41
98
  We have seen this bug triggered when we run many clients in parallel, for example, when we create a new dataset using `xarray-zarr`.
42
99
  """
43
100
 
101
+ if options is None:
102
+ options = {}
103
+
44
104
  context.trace(emoji, dataset, options, kwargs)
45
105
 
46
106
  if isinstance(dataset, str) and ".zarr" in dataset:
@@ -59,6 +119,7 @@ def load_one(emoji, context, dates, dataset, *, options={}, flavour=None, patch=
59
119
  if len(dates) == 0:
60
120
  result = fs.sel(**kwargs)
61
121
  else:
122
+ print("dates", dates, kwargs)
62
123
  result = MultiFieldList([fs.sel(valid_datetime=date, **kwargs) for date in dates])
63
124
 
64
125
  if len(result) == 0:
@@ -79,8 +140,27 @@ def load_one(emoji, context, dates, dataset, *, options={}, flavour=None, patch=
79
140
  return result
80
141
 
81
142
 
82
- def load_many(emoji, context, dates, pattern, **kwargs):
83
-
143
+ def load_many(emoji: str, context: Any, dates: List[datetime.datetime], pattern: str, **kwargs: Any) -> ekd.FieldList:
144
+ """Loads multiple datasets.
145
+
146
+ Parameters
147
+ ----------
148
+ emoji : str
149
+ Emoji for tracing.
150
+ context : Any
151
+ Context object.
152
+ dates : List[str]
153
+ List of dates.
154
+ pattern : str
155
+ Pattern for loading datasets.
156
+ **kwargs : Any
157
+ Additional keyword arguments.
158
+
159
+ Returns
160
+ -------
161
+ MultiFieldList
162
+ The loaded datasets.
163
+ """
84
164
  result = []
85
165
 
86
166
  for path, dates in iterate_patterns(pattern, dates, **kwargs):
@@ -89,5 +169,26 @@ def load_many(emoji, context, dates, pattern, **kwargs):
89
169
  return MultiFieldList(result)
90
170
 
91
171
 
92
- def execute(context, dates, url, *args, **kwargs):
172
+ @legacy_source("xarray")
173
+ def execute(context: Any, dates: List[str], url: str, *args: Any, **kwargs: Any) -> ekd.FieldList:
174
+ """Executes the loading of datasets.
175
+
176
+ Parameters
177
+ ----------
178
+ context : Any
179
+ Context object.
180
+ dates : List[str]
181
+ List of dates.
182
+ url : str
183
+ URL pattern for loading datasets.
184
+ *args : Any
185
+ Additional arguments.
186
+ **kwargs : Any
187
+ Additional keyword arguments.
188
+
189
+ Returns
190
+ -------
191
+ ekd.FieldList
192
+ The loaded datasets.
193
+ """
93
194
  return load_many("🌐", context, dates, url, *args, **kwargs)
@@ -0,0 +1,442 @@
1
+ # (C) Copyright 2024 Anemoi contributors.
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
+ #
6
+ # In applying this licence, ECMWF does not waive the privileges and immunities
7
+ # granted to it by virtue of its status as an intergovernmental organisation
8
+ # nor does it submit to any jurisdiction.
9
+
10
+
11
+ from __future__ import annotations
12
+
13
+ import datetime
14
+ import logging
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
+
21
+ import numpy as np
22
+ import xarray as xr
23
+ from earthkit.data.utils.dates import to_datetime
24
+
25
+ LOG = logging.getLogger(__name__)
26
+
27
+
28
+ def is_scalar(variable: Any) -> bool:
29
+ """Check if the variable is scalar.
30
+
31
+ Parameters
32
+ ----------
33
+ variable : Any
34
+ The variable to check.
35
+
36
+ Returns
37
+ -------
38
+ bool
39
+ True if the variable is scalar, False otherwise.
40
+ """
41
+ shape = variable.shape
42
+ if shape == (1,):
43
+ return True
44
+ if len(shape) == 0:
45
+ return True
46
+ return False
47
+
48
+
49
+ def extract_single_value(variable: Any) -> Any:
50
+ """Extract a single value from the variable.
51
+
52
+ Parameters
53
+ ----------
54
+ variable : Any
55
+ The variable to extract the value from.
56
+
57
+ Returns
58
+ -------
59
+ Any
60
+ The extracted single value.
61
+ """
62
+ shape = variable.shape
63
+ if np.issubdtype(variable.values.dtype, np.datetime64):
64
+ if len(shape) == 0:
65
+ return to_datetime(variable.values) # Convert to python datetime
66
+ if shape == (1,):
67
+ return to_datetime(variable.values[0])
68
+ assert False, (shape, variable.values[:2])
69
+
70
+ if np.issubdtype(variable.values.dtype, np.timedelta64):
71
+ if len(shape) == 0:
72
+ # Convert to python timedelta64
73
+ return datetime.timedelta(seconds=variable.values.astype("timedelta64[s]").astype(int).item())
74
+ assert False, (shape, variable.values)
75
+
76
+ if shape == (1,):
77
+ return variable.values[0]
78
+
79
+ if len(shape) == 0:
80
+ return variable.values.item()
81
+
82
+ assert False, (shape, variable.values)
83
+
84
+
85
+ class Coordinate:
86
+ """Base class for coordinates."""
87
+
88
+ is_grid = False
89
+ is_dim = True
90
+ is_lat = False
91
+ is_lon = False
92
+ is_time = False
93
+ is_step = False
94
+ is_date = False
95
+ is_member = False
96
+ is_x = False
97
+ is_y = False
98
+
99
+ def __init__(self, variable: xr.DataArray) -> None:
100
+ """Initialize the coordinate.
101
+
102
+ Parameters
103
+ ----------
104
+ variable : Any
105
+ The variable representing the coordinate.
106
+ """
107
+ self.variable = variable
108
+ self.scalar = is_scalar(variable)
109
+ self.kwargs: Dict[str, Any] = {} # Used when creating a new coordinate (reduced method)
110
+
111
+ def __len__(self) -> int:
112
+ """Get the length of the coordinate.
113
+
114
+ Returns
115
+ -------
116
+ int
117
+ The length of the coordinate.
118
+ """
119
+ return 1 if self.scalar else len(self.variable)
120
+
121
+ def __repr__(self) -> str:
122
+ """Get the string representation of the coordinate.
123
+
124
+ Returns
125
+ -------
126
+ str
127
+ The string representation of the coordinate.
128
+ """
129
+ return "%s[name=%s,values=%s,shape=%s]" % (
130
+ self.__class__.__name__,
131
+ self.variable.name,
132
+ self.variable.values if self.scalar else len(self),
133
+ self.variable.shape,
134
+ )
135
+
136
+ def reduced(self, i: int) -> Coordinate:
137
+ """Create a new coordinate with a single value.
138
+
139
+ Parameters
140
+ ----------
141
+ i : int
142
+ The index of the value to select.
143
+
144
+ Returns
145
+ -------
146
+ Coordinate
147
+ A new coordinate with the selected value.
148
+ """
149
+ return self.__class__(
150
+ self.variable.isel({self.variable.dims[0]: i}),
151
+ **self.kwargs,
152
+ )
153
+
154
+ def index(self, value: Union[Any, list, tuple]) -> Optional[Union[int, list]]:
155
+ """Return the index of the value in the coordinate.
156
+
157
+ Parameters
158
+ ----------
159
+ value : Union[Any, list, tuple]
160
+ The value to search for.
161
+
162
+ Returns
163
+ -------
164
+ Optional[Union[int, list]]
165
+ The index or indices of the value in the coordinate, or None if not found.
166
+ """
167
+ if isinstance(value, (list, tuple)):
168
+ if len(value) == 1:
169
+ return self._index_single(value)
170
+ else:
171
+ return self._index_multiple(value)
172
+ return self._index_single(value)
173
+
174
+ def _index_single(self, value: Any) -> Optional[int]:
175
+ """Return the index of a single value in the coordinate.
176
+
177
+ Parameters
178
+ ----------
179
+ value : Any
180
+ The value to search for.
181
+
182
+ Returns
183
+ -------
184
+ Optional[int]
185
+ The index of the value in the coordinate, or None if not found.
186
+ """
187
+ values = self.variable.values
188
+
189
+ # Check if dimension is 0D
190
+ if not isinstance(values, (list, np.ndarray)):
191
+ values = [values]
192
+
193
+ # Assume the array is sorted
194
+ index = np.searchsorted(values, value)
195
+
196
+ if index < len(values) and values[index] == value:
197
+ return index
198
+
199
+ # If not found, we need to check if the value is in the array
200
+
201
+ index = np.where(values == value)[0]
202
+ if len(index) > 0:
203
+ return index[0]
204
+
205
+ return None
206
+
207
+ def _index_multiple(self, value: list) -> Optional[list]:
208
+ """Return the indices of multiple values in the coordinate.
209
+
210
+ Parameters
211
+ ----------
212
+ value : list
213
+ The values to search for.
214
+
215
+ Returns
216
+ -------
217
+ Optional[list]
218
+ The indices of the values in the coordinate, or None if not found.
219
+ """
220
+ values = self.variable.values
221
+
222
+ # Check if dimension is 0D
223
+ if not isinstance(values, (list, np.ndarray)):
224
+ values = [values]
225
+
226
+ # Assume the array is sorted
227
+
228
+ index = np.searchsorted(values, value)
229
+ index = index[index < len(values)]
230
+
231
+ if np.all(values[index] == value):
232
+ return index
233
+
234
+ # If not found, we need to check if the value is in the array
235
+
236
+ index = np.where(np.isin(values, value))[0]
237
+
238
+ # We could also return incomplete matches
239
+ if len(index) == len(value):
240
+ return index
241
+
242
+ return None
243
+
244
+ @property
245
+ def name(self) -> str:
246
+ """Get the name of the coordinate."""
247
+ return self.variable.name
248
+
249
+ def normalise(self, value: Any) -> Any:
250
+ """Normalize the value for the coordinate.
251
+
252
+ Parameters
253
+ ----------
254
+ value : Any
255
+ The value to normalize.
256
+
257
+ Returns
258
+ -------
259
+ Any
260
+ The normalized value.
261
+ """
262
+ # Subclasses to format values that will be added to the field metadata
263
+ return value
264
+
265
+ @property
266
+ def single_value(self) -> Any:
267
+ """Get the single value of the coordinate."""
268
+ return extract_single_value(self.variable)
269
+
270
+
271
+ class TimeCoordinate(Coordinate):
272
+ """Coordinate class for time."""
273
+
274
+ is_time = True
275
+ mars_names = ("valid_datetime",)
276
+
277
+ def index(self, time: datetime.datetime) -> Optional[int]:
278
+ """Return the index of the time in the coordinate.
279
+
280
+ Parameters
281
+ ----------
282
+ time : datetime.datetime
283
+ The time to search for.
284
+
285
+ Returns
286
+ -------
287
+ Optional[int]
288
+ The index of the time in the coordinate, or None if not found.
289
+ """
290
+ return super().index(np.datetime64(time))
291
+
292
+
293
+ class DateCoordinate(Coordinate):
294
+ """Coordinate class for date."""
295
+
296
+ is_date = True
297
+ mars_names = ("date",)
298
+
299
+ def index(self, date: datetime.datetime) -> Optional[int]:
300
+ """Return the index of the date in the coordinate.
301
+
302
+ Parameters
303
+ ----------
304
+ date : datetime.datetime
305
+ The date to search for.
306
+
307
+ Returns
308
+ -------
309
+ Optional[int]
310
+ The index of the date in the coordinate, or None if not found.
311
+ """
312
+ return super().index(np.datetime64(date))
313
+
314
+
315
+ class StepCoordinate(Coordinate):
316
+ """Coordinate class for step."""
317
+
318
+ is_step = True
319
+ mars_names = ("step",)
320
+
321
+
322
+ class LevelCoordinate(Coordinate):
323
+ """Coordinate class for level.
324
+
325
+ Parameters
326
+ ----------
327
+ variable : Any
328
+ The variable representing the coordinate.
329
+ levtype : str
330
+ The type of level.
331
+ """
332
+
333
+ mars_names = ("level", "levelist")
334
+
335
+ def __init__(self, variable: Any, levtype: str) -> None:
336
+ """Initialize the level coordinate.
337
+
338
+ Parameters
339
+ ----------
340
+ variable : Any
341
+ The variable representing the coordinate.
342
+ levtype : str
343
+ The type of level.
344
+ """
345
+ super().__init__(variable)
346
+ self.levtype = levtype
347
+ # kwargs is used when creating a new coordinate (reduced method)
348
+ self.kwargs = {"levtype": levtype}
349
+
350
+ def normalise(self, value: Any) -> Any:
351
+ """Normalize the value for the level coordinate.
352
+
353
+ Parameters
354
+ ----------
355
+ value : Any
356
+ The value to normalize.
357
+
358
+ Returns
359
+ -------
360
+ Any
361
+ The normalized value.
362
+ """
363
+ # Some netcdf have pressue levels in float
364
+ if int(value) == value:
365
+ return int(value)
366
+ return value
367
+
368
+
369
+ class EnsembleCoordinate(Coordinate):
370
+ """Coordinate class for ensemble."""
371
+
372
+ is_member = True
373
+ mars_names = ("number",)
374
+
375
+ def normalise(self, value: Any) -> Any:
376
+ """Normalize the value for the ensemble coordinate.
377
+
378
+ Parameters
379
+ ----------
380
+ value : Any
381
+ The value to normalize.
382
+
383
+ Returns
384
+ -------
385
+ Any
386
+ The normalized value.
387
+ """
388
+ if int(value) == value:
389
+ return int(value)
390
+ return value
391
+
392
+
393
+ class LongitudeCoordinate(Coordinate):
394
+ """Coordinate class for longitude."""
395
+
396
+ is_grid = True
397
+ is_lon = True
398
+ mars_names = ("longitude",)
399
+
400
+
401
+ class LatitudeCoordinate(Coordinate):
402
+ """Coordinate class for latitude."""
403
+
404
+ is_grid = True
405
+ is_lat = True
406
+ mars_names = ("latitude",)
407
+
408
+
409
+ class XCoordinate(Coordinate):
410
+ """Coordinate class for X."""
411
+
412
+ is_grid = True
413
+ is_x = True
414
+ mars_names = ("x",)
415
+
416
+
417
+ class YCoordinate(Coordinate):
418
+ """Coordinate class for Y."""
419
+
420
+ is_grid = True
421
+ is_y = True
422
+ mars_names = ("y",)
423
+
424
+
425
+ class ScalarCoordinate(Coordinate):
426
+ """Coordinate class for scalar."""
427
+
428
+ is_grid = False
429
+
430
+ @property
431
+ def mars_names(self) -> Tuple[str, ...]:
432
+ """Get the MARS names for the coordinate."""
433
+ return (self.variable.name,)
434
+
435
+
436
+ class UnsupportedCoordinate(Coordinate):
437
+ """Coordinate class for unsupported coordinates."""
438
+
439
+ @property
440
+ def mars_names(self) -> tuple:
441
+ """Get the MARS names for the coordinate."""
442
+ return (self.variable.name,)