anemoi-datasets 0.4.5__py3-none-any.whl → 0.5.5__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 (67) hide show
  1. anemoi/datasets/_version.py +2 -2
  2. anemoi/datasets/commands/create.py +3 -2
  3. anemoi/datasets/commands/inspect.py +1 -1
  4. anemoi/datasets/commands/publish.py +30 -0
  5. anemoi/datasets/create/__init__.py +72 -35
  6. anemoi/datasets/create/check.py +6 -0
  7. anemoi/datasets/create/config.py +4 -3
  8. anemoi/datasets/create/functions/filters/pressure_level_relative_humidity_to_specific_humidity.py +57 -0
  9. anemoi/datasets/create/functions/filters/pressure_level_specific_humidity_to_relative_humidity.py +57 -0
  10. anemoi/datasets/create/functions/filters/rename.py +2 -3
  11. anemoi/datasets/create/functions/filters/single_level_dewpoint_to_relative_humidity.py +54 -0
  12. anemoi/datasets/create/functions/filters/single_level_relative_humidity_to_dewpoint.py +59 -0
  13. anemoi/datasets/create/functions/filters/single_level_relative_humidity_to_specific_humidity.py +115 -0
  14. anemoi/datasets/create/functions/filters/single_level_specific_humidity_to_relative_humidity.py +390 -0
  15. anemoi/datasets/create/functions/filters/speeddir_to_uv.py +77 -0
  16. anemoi/datasets/create/functions/filters/uv_to_speeddir.py +55 -0
  17. anemoi/datasets/create/functions/sources/__init__.py +7 -1
  18. anemoi/datasets/create/functions/sources/accumulations.py +2 -0
  19. anemoi/datasets/create/functions/sources/grib.py +87 -2
  20. anemoi/datasets/create/functions/sources/hindcasts.py +14 -73
  21. anemoi/datasets/create/functions/sources/mars.py +9 -3
  22. anemoi/datasets/create/functions/sources/xarray/__init__.py +6 -1
  23. anemoi/datasets/create/functions/sources/xarray/coordinates.py +6 -1
  24. anemoi/datasets/create/functions/sources/xarray/field.py +20 -5
  25. anemoi/datasets/create/functions/sources/xarray/fieldlist.py +16 -16
  26. anemoi/datasets/create/functions/sources/xarray/flavour.py +126 -12
  27. anemoi/datasets/create/functions/sources/xarray/grid.py +106 -17
  28. anemoi/datasets/create/functions/sources/xarray/metadata.py +6 -12
  29. anemoi/datasets/create/functions/sources/xarray/time.py +1 -5
  30. anemoi/datasets/create/functions/sources/xarray/variable.py +10 -10
  31. anemoi/datasets/create/input/__init__.py +69 -0
  32. anemoi/datasets/create/input/action.py +123 -0
  33. anemoi/datasets/create/input/concat.py +92 -0
  34. anemoi/datasets/create/input/context.py +59 -0
  35. anemoi/datasets/create/input/data_sources.py +71 -0
  36. anemoi/datasets/create/input/empty.py +42 -0
  37. anemoi/datasets/create/input/filter.py +76 -0
  38. anemoi/datasets/create/input/function.py +122 -0
  39. anemoi/datasets/create/input/join.py +57 -0
  40. anemoi/datasets/create/input/misc.py +85 -0
  41. anemoi/datasets/create/input/pipe.py +33 -0
  42. anemoi/datasets/create/input/repeated_dates.py +217 -0
  43. anemoi/datasets/create/input/result.py +413 -0
  44. anemoi/datasets/create/input/step.py +99 -0
  45. anemoi/datasets/create/{template.py → input/template.py} +0 -42
  46. anemoi/datasets/create/persistent.py +1 -1
  47. anemoi/datasets/create/statistics/__init__.py +1 -1
  48. anemoi/datasets/create/utils.py +3 -0
  49. anemoi/datasets/create/zarr.py +4 -2
  50. anemoi/datasets/data/dataset.py +11 -1
  51. anemoi/datasets/data/debug.py +5 -1
  52. anemoi/datasets/data/masked.py +2 -2
  53. anemoi/datasets/data/rescale.py +147 -0
  54. anemoi/datasets/data/stores.py +20 -7
  55. anemoi/datasets/dates/__init__.py +113 -30
  56. anemoi/datasets/dates/groups.py +92 -19
  57. anemoi/datasets/fields.py +66 -0
  58. anemoi/datasets/utils/fields.py +47 -0
  59. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.5.dist-info}/METADATA +10 -19
  60. anemoi_datasets-0.5.5.dist-info/RECORD +121 -0
  61. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.5.dist-info}/WHEEL +1 -1
  62. anemoi/datasets/create/input.py +0 -1065
  63. anemoi_datasets-0.4.5.dist-info/RECORD +0 -96
  64. /anemoi/datasets/create/{trace.py → input/trace.py} +0 -0
  65. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.5.dist-info}/LICENSE +0 -0
  66. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.5.dist-info}/entry_points.txt +0 -0
  67. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,115 @@
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
+ from earthkit.data.indexing.fieldlist import FieldArray
13
+ from earthkit.meteo import thermo
14
+
15
+ from .single_level_specific_humidity_to_relative_humidity import AutoDict
16
+ from .single_level_specific_humidity_to_relative_humidity import NewDataField
17
+ from .single_level_specific_humidity_to_relative_humidity import pressure_at_height_level
18
+
19
+
20
+ def execute(context, input, height, t, rh, sp, new_name="2q", **kwargs):
21
+ """Convert the single (height) level relative humidity to specific humidity"""
22
+ result = FieldArray()
23
+
24
+ MANDATORY_KEYS = ["A", "B"]
25
+ OPTIONAL_KEYS = ["t_ml", "q_ml"]
26
+ MISSING_KEYS = []
27
+ DEFAULTS = dict(t_ml="t", q_ml="q")
28
+
29
+ for key in OPTIONAL_KEYS:
30
+ if key not in kwargs:
31
+ print(f"key {key} not found in yaml-file, using default key: {DEFAULTS[key]}")
32
+ kwargs[key] = DEFAULTS[key]
33
+
34
+ for key in MANDATORY_KEYS:
35
+ if key not in kwargs:
36
+ MISSING_KEYS.append(key)
37
+
38
+ if MISSING_KEYS:
39
+ raise KeyError(f"Following keys are missing: {', '.join(MISSING_KEYS)}")
40
+
41
+ single_level_params = (t, rh, sp)
42
+ model_level_params = (kwargs["t_ml"], kwargs["q_ml"])
43
+
44
+ needed_fields = AutoDict()
45
+
46
+ # Gather all necessary fields
47
+ for f in input:
48
+ key = f.metadata(namespace="mars")
49
+ param = key.pop("param")
50
+ # check single level parameters
51
+ if param in single_level_params:
52
+ levtype = key.pop("levtype")
53
+ key = tuple(key.items())
54
+
55
+ if param in needed_fields[key][levtype]:
56
+ raise ValueError(f"Duplicate single level field {param} for {key}")
57
+
58
+ needed_fields[key][levtype][param] = f
59
+ if param == rh:
60
+ if kwargs.get("keep_rh", False):
61
+ result.append(f)
62
+ else:
63
+ result.append(f)
64
+
65
+ # check model level parameters
66
+ elif param in model_level_params:
67
+ levtype = key.pop("levtype")
68
+ levelist = key.pop("levelist")
69
+ key = tuple(key.items())
70
+
71
+ if param in needed_fields[key][levtype][levelist]:
72
+ raise ValueError(f"Duplicate model level field {param} for {key} at level {levelist}")
73
+
74
+ needed_fields[key][levtype][levelist][param] = f
75
+
76
+ # all other parameters
77
+ else:
78
+ result.append(f)
79
+
80
+ for _, values in needed_fields.items():
81
+ # some checks
82
+ if len(values["sfc"]) != 3:
83
+ raise ValueError("Missing surface fields")
84
+
85
+ rh_sl = values["sfc"][rh].to_numpy(flatten=True)
86
+ t_sl = values["sfc"][t].to_numpy(flatten=True)
87
+ sp_sl = values["sfc"][sp].to_numpy(flatten=True)
88
+
89
+ nlevels = len(kwargs["A"]) - 1
90
+ if len(values["ml"]) != nlevels:
91
+ raise ValueError("Missing model levels")
92
+
93
+ for key in values["ml"].keys():
94
+ if len(values["ml"][key]) != 2:
95
+ raise ValueError(f"Missing field on level {key}")
96
+
97
+ # create 3D arrays for upper air fields
98
+ levels = list(values["ml"].keys())
99
+ levels.sort()
100
+ t_ml = []
101
+ q_ml = []
102
+ for level in levels:
103
+ t_ml.append(values["ml"][level][kwargs["t_ml"]].to_numpy(flatten=True))
104
+ q_ml.append(values["ml"][level][kwargs["q_ml"]].to_numpy(flatten=True))
105
+
106
+ t_ml = np.stack(t_ml)
107
+ q_ml = np.stack(q_ml)
108
+
109
+ # actual conversion from rh --> q_v
110
+ p_sl = pressure_at_height_level(height, q_ml, t_ml, sp_sl, np.array(kwargs["A"]), np.array(kwargs["B"]))
111
+ q_sl = thermo.specific_humidity_from_relative_humidity(t_sl, rh_sl, p_sl)
112
+
113
+ result.append(NewDataField(values["sfc"][rh], q_sl, new_name))
114
+
115
+ return result
@@ -0,0 +1,390 @@
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
+ from earthkit.data.indexing.fieldlist import FieldArray
13
+ from earthkit.meteo import constants
14
+ from earthkit.meteo import thermo
15
+
16
+
17
+ # Alternative proposed by Baudouin Raoult
18
+ class AutoDict(dict):
19
+ def __missing__(self, key):
20
+ value = self[key] = type(self)()
21
+ return value
22
+
23
+
24
+ class NewDataField:
25
+ def __init__(self, field, data, new_name):
26
+ self.field = field
27
+ self.data = data
28
+ self.new_name = new_name
29
+
30
+ def to_numpy(self, *args, **kwargs):
31
+ return self.data
32
+
33
+ def metadata(self, key=None, **kwargs):
34
+ if key is None:
35
+ return self.field.metadata(**kwargs)
36
+
37
+ value = self.field.metadata(key, **kwargs)
38
+ if key == "param":
39
+ return self.new_name
40
+ return value
41
+
42
+ def __getattr__(self, name):
43
+ return getattr(self.field, name)
44
+
45
+
46
+ def model_level_pressure(A, B, surface_pressure):
47
+ """Calculates:
48
+ - pressure at the model full- and half-levels
49
+ - delta: depth of log(pressure) at full levels
50
+ - alpha: alpha term #TODO: more descriptive information
51
+
52
+ Parameters
53
+ ----------
54
+ A : ndarray
55
+ A-coefficients defining the model levels
56
+ B : ndarray
57
+ B-coefficients defining the model levels
58
+ surface_pressure: number or ndarray
59
+ surface pressure (Pa)
60
+
61
+ Returns
62
+ -------
63
+ ndarray
64
+ pressure at model full-levels
65
+ ndarray
66
+ pressure at model half-levels
67
+ ndarray
68
+ delta at full-levels
69
+ ndarray
70
+ alpha at full levels
71
+ """
72
+
73
+ # constants
74
+ PRESSURE_TOA = 0.1 # safety when highest pressure level = 0.0
75
+
76
+ # make the calculation agnostic to the number of dimensions
77
+ ndim = surface_pressure.ndim
78
+ new_shape_half = (A.shape[0],) + (1,) * ndim
79
+ A_reshaped = A.reshape(new_shape_half)
80
+ B_reshaped = B.reshape(new_shape_half)
81
+
82
+ # calculate pressure on model half-levels
83
+ p_half_level = A_reshaped + B_reshaped * surface_pressure[np.newaxis, ...]
84
+
85
+ # calculate delta
86
+ new_shape_full = (A.shape[0] - 1,) + surface_pressure.shape
87
+ delta = np.zeros(new_shape_full)
88
+ delta[1:, ...] = np.log(p_half_level[2:, ...] / p_half_level[1:-1, ...])
89
+
90
+ # pressure at highest half level<= 0.1
91
+ if np.any(p_half_level[0, ...] <= PRESSURE_TOA):
92
+ delta[0, ...] = np.log(p_half_level[1, ...] / PRESSURE_TOA)
93
+ # pressure at highest half level > 0.1
94
+ else:
95
+ delta[0, ...] = np.log(p_half_level[1, ...] / p_half_level[0, ...])
96
+
97
+ # calculate alpha
98
+ alpha = np.zeros(new_shape_full)
99
+
100
+ alpha[1:, ...] = 1.0 - p_half_level[1:-1, ...] / (p_half_level[2:, ...] - p_half_level[1:-1, ...]) * delta[1:, ...]
101
+
102
+ # pressure at highest half level <= 0.1
103
+ if np.any(p_half_level[0, ...] <= PRESSURE_TOA):
104
+ alpha[0, ...] = 1.0 # ARPEGE choice, ECMWF IFS uses log(2)
105
+ # pressure at highest half level > 0.1
106
+ else:
107
+ alpha[0, ...] = 1.0 - p_half_level[0, ...] / (p_half_level[1, ...] - p_half_level[0, ...]) * delta[0, ...]
108
+
109
+ # calculate pressure on model full levels
110
+ # TODO: is there a faster way to calculate the averages?
111
+ # TODO: introduce option to calculate full levels in more complicated way
112
+ p_full_level = np.apply_along_axis(lambda m: np.convolve(m, np.ones(2) / 2, mode="valid"), axis=0, arr=p_half_level)
113
+
114
+ return p_full_level, p_half_level, delta, alpha
115
+
116
+
117
+ def calc_specific_gas_constant(q):
118
+ """Calculates the specific gas constant of moist air
119
+ (specific content of cloud particles and hydrometeors are neglected)
120
+
121
+ Parameters
122
+ ----------
123
+ q : number or ndarray
124
+ specific humidity
125
+
126
+ Returns
127
+ -------
128
+ number or ndarray
129
+ specific gas constant of moist air
130
+ """
131
+
132
+ R = constants.Rd + (constants.Rv - constants.Rd) * q
133
+ return R
134
+
135
+
136
+ def relative_geopotential_thickness(alpha, q, T):
137
+ """Calculates the geopotential thickness w.r.t the surface on model full-levels
138
+
139
+ Parameters
140
+ ----------
141
+ alpha : ndarray
142
+ alpha term of pressure calculations
143
+ q : ndarray
144
+ specific humidity (in kg/kg) on model full-levels
145
+ T : ndarray
146
+ temperature (in Kelvin) on model full-levels
147
+
148
+ Returns
149
+ -------
150
+ ndarray
151
+ geopotential thickness of model full-levels w.r.t. the surface
152
+ """
153
+
154
+ R = calc_specific_gas_constant(q)
155
+ dphi = np.cumsum(np.flip(alpha * R * T, axis=0), axis=0)
156
+ dphi = np.flip(dphi, axis=0)
157
+
158
+ return dphi
159
+
160
+
161
+ def pressure_at_height_level(height, q, T, sp, A, B):
162
+ """Calculates the pressure at a height level given in meters above surface.
163
+ This is done by finding the model level above and below the specified height
164
+ and interpolating the pressure
165
+
166
+ Parameters
167
+ ----------
168
+ height : number
169
+ height (in meters) above the surface for which the pressure is wanted
170
+ q : ndarray
171
+ specific humidity (kg/kg) at model full-levels
172
+ T : ndarray
173
+ temperature (K) at model full-levels
174
+ sp : ndarray
175
+ surface pressure (Pa)
176
+ A : ndarray
177
+ A-coefficients defining the model levels
178
+ B : ndarray
179
+ B-coefficients defining the model levels
180
+
181
+ Returns
182
+ -------
183
+ number or ndarray
184
+ pressure (Pa) at the given height level
185
+ """
186
+
187
+ # geopotential thickness of the height level
188
+ tdphi = height * constants.g
189
+
190
+ # pressure(-related) variables
191
+ p_full, p_half, _, alpha = model_level_pressure(A, B, sp)
192
+
193
+ # relative geopot. thickness of full levels
194
+ dphi = relative_geopotential_thickness(alpha, q, T)
195
+
196
+ # find the model full level right above the height level
197
+ i_phi = (tdphi > dphi).sum(0)
198
+
199
+ # initialize the output array
200
+ p_height = np.zeros_like(i_phi, dtype=np.float64)
201
+
202
+ # define mask: requested height is below the lowest model full-level
203
+ mask = i_phi == 0
204
+
205
+ # CASE 1: requested height is below the lowest model full-level
206
+ # --> interpolation between surface pressure and lowest model full-level
207
+ p_height[mask] = (p_half[-1, ...] + tdphi / dphi[-1, ...] * (p_full[-1, ...] - p_half[-1, ...]))[mask]
208
+
209
+ # CASE 2: requested height is above the lowest model full-level
210
+ # --> interpolation between between model full-level above and below
211
+
212
+ # define some indices for masking and readability
213
+ i_lev = alpha.shape[0] - i_phi - 1 # convert phi index to model level index
214
+ indices = np.indices(i_lev.shape)
215
+ masked_indices = tuple(dim[~mask] for dim in indices)
216
+ above = (i_lev[~mask],) + masked_indices
217
+ below = (i_lev[~mask] + 1,) + masked_indices
218
+
219
+ dphi_above = dphi[above]
220
+ dphi_below = dphi[below]
221
+
222
+ factor = (tdphi - dphi_above) / (dphi_below - dphi_above)
223
+ p_height[~mask] = p_full[above] + factor * (p_full[below] - p_full[above])
224
+
225
+ return p_height
226
+
227
+
228
+ def execute(context, input, height, t, q, sp, new_name="2r", **kwargs):
229
+ """Convert the single (height) level specific humidity to relative humidity"""
230
+ result = FieldArray()
231
+
232
+ MANDATORY_KEYS = ["A", "B"]
233
+ OPTIONAL_KEYS = ["t_ml", "q_ml"]
234
+ MISSING_KEYS = []
235
+ DEFAULTS = dict(t_ml="t", q_ml="q")
236
+
237
+ for key in OPTIONAL_KEYS:
238
+ if key not in kwargs:
239
+ print(f"key {key} not found in yaml-file, using default key: {DEFAULTS[key]}")
240
+ kwargs[key] = DEFAULTS[key]
241
+
242
+ for key in MANDATORY_KEYS:
243
+ if key not in kwargs:
244
+ MISSING_KEYS.append(key)
245
+
246
+ if MISSING_KEYS:
247
+ raise KeyError(f"Following keys are missing: {', '.join(MISSING_KEYS)}")
248
+
249
+ single_level_params = (t, q, sp)
250
+ model_level_params = (kwargs["t_ml"], kwargs["q_ml"])
251
+
252
+ needed_fields = AutoDict()
253
+
254
+ # Gather all necessary fields
255
+ for f in input:
256
+ key = f.metadata(namespace="mars")
257
+ param = key.pop("param")
258
+ # check single level parameters
259
+ if param in single_level_params:
260
+ levtype = key.pop("levtype")
261
+ key = tuple(sorted(key.items()))
262
+
263
+ if param in needed_fields[key][levtype]:
264
+ raise ValueError(f"Duplicate single level field {param} for {key}")
265
+
266
+ needed_fields[key][levtype][param] = f
267
+ if param == q:
268
+ if kwargs.get("keep_q", False):
269
+ result.append(f)
270
+ else:
271
+ result.append(f)
272
+
273
+ # check model level parameters
274
+ elif param in model_level_params:
275
+ levtype = key.pop("levtype")
276
+ levelist = key.pop("levelist")
277
+ key = tuple(sorted(key.items()))
278
+
279
+ if param in needed_fields[key][levtype][levelist]:
280
+ raise ValueError(f"Duplicate model level field {param} for {key} at level {levelist}")
281
+
282
+ needed_fields[key][levtype][levelist][param] = f
283
+
284
+ # all other parameters
285
+ else:
286
+ result.append(f)
287
+
288
+ for _, values in needed_fields.items():
289
+ # some checks
290
+ if len(values["sfc"]) != 3:
291
+ raise ValueError("Missing surface fields")
292
+
293
+ q_sl = values["sfc"][q].to_numpy(flatten=True)
294
+ t_sl = values["sfc"][t].to_numpy(flatten=True)
295
+ sp_sl = values["sfc"][sp].to_numpy(flatten=True)
296
+
297
+ nlevels = len(kwargs["A"]) - 1
298
+ if len(values["ml"]) != nlevels:
299
+ raise ValueError("Missing model levels")
300
+
301
+ for key in values["ml"].keys():
302
+ if len(values["ml"][key]) != 2:
303
+ raise ValueError(f"Missing field on level {key}")
304
+
305
+ # create 3D arrays for upper air fields
306
+ levels = list(values["ml"].keys())
307
+ levels.sort()
308
+ t_ml = []
309
+ q_ml = []
310
+ for level in levels:
311
+ t_ml.append(values["ml"][level][kwargs["t_ml"]].to_numpy(flatten=True))
312
+ q_ml.append(values["ml"][level][kwargs["q_ml"]].to_numpy(flatten=True))
313
+
314
+ t_ml = np.stack(t_ml)
315
+ q_ml = np.stack(q_ml)
316
+
317
+ # actual conversion from qv --> rh
318
+ # FIXME:
319
+ # For now We need to go from qv --> td --> rh to take into account
320
+ # the mixed / ice phase when T ~ 0C / T < 0C
321
+ # See https://github.com/ecmwf/earthkit-meteo/issues/15
322
+ p_sl = pressure_at_height_level(height, q_ml, t_ml, sp_sl, np.array(kwargs["A"]), np.array(kwargs["B"]))
323
+ td_sl = thermo.dewpoint_from_specific_humidity(q=q_sl, p=p_sl)
324
+ rh_sl = thermo.relative_humidity_from_dewpoint(t=t_sl, td=td_sl)
325
+
326
+ result.append(NewDataField(values["sfc"][q], rh_sl, new_name))
327
+
328
+ return result
329
+
330
+
331
+ def test():
332
+ from earthkit.data import from_source
333
+ from earthkit.data.readers.grib.index import GribFieldList
334
+
335
+ # IFS forecasts have both specific humidity and dewpoint
336
+ sl = from_source(
337
+ "mars",
338
+ {
339
+ "date": "2022-01-01",
340
+ "class": "od",
341
+ "expver": "1",
342
+ "stream": "oper",
343
+ "levtype": "sfc",
344
+ "param": "96.174/134.128/167.128/168.128",
345
+ "time": "00:00:00",
346
+ "type": "fc",
347
+ "step": "2",
348
+ "grid": "O640",
349
+ },
350
+ )
351
+
352
+ ml = from_source(
353
+ "mars",
354
+ {
355
+ "date": "2022-01-01",
356
+ "class": "od",
357
+ "expver": "1",
358
+ "stream": "oper",
359
+ "levtype": "ml",
360
+ "levelist": "130/131/132/133/134/135/136/137",
361
+ "param": "130/133",
362
+ "time": "00:00:00",
363
+ "type": "fc",
364
+ "step": "2",
365
+ "grid": "O640",
366
+ },
367
+ )
368
+ source = GribFieldList.merge([sl, ml])
369
+
370
+ # IFS A and B coeffients for level 137 - 129
371
+ kwargs = {
372
+ "A": [424.414063, 302.476563, 202.484375, 122.101563, 62.781250, 22.835938, 3.757813, 0.0, 0.0],
373
+ "B": [0.969513, 0.975078, 0.980072, 0.984542, 0.988500, 0.991984, 0.995003, 0.997630, 1.000000],
374
+ }
375
+ source = execute(None, source, 2, "2t", "2sh", "sp", "2r", **kwargs)
376
+
377
+ temperature = source[2].to_numpy(flatten=True)
378
+ dewpoint = source[3].to_numpy(flatten=True)
379
+ relhum = source[4].to_numpy()
380
+ newdew = thermo.dewpoint_from_relative_humidity(temperature, relhum)
381
+
382
+ print(f"Mean difference in dewpoint temperature: {np.abs(newdew - dewpoint).mean():02f} degC")
383
+ print(f"Median difference in dewpoint temperature: {np.median(np.abs(newdew - dewpoint)):02f} degC")
384
+ print(f"Maximum difference in dewpoint temperature: {np.abs(newdew - dewpoint).max():02f} degC")
385
+
386
+ # source.save("source.grib")
387
+
388
+
389
+ if __name__ == "__main__":
390
+ test()
@@ -0,0 +1,77 @@
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
+ from collections import defaultdict
11
+
12
+ import numpy as np
13
+ from earthkit.data.indexing.fieldlist import FieldArray
14
+ from earthkit.meteo.wind.array import polar_to_xy
15
+
16
+
17
+ class NewDataField:
18
+ def __init__(self, field, data, new_name):
19
+ self.field = field
20
+ self.data = data
21
+ self.new_name = new_name
22
+
23
+ def to_numpy(self, *args, **kwargs):
24
+ return self.data
25
+
26
+ def metadata(self, key=None, **kwargs):
27
+ if key is None:
28
+ return self.field.metadata(**kwargs)
29
+
30
+ value = self.field.metadata(key, **kwargs)
31
+ if key == "param":
32
+ return self.new_name
33
+ return value
34
+
35
+ def __getattr__(self, name):
36
+ return getattr(self.field, name)
37
+
38
+
39
+ def execute(context, input, wind_speed, wind_dir, u_component="u", v_component="v", in_radians=False):
40
+
41
+ result = FieldArray()
42
+
43
+ wind_params = (wind_speed, wind_dir)
44
+ wind_pairs = defaultdict(dict)
45
+
46
+ for f in input:
47
+ key = f.metadata(namespace="mars")
48
+ param = key.pop("param")
49
+
50
+ if param not in wind_params:
51
+ result.append(f)
52
+ continue
53
+
54
+ key = tuple(key.items())
55
+
56
+ if param in wind_pairs[key]:
57
+ raise ValueError(f"Duplicate wind component {param} for {key}")
58
+
59
+ wind_pairs[key][param] = f
60
+
61
+ for _, pairs in wind_pairs.items():
62
+ if len(pairs) != 2:
63
+ raise ValueError("Missing wind component")
64
+
65
+ magnitude = pairs[wind_speed]
66
+ direction = pairs[wind_dir]
67
+
68
+ # assert speed.grid_mapping == dir.grid_mapping
69
+ if in_radians:
70
+ direction = np.rad2deg(direction)
71
+
72
+ u, v = polar_to_xy(magnitude.to_numpy(flatten=True), direction.to_numpy(flatten=True))
73
+
74
+ result.append(NewDataField(magnitude, u, u_component))
75
+ result.append(NewDataField(direction, v, v_component))
76
+
77
+ return result
@@ -0,0 +1,55 @@
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
+ from collections import defaultdict
11
+
12
+ import numpy as np
13
+ from earthkit.data.indexing.fieldlist import FieldArray
14
+ from earthkit.meteo.wind.array import xy_to_polar
15
+
16
+ from anemoi.datasets.create.functions.filters.speeddir_to_uv import NewDataField
17
+
18
+
19
+ def execute(context, input, u_component, v_component, wind_speed, wind_dir, in_radians=False):
20
+ result = FieldArray()
21
+
22
+ wind_params = (u_component, v_component)
23
+ wind_pairs = defaultdict(dict)
24
+
25
+ for f in input:
26
+ key = f.metadata(namespace="mars")
27
+ param = key.pop("param")
28
+
29
+ if param not in wind_params:
30
+ result.append(f)
31
+ continue
32
+
33
+ key = tuple(key.items())
34
+
35
+ if param in wind_pairs[key]:
36
+ raise ValueError(f"Duplicate wind component {param} for {key}")
37
+
38
+ wind_pairs[key][param] = f
39
+
40
+ for _, pairs in wind_pairs.items():
41
+ if len(pairs) != 2:
42
+ raise ValueError("Missing wind component")
43
+
44
+ u = pairs[u_component]
45
+ v = pairs[v_component]
46
+
47
+ # assert speed.grid_mapping == dir.grid_mapping
48
+ magnitude, direction = xy_to_polar(u.to_numpy(flatten=True), v.to_numpy(flatten=True))
49
+ if in_radians:
50
+ direction = np.deg2rad(direction)
51
+
52
+ result.append(NewDataField(u, magnitude, wind_speed))
53
+ result.append(NewDataField(v, direction, wind_dir))
54
+
55
+ return result
@@ -16,6 +16,10 @@ LOG = logging.getLogger(__name__)
16
16
 
17
17
 
18
18
  def _expand(paths):
19
+
20
+ if not isinstance(paths, list):
21
+ paths = [paths]
22
+
19
23
  for path in paths:
20
24
  if path.startswith("file://"):
21
25
  path = path[7:]
@@ -40,8 +44,10 @@ def iterate_patterns(path, dates, **kwargs):
40
44
  given_paths = path if isinstance(path, list) else [path]
41
45
 
42
46
  dates = [d.isoformat() for d in dates]
47
+ if len(dates) > 0:
48
+ kwargs["date"] = dates
43
49
 
44
50
  for path in given_paths:
45
- paths = Pattern(path, ignore_missing_keys=True).substitute(date=dates, **kwargs)
51
+ paths = Pattern(path, ignore_missing_keys=True).substitute(**kwargs)
46
52
  for path in _expand(paths):
47
53
  yield path, dates
@@ -375,6 +375,8 @@ def accumulations(context, dates, **request):
375
375
  ("od", "elda"): dict(base_times=(6, 18)),
376
376
  ("ea", "oper"): dict(data_accumulation_period=1, base_times=(6, 18)),
377
377
  ("ea", "enda"): dict(data_accumulation_period=3, base_times=(6, 18)),
378
+ ("rr", "oper"): dict(data_accumulation_period=3, base_times=(0, 3, 6, 9, 12, 15, 18, 21)),
379
+ ("l5", "oper"): dict(data_accumulation_period=1, base_times=(0,)),
378
380
  }
379
381
 
380
382
  kwargs = KWARGS.get((class_, stream), {})