anemoi-datasets 0.5.24__py3-none-any.whl → 0.5.26__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.
- anemoi/datasets/_version.py +2 -2
- anemoi/datasets/commands/finalise-additions.py +2 -1
- anemoi/datasets/commands/finalise.py +2 -1
- anemoi/datasets/commands/grib-index.py +1 -1
- anemoi/datasets/commands/init-additions.py +2 -1
- anemoi/datasets/commands/load-additions.py +2 -1
- anemoi/datasets/commands/load.py +2 -1
- anemoi/datasets/create/__init__.py +24 -33
- anemoi/datasets/create/filter.py +22 -24
- anemoi/datasets/create/input/__init__.py +0 -20
- anemoi/datasets/create/input/step.py +2 -16
- anemoi/datasets/create/sources/accumulations.py +7 -6
- anemoi/datasets/create/sources/planetary_computer.py +44 -0
- anemoi/datasets/create/sources/xarray_support/__init__.py +6 -22
- anemoi/datasets/create/sources/xarray_support/coordinates.py +8 -0
- anemoi/datasets/create/sources/xarray_support/field.py +1 -4
- anemoi/datasets/create/sources/xarray_support/flavour.py +44 -6
- anemoi/datasets/create/sources/xarray_support/patch.py +44 -1
- anemoi/datasets/create/sources/xarray_support/variable.py +6 -2
- anemoi/datasets/data/complement.py +44 -10
- anemoi/datasets/data/dataset.py +29 -0
- anemoi/datasets/data/forwards.py +8 -2
- anemoi/datasets/data/misc.py +74 -16
- anemoi/datasets/data/observations/__init__.py +316 -0
- anemoi/datasets/data/observations/legacy_obs_dataset.py +200 -0
- anemoi/datasets/data/observations/multi.py +64 -0
- anemoi/datasets/data/padded.py +227 -0
- anemoi/datasets/data/records/__init__.py +442 -0
- anemoi/datasets/data/records/backends/__init__.py +157 -0
- anemoi/datasets/data/stores.py +7 -56
- anemoi/datasets/data/subset.py +5 -0
- anemoi/datasets/grids.py +6 -3
- {anemoi_datasets-0.5.24.dist-info → anemoi_datasets-0.5.26.dist-info}/METADATA +3 -2
- {anemoi_datasets-0.5.24.dist-info → anemoi_datasets-0.5.26.dist-info}/RECORD +38 -51
- {anemoi_datasets-0.5.24.dist-info → anemoi_datasets-0.5.26.dist-info}/WHEEL +1 -1
- anemoi/datasets/create/filters/__init__.py +0 -33
- anemoi/datasets/create/filters/empty.py +0 -37
- anemoi/datasets/create/filters/legacy.py +0 -93
- anemoi/datasets/create/filters/noop.py +0 -37
- anemoi/datasets/create/filters/orog_to_z.py +0 -58
- anemoi/datasets/create/filters/pressure_level_relative_humidity_to_specific_humidity.py +0 -83
- anemoi/datasets/create/filters/pressure_level_specific_humidity_to_relative_humidity.py +0 -84
- anemoi/datasets/create/filters/rename.py +0 -205
- anemoi/datasets/create/filters/rotate_winds.py +0 -105
- anemoi/datasets/create/filters/single_level_dewpoint_to_relative_humidity.py +0 -78
- anemoi/datasets/create/filters/single_level_relative_humidity_to_dewpoint.py +0 -84
- anemoi/datasets/create/filters/single_level_relative_humidity_to_specific_humidity.py +0 -163
- anemoi/datasets/create/filters/single_level_specific_humidity_to_relative_humidity.py +0 -451
- anemoi/datasets/create/filters/speeddir_to_uv.py +0 -95
- anemoi/datasets/create/filters/sum.py +0 -68
- anemoi/datasets/create/filters/transform.py +0 -51
- anemoi/datasets/create/filters/unrotate_winds.py +0 -105
- anemoi/datasets/create/filters/uv_to_speeddir.py +0 -94
- anemoi/datasets/create/filters/wz_to_w.py +0 -98
- anemoi/datasets/create/testing.py +0 -76
- {anemoi_datasets-0.5.24.dist-info → anemoi_datasets-0.5.26.dist-info}/entry_points.txt +0 -0
- {anemoi_datasets-0.5.24.dist-info → anemoi_datasets-0.5.26.dist-info}/licenses/LICENSE +0 -0
- {anemoi_datasets-0.5.24.dist-info → anemoi_datasets-0.5.26.dist-info}/top_level.txt +0 -0
|
@@ -1,451 +0,0 @@
|
|
|
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 typing import Any
|
|
12
|
-
from typing import Dict
|
|
13
|
-
from typing import List
|
|
14
|
-
from typing import Tuple
|
|
15
|
-
from typing import Union
|
|
16
|
-
|
|
17
|
-
import earthkit.data as ekd
|
|
18
|
-
import numpy as np
|
|
19
|
-
from anemoi.transform.fields import new_field_from_numpy
|
|
20
|
-
from anemoi.transform.fields import new_fieldlist_from_list
|
|
21
|
-
from earthkit.meteo import constants
|
|
22
|
-
from earthkit.meteo import thermo
|
|
23
|
-
from numpy.typing import NDArray
|
|
24
|
-
|
|
25
|
-
from .legacy import legacy_filter
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# Alternative proposed by Baudouin Raoult
|
|
29
|
-
class AutoDict(dict):
|
|
30
|
-
"""A dictionary that automatically creates nested dictionaries for missing keys."""
|
|
31
|
-
|
|
32
|
-
def __missing__(self, key: Any) -> Any:
|
|
33
|
-
"""Handle missing keys by creating nested dictionaries.
|
|
34
|
-
|
|
35
|
-
Parameters
|
|
36
|
-
----------
|
|
37
|
-
key : Any
|
|
38
|
-
The missing key.
|
|
39
|
-
|
|
40
|
-
Returns
|
|
41
|
-
-------
|
|
42
|
-
Any
|
|
43
|
-
A new nested dictionary.
|
|
44
|
-
"""
|
|
45
|
-
value = self[key] = type(self)()
|
|
46
|
-
return value
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def model_level_pressure(
|
|
50
|
-
A: NDArray[Any], B: NDArray[Any], surface_pressure: Union[float, np.ndarray]
|
|
51
|
-
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
52
|
-
"""Calculates:
|
|
53
|
-
- pressure at the model full- and half-levels
|
|
54
|
-
- delta: depth of log(pressure) at full levels
|
|
55
|
-
- alpha: alpha term #TODO: more descriptive information.
|
|
56
|
-
|
|
57
|
-
Parameters
|
|
58
|
-
----------
|
|
59
|
-
A : ndarray
|
|
60
|
-
A-coefficients defining the model levels
|
|
61
|
-
B : ndarray
|
|
62
|
-
B-coefficients defining the model levels
|
|
63
|
-
surface_pressure : number or ndarray
|
|
64
|
-
surface pressure (Pa)
|
|
65
|
-
|
|
66
|
-
Returns
|
|
67
|
-
-------
|
|
68
|
-
ndarray
|
|
69
|
-
pressure at model full-levels
|
|
70
|
-
ndarray
|
|
71
|
-
pressure at model half-levels
|
|
72
|
-
ndarray
|
|
73
|
-
delta at full-levels
|
|
74
|
-
ndarray
|
|
75
|
-
alpha at full levels
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
# constants
|
|
79
|
-
PRESSURE_TOA = 0.1 # safety when highest pressure level = 0.0
|
|
80
|
-
|
|
81
|
-
# make the calculation agnostic to the number of dimensions
|
|
82
|
-
ndim = surface_pressure.ndim
|
|
83
|
-
new_shape_half = (A.shape[0],) + (1,) * ndim
|
|
84
|
-
A_reshaped = A.reshape(new_shape_half)
|
|
85
|
-
B_reshaped = B.reshape(new_shape_half)
|
|
86
|
-
|
|
87
|
-
# calculate pressure on model half-levels
|
|
88
|
-
p_half_level = A_reshaped + B_reshaped * surface_pressure[np.newaxis, ...]
|
|
89
|
-
|
|
90
|
-
# calculate delta
|
|
91
|
-
new_shape_full = (A.shape[0] - 1,) + surface_pressure.shape
|
|
92
|
-
delta = np.zeros(new_shape_full)
|
|
93
|
-
delta[1:, ...] = np.log(p_half_level[2:, ...] / p_half_level[1:-1, ...])
|
|
94
|
-
|
|
95
|
-
# pressure at highest half level<= 0.1
|
|
96
|
-
if np.any(p_half_level[0, ...] <= PRESSURE_TOA):
|
|
97
|
-
delta[0, ...] = np.log(p_half_level[1, ...] / PRESSURE_TOA)
|
|
98
|
-
# pressure at highest half level > 0.1
|
|
99
|
-
else:
|
|
100
|
-
delta[0, ...] = np.log(p_half_level[1, ...] / p_half_level[0, ...])
|
|
101
|
-
|
|
102
|
-
# calculate alpha
|
|
103
|
-
alpha = np.zeros(new_shape_full)
|
|
104
|
-
|
|
105
|
-
alpha[1:, ...] = 1.0 - p_half_level[1:-1, ...] / (p_half_level[2:, ...] - p_half_level[1:-1, ...]) * delta[1:, ...]
|
|
106
|
-
|
|
107
|
-
# pressure at highest half level <= 0.1
|
|
108
|
-
if np.any(p_half_level[0, ...] <= PRESSURE_TOA):
|
|
109
|
-
alpha[0, ...] = 1.0 # ARPEGE choice, ECMWF IFS uses log(2)
|
|
110
|
-
# pressure at highest half level > 0.1
|
|
111
|
-
else:
|
|
112
|
-
alpha[0, ...] = 1.0 - p_half_level[0, ...] / (p_half_level[1, ...] - p_half_level[0, ...]) * delta[0, ...]
|
|
113
|
-
|
|
114
|
-
# calculate pressure on model full levels
|
|
115
|
-
# TODO: is there a faster way to calculate the averages?
|
|
116
|
-
# TODO: introduce option to calculate full levels in more complicated way
|
|
117
|
-
p_full_level = np.apply_along_axis(lambda m: np.convolve(m, np.ones(2) / 2, mode="valid"), axis=0, arr=p_half_level)
|
|
118
|
-
|
|
119
|
-
return p_full_level, p_half_level, delta, alpha
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def calc_specific_gas_constant(q: Union[float, np.ndarray]) -> Union[float, NDArray[Any]]:
|
|
123
|
-
"""Calculates the specific gas constant of moist air
|
|
124
|
-
(specific content of cloud particles and hydrometeors are neglected).
|
|
125
|
-
|
|
126
|
-
Parameters
|
|
127
|
-
----------
|
|
128
|
-
q : number or ndarray
|
|
129
|
-
specific humidity
|
|
130
|
-
|
|
131
|
-
Returns
|
|
132
|
-
-------
|
|
133
|
-
number or ndarray
|
|
134
|
-
specific gas constant of moist air
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
R = constants.Rd + (constants.Rv - constants.Rd) * q
|
|
138
|
-
return R
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def relative_geopotential_thickness(alpha: NDArray[Any], q: NDArray[Any], T: NDArray[Any]) -> NDArray[Any]:
|
|
142
|
-
"""Calculates the geopotential thickness w.r.t the surface on model full-levels.
|
|
143
|
-
|
|
144
|
-
Parameters
|
|
145
|
-
----------
|
|
146
|
-
alpha : ndarray
|
|
147
|
-
alpha term of pressure calculations
|
|
148
|
-
q : ndarray
|
|
149
|
-
specific humidity (in kg/kg) on model full-levels
|
|
150
|
-
T : ndarray
|
|
151
|
-
temperature (in Kelvin) on model full-levels
|
|
152
|
-
|
|
153
|
-
Returns
|
|
154
|
-
-------
|
|
155
|
-
ndarray
|
|
156
|
-
geopotential thickness of model full-levels w.r.t. the surface
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
R = calc_specific_gas_constant(q)
|
|
160
|
-
dphi = np.cumsum(np.flip(alpha * R * T, axis=0), axis=0)
|
|
161
|
-
dphi = np.flip(dphi, axis=0)
|
|
162
|
-
|
|
163
|
-
return dphi
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def pressure_at_height_level(
|
|
167
|
-
height: float, q: NDArray[Any], T: NDArray[Any], sp: NDArray[Any], A: NDArray[Any], B: NDArray[Any]
|
|
168
|
-
) -> Union[float, NDArray[Any]]:
|
|
169
|
-
"""Calculates the pressure at a height level given in meters above surface.
|
|
170
|
-
This is done by finding the model level above and below the specified height
|
|
171
|
-
and interpolating the pressure.
|
|
172
|
-
|
|
173
|
-
Parameters
|
|
174
|
-
----------
|
|
175
|
-
height : number
|
|
176
|
-
height (in meters) above the surface for which the pressure is wanted
|
|
177
|
-
q : ndarray
|
|
178
|
-
specific humidity (kg/kg) at model full-levels
|
|
179
|
-
T : ndarray
|
|
180
|
-
temperature (K) at model full-levels
|
|
181
|
-
sp : ndarray
|
|
182
|
-
surface pressure (Pa)
|
|
183
|
-
A : ndarray
|
|
184
|
-
A-coefficients defining the model levels
|
|
185
|
-
B : ndarray
|
|
186
|
-
B-coefficients defining the model levels
|
|
187
|
-
|
|
188
|
-
Returns
|
|
189
|
-
-------
|
|
190
|
-
number or ndarray
|
|
191
|
-
pressure (Pa) at the given height level
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
# geopotential thickness of the height level
|
|
195
|
-
tdphi = height * constants.g
|
|
196
|
-
|
|
197
|
-
# pressure(-related) variables
|
|
198
|
-
p_full, p_half, _, alpha = model_level_pressure(A, B, sp)
|
|
199
|
-
|
|
200
|
-
# relative geopot. thickness of full levels
|
|
201
|
-
dphi = relative_geopotential_thickness(alpha, q, T)
|
|
202
|
-
|
|
203
|
-
# find the model full level right above the height level
|
|
204
|
-
i_phi = (tdphi > dphi).sum(0)
|
|
205
|
-
|
|
206
|
-
# initialize the output array
|
|
207
|
-
p_height = np.zeros_like(i_phi, dtype=np.float64)
|
|
208
|
-
|
|
209
|
-
# define mask: requested height is below the lowest model full-level
|
|
210
|
-
mask = i_phi == 0
|
|
211
|
-
|
|
212
|
-
# CASE 1: requested height is below the lowest model full-level
|
|
213
|
-
# --> interpolation between surface pressure and lowest model full-level
|
|
214
|
-
p_height[mask] = (p_half[-1, ...] + tdphi / dphi[-1, ...] * (p_full[-1, ...] - p_half[-1, ...]))[mask]
|
|
215
|
-
|
|
216
|
-
# CASE 2: requested height is above the lowest model full-level
|
|
217
|
-
# --> interpolation between between model full-level above and below
|
|
218
|
-
|
|
219
|
-
# define some indices for masking and readability
|
|
220
|
-
i_lev = alpha.shape[0] - i_phi - 1 # convert phi index to model level index
|
|
221
|
-
indices = np.indices(i_lev.shape)
|
|
222
|
-
masked_indices = tuple(dim[~mask] for dim in indices)
|
|
223
|
-
above = (i_lev[~mask],) + masked_indices
|
|
224
|
-
below = (i_lev[~mask] + 1,) + masked_indices
|
|
225
|
-
|
|
226
|
-
dphi_above = dphi[above]
|
|
227
|
-
dphi_below = dphi[below]
|
|
228
|
-
|
|
229
|
-
factor = (tdphi - dphi_above) / (dphi_below - dphi_above)
|
|
230
|
-
p_height[~mask] = p_full[above] + factor * (p_full[below] - p_full[above])
|
|
231
|
-
|
|
232
|
-
return p_height
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
@legacy_filter(__file__)
|
|
236
|
-
def execute(
|
|
237
|
-
context: Any,
|
|
238
|
-
input: List[Any],
|
|
239
|
-
height: float,
|
|
240
|
-
t: str,
|
|
241
|
-
q: str,
|
|
242
|
-
sp: str,
|
|
243
|
-
new_name: str = "2r",
|
|
244
|
-
**kwargs: Dict[str, Any],
|
|
245
|
-
) -> ekd.FieldList:
|
|
246
|
-
"""Convert the single (height) level specific humidity to relative humidity.
|
|
247
|
-
|
|
248
|
-
Parameters
|
|
249
|
-
----------
|
|
250
|
-
context : Any
|
|
251
|
-
The context for the execution.
|
|
252
|
-
input : list of Any
|
|
253
|
-
The input data.
|
|
254
|
-
height : float
|
|
255
|
-
The height level in meters.
|
|
256
|
-
t : str
|
|
257
|
-
The temperature parameter name.
|
|
258
|
-
q : str
|
|
259
|
-
The specific humidity parameter name.
|
|
260
|
-
sp : str
|
|
261
|
-
The surface pressure parameter name.
|
|
262
|
-
new_name : str, optional
|
|
263
|
-
The new name for the relative humidity parameter, by default "2r".
|
|
264
|
-
**kwargs : dict
|
|
265
|
-
Additional keyword arguments.
|
|
266
|
-
t_ml : str, optional
|
|
267
|
-
The temperature parameter name for model levels, by default "t".
|
|
268
|
-
q_ml : str, optional
|
|
269
|
-
The specific humidity parameter name for model levels, by default "q".
|
|
270
|
-
A : list of float
|
|
271
|
-
A-coefficients defining the model levels.
|
|
272
|
-
B : list of float
|
|
273
|
-
B-coefficients defining the model levels.
|
|
274
|
-
keep_q : bool, optional
|
|
275
|
-
Whether to keep the specific humidity field in the result, by default False.
|
|
276
|
-
|
|
277
|
-
Returns
|
|
278
|
-
-------
|
|
279
|
-
ekd.FieldList
|
|
280
|
-
The resulting field array with relative humidity.
|
|
281
|
-
"""
|
|
282
|
-
result = []
|
|
283
|
-
|
|
284
|
-
MANDATORY_KEYS = ["A", "B"]
|
|
285
|
-
OPTIONAL_KEYS = ["t_ml", "q_ml"]
|
|
286
|
-
MISSING_KEYS = []
|
|
287
|
-
DEFAULTS = dict(t_ml="t", q_ml="q")
|
|
288
|
-
|
|
289
|
-
for key in OPTIONAL_KEYS:
|
|
290
|
-
if key not in kwargs:
|
|
291
|
-
print(f"key {key} not found in yaml-file, using default key: {DEFAULTS[key]}")
|
|
292
|
-
kwargs[key] = DEFAULTS[key]
|
|
293
|
-
|
|
294
|
-
for key in MANDATORY_KEYS:
|
|
295
|
-
if key not in kwargs:
|
|
296
|
-
MISSING_KEYS.append(key)
|
|
297
|
-
|
|
298
|
-
if MISSING_KEYS:
|
|
299
|
-
raise KeyError(f"Following keys are missing: {', '.join(MISSING_KEYS)}")
|
|
300
|
-
|
|
301
|
-
single_level_params = (t, q, sp)
|
|
302
|
-
model_level_params = (kwargs["t_ml"], kwargs["q_ml"])
|
|
303
|
-
|
|
304
|
-
needed_fields = AutoDict()
|
|
305
|
-
|
|
306
|
-
# Gather all necessary fields
|
|
307
|
-
for f in input:
|
|
308
|
-
key = f.metadata(namespace="mars")
|
|
309
|
-
param = key.pop("param")
|
|
310
|
-
# check single level parameters
|
|
311
|
-
if param in single_level_params:
|
|
312
|
-
levtype = key.pop("levtype")
|
|
313
|
-
key = tuple(sorted(key.items()))
|
|
314
|
-
|
|
315
|
-
if param in needed_fields[key][levtype]:
|
|
316
|
-
raise ValueError(f"Duplicate single level field {param} for {key}")
|
|
317
|
-
|
|
318
|
-
needed_fields[key][levtype][param] = f
|
|
319
|
-
if param == q:
|
|
320
|
-
if kwargs.get("keep_q", False):
|
|
321
|
-
result.append(f)
|
|
322
|
-
else:
|
|
323
|
-
result.append(f)
|
|
324
|
-
|
|
325
|
-
# check model level parameters
|
|
326
|
-
elif param in model_level_params:
|
|
327
|
-
levtype = key.pop("levtype")
|
|
328
|
-
levelist = key.pop("levelist")
|
|
329
|
-
key = tuple(sorted(key.items()))
|
|
330
|
-
|
|
331
|
-
if param in needed_fields[key][levtype][levelist]:
|
|
332
|
-
raise ValueError(f"Duplicate model level field {param} for {key} at level {levelist}")
|
|
333
|
-
|
|
334
|
-
needed_fields[key][levtype][levelist][param] = f
|
|
335
|
-
|
|
336
|
-
# all other parameters
|
|
337
|
-
else:
|
|
338
|
-
result.append(f)
|
|
339
|
-
|
|
340
|
-
for _, values in needed_fields.items():
|
|
341
|
-
# some checks
|
|
342
|
-
if len(values["sfc"]) != 3:
|
|
343
|
-
raise ValueError("Missing surface fields")
|
|
344
|
-
|
|
345
|
-
q_sl = values["sfc"][q].to_numpy(flatten=True)
|
|
346
|
-
t_sl = values["sfc"][t].to_numpy(flatten=True)
|
|
347
|
-
sp_sl = values["sfc"][sp].to_numpy(flatten=True)
|
|
348
|
-
|
|
349
|
-
nlevels = len(kwargs["A"]) - 1
|
|
350
|
-
if len(values["ml"]) != nlevels:
|
|
351
|
-
raise ValueError("Missing model levels")
|
|
352
|
-
|
|
353
|
-
for key in values["ml"].keys():
|
|
354
|
-
if len(values["ml"][key]) != 2:
|
|
355
|
-
raise ValueError(f"Missing field on level {key}")
|
|
356
|
-
|
|
357
|
-
# create 3D arrays for upper air fields
|
|
358
|
-
levels = list(values["ml"].keys())
|
|
359
|
-
levels.sort()
|
|
360
|
-
t_ml = []
|
|
361
|
-
q_ml = []
|
|
362
|
-
for level in levels:
|
|
363
|
-
t_ml.append(values["ml"][level][kwargs["t_ml"]].to_numpy(flatten=True))
|
|
364
|
-
q_ml.append(values["ml"][level][kwargs["q_ml"]].to_numpy(flatten=True))
|
|
365
|
-
|
|
366
|
-
t_ml = np.stack(t_ml)
|
|
367
|
-
q_ml = np.stack(q_ml)
|
|
368
|
-
|
|
369
|
-
# actual conversion from qv --> rh
|
|
370
|
-
# FIXME:
|
|
371
|
-
# For now We need to go from qv --> td --> rh to take into account
|
|
372
|
-
# the mixed / ice phase when T ~ 0C / T < 0C
|
|
373
|
-
# See https://github.com/ecmwf/earthkit-meteo/issues/15
|
|
374
|
-
p_sl = pressure_at_height_level(height, q_ml, t_ml, sp_sl, np.array(kwargs["A"]), np.array(kwargs["B"]))
|
|
375
|
-
td_sl = thermo.dewpoint_from_specific_humidity(q=q_sl, p=p_sl)
|
|
376
|
-
rh_sl = thermo.relative_humidity_from_dewpoint(t=t_sl, td=td_sl)
|
|
377
|
-
|
|
378
|
-
result.append(new_field_from_numpy(values["sfc"][q], rh_sl, param=new_name))
|
|
379
|
-
|
|
380
|
-
return new_fieldlist_from_list(result)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def test() -> None:
|
|
384
|
-
"""Test the conversion from specific humidity to relative humidity.
|
|
385
|
-
|
|
386
|
-
This function fetches data from a source, performs the conversion, and prints
|
|
387
|
-
the mean, median, and maximum differences in dewpoint temperature.
|
|
388
|
-
|
|
389
|
-
Returns
|
|
390
|
-
-------
|
|
391
|
-
None
|
|
392
|
-
"""
|
|
393
|
-
from earthkit.data import from_source
|
|
394
|
-
from earthkit.data.readers.grib.index import GribFieldList
|
|
395
|
-
|
|
396
|
-
# IFS forecasts have both specific humidity and dewpoint
|
|
397
|
-
sl = from_source(
|
|
398
|
-
"mars",
|
|
399
|
-
{
|
|
400
|
-
"date": "2022-01-01",
|
|
401
|
-
"class": "od",
|
|
402
|
-
"expver": "1",
|
|
403
|
-
"stream": "oper",
|
|
404
|
-
"levtype": "sfc",
|
|
405
|
-
"param": "96.174/134.128/167.128/168.128",
|
|
406
|
-
"time": "00:00:00",
|
|
407
|
-
"type": "fc",
|
|
408
|
-
"step": "2",
|
|
409
|
-
"grid": "O640",
|
|
410
|
-
},
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
ml = from_source(
|
|
414
|
-
"mars",
|
|
415
|
-
{
|
|
416
|
-
"date": "2022-01-01",
|
|
417
|
-
"class": "od",
|
|
418
|
-
"expver": "1",
|
|
419
|
-
"stream": "oper",
|
|
420
|
-
"levtype": "ml",
|
|
421
|
-
"levelist": "130/131/132/133/134/135/136/137",
|
|
422
|
-
"param": "130/133",
|
|
423
|
-
"time": "00:00:00",
|
|
424
|
-
"type": "fc",
|
|
425
|
-
"step": "2",
|
|
426
|
-
"grid": "O640",
|
|
427
|
-
},
|
|
428
|
-
)
|
|
429
|
-
source = GribFieldList.merge([sl, ml])
|
|
430
|
-
|
|
431
|
-
# IFS A and B coeffients for level 137 - 129
|
|
432
|
-
kwargs = {
|
|
433
|
-
"A": [424.414063, 302.476563, 202.484375, 122.101563, 62.781250, 22.835938, 3.757813, 0.0, 0.0],
|
|
434
|
-
"B": [0.969513, 0.975078, 0.980072, 0.984542, 0.988500, 0.991984, 0.995003, 0.997630, 1.000000],
|
|
435
|
-
}
|
|
436
|
-
source = execute(None, source, 2, "2t", "2sh", "sp", "2r", **kwargs)
|
|
437
|
-
|
|
438
|
-
temperature = source[2].to_numpy(flatten=True)
|
|
439
|
-
dewpoint = source[3].to_numpy(flatten=True)
|
|
440
|
-
relhum = source[4].to_numpy()
|
|
441
|
-
newdew = thermo.dewpoint_from_relative_humidity(temperature, relhum)
|
|
442
|
-
|
|
443
|
-
print(f"Mean difference in dewpoint temperature: {np.abs(newdew - dewpoint).mean():02f} degC")
|
|
444
|
-
print(f"Median difference in dewpoint temperature: {np.median(np.abs(newdew - dewpoint)):02f} degC")
|
|
445
|
-
print(f"Maximum difference in dewpoint temperature: {np.abs(newdew - dewpoint).max():02f} degC")
|
|
446
|
-
|
|
447
|
-
# source.save("source.grib")
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if __name__ == "__main__":
|
|
451
|
-
test()
|
|
@@ -1,95 +0,0 @@
|
|
|
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 collections import defaultdict
|
|
12
|
-
from typing import Any
|
|
13
|
-
from typing import List
|
|
14
|
-
|
|
15
|
-
import earthkit.data as ekd
|
|
16
|
-
import numpy as np
|
|
17
|
-
from anemoi.transform.fields import new_field_from_numpy
|
|
18
|
-
from anemoi.transform.fields import new_fieldlist_from_list
|
|
19
|
-
from earthkit.meteo.wind.array import polar_to_xy
|
|
20
|
-
|
|
21
|
-
from .legacy import legacy_filter
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@legacy_filter(__file__)
|
|
25
|
-
def execute(
|
|
26
|
-
context: Any,
|
|
27
|
-
input: List[Any],
|
|
28
|
-
wind_speed: str,
|
|
29
|
-
wind_dir: str,
|
|
30
|
-
u_component: str = "u",
|
|
31
|
-
v_component: str = "v",
|
|
32
|
-
in_radians: bool = False,
|
|
33
|
-
) -> ekd.FieldList:
|
|
34
|
-
"""Convert wind speed and direction to u and v components.
|
|
35
|
-
|
|
36
|
-
Parameters
|
|
37
|
-
----------
|
|
38
|
-
context : Any
|
|
39
|
-
The context for the execution.
|
|
40
|
-
input : List[Any]
|
|
41
|
-
The input data fields.
|
|
42
|
-
wind_speed : str
|
|
43
|
-
The name of the wind speed parameter.
|
|
44
|
-
wind_dir : str
|
|
45
|
-
The name of the wind direction parameter.
|
|
46
|
-
u_component : str, optional
|
|
47
|
-
The name for the u component. Defaults to "u".
|
|
48
|
-
v_component : str, optional
|
|
49
|
-
The name for the v component. Defaults to "v".
|
|
50
|
-
in_radians : bool, optional
|
|
51
|
-
Whether the wind direction is in radians. Defaults to False.
|
|
52
|
-
|
|
53
|
-
Returns
|
|
54
|
-
-------
|
|
55
|
-
ekd.FieldList
|
|
56
|
-
The resulting field array with u and v components.
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
result = []
|
|
60
|
-
|
|
61
|
-
wind_params = (wind_speed, wind_dir)
|
|
62
|
-
wind_pairs = defaultdict(dict)
|
|
63
|
-
|
|
64
|
-
for f in input:
|
|
65
|
-
key = f.metadata(namespace="mars")
|
|
66
|
-
param = key.pop("param")
|
|
67
|
-
|
|
68
|
-
if param not in wind_params:
|
|
69
|
-
result.append(f)
|
|
70
|
-
continue
|
|
71
|
-
|
|
72
|
-
key = tuple(key.items())
|
|
73
|
-
|
|
74
|
-
if param in wind_pairs[key]:
|
|
75
|
-
raise ValueError(f"Duplicate wind component {param} for {key}")
|
|
76
|
-
|
|
77
|
-
wind_pairs[key][param] = f
|
|
78
|
-
|
|
79
|
-
for _, pairs in wind_pairs.items():
|
|
80
|
-
if len(pairs) != 2:
|
|
81
|
-
raise ValueError("Missing wind component")
|
|
82
|
-
|
|
83
|
-
magnitude = pairs[wind_speed]
|
|
84
|
-
direction = pairs[wind_dir]
|
|
85
|
-
|
|
86
|
-
# assert speed.grid_mapping == dir.grid_mapping
|
|
87
|
-
if in_radians:
|
|
88
|
-
direction = np.rad2deg(direction)
|
|
89
|
-
|
|
90
|
-
u, v = polar_to_xy(magnitude.to_numpy(flatten=True), direction.to_numpy(flatten=True))
|
|
91
|
-
|
|
92
|
-
result.append(new_field_from_numpy(magnitude, u, param=u_component))
|
|
93
|
-
result.append(new_field_from_numpy(direction, v, param=v_component))
|
|
94
|
-
|
|
95
|
-
return new_fieldlist_from_list(result)
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
from collections import defaultdict
|
|
11
|
-
from typing import Any
|
|
12
|
-
from typing import Dict
|
|
13
|
-
from typing import Hashable
|
|
14
|
-
from typing import List
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
|
|
17
|
-
import earthkit.data as ekd
|
|
18
|
-
from anemoi.transform.fields import new_field_from_numpy
|
|
19
|
-
from anemoi.transform.fields import new_fieldlist_from_list
|
|
20
|
-
|
|
21
|
-
from .legacy import legacy_filter
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@legacy_filter(__file__)
|
|
25
|
-
def execute(context: Any, input: ekd.FieldList, params: List[str], output: str) -> ekd.FieldList:
|
|
26
|
-
"""Computes the sum over a set of variables.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
context (Any): The execution context.
|
|
30
|
-
input (List[Any]): The list of input fields.
|
|
31
|
-
params (List[str]): The list of parameters to sum over.
|
|
32
|
-
output (str): The name for the output field.
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
ekd.FieldList: The resulting FieldArray with summed fields.
|
|
36
|
-
"""
|
|
37
|
-
result = []
|
|
38
|
-
|
|
39
|
-
needed_fields: Dict[Tuple[Hashable, ...], Dict[str, ekd.Field]] = defaultdict(dict)
|
|
40
|
-
|
|
41
|
-
for f in input:
|
|
42
|
-
key = f.metadata(namespace="mars")
|
|
43
|
-
param = key.pop("param")
|
|
44
|
-
if param in params:
|
|
45
|
-
key = tuple(key.items())
|
|
46
|
-
|
|
47
|
-
if param in needed_fields[key]:
|
|
48
|
-
raise ValueError(f"Duplicate field {param} for {key}")
|
|
49
|
-
|
|
50
|
-
needed_fields[key][param] = f
|
|
51
|
-
else:
|
|
52
|
-
result.append(f)
|
|
53
|
-
|
|
54
|
-
for keys, values in needed_fields.items():
|
|
55
|
-
|
|
56
|
-
if len(values) != len(params):
|
|
57
|
-
raise ValueError("Missing fields")
|
|
58
|
-
|
|
59
|
-
s = None
|
|
60
|
-
for k, v in values.items():
|
|
61
|
-
c = v.to_numpy(flatten=True)
|
|
62
|
-
if s is None:
|
|
63
|
-
s = c
|
|
64
|
-
else:
|
|
65
|
-
s += c
|
|
66
|
-
result.append(new_field_from_numpy(values[list(values.keys())[0]], s, param=output))
|
|
67
|
-
|
|
68
|
-
return new_fieldlist_from_list(result)
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# (C) Copyright 2025 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
|
-
from typing import Any
|
|
11
|
-
from typing import Dict
|
|
12
|
-
|
|
13
|
-
import earthkit.data as ekd
|
|
14
|
-
|
|
15
|
-
from ..filter import Filter
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class TransformFilter(Filter):
|
|
19
|
-
"""Calls filters from anemoi.transform.filters
|
|
20
|
-
|
|
21
|
-
Parameters
|
|
22
|
-
----------
|
|
23
|
-
context : Any
|
|
24
|
-
The context in which the filter is created.
|
|
25
|
-
name : str
|
|
26
|
-
The name of the filter.
|
|
27
|
-
config : Dict[str, Any]
|
|
28
|
-
The configuration for the filter.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
def __init__(self, context: Any, name: str, config: Dict[str, Any]) -> None:
|
|
32
|
-
|
|
33
|
-
from anemoi.transform.filters import create_filter
|
|
34
|
-
|
|
35
|
-
self.name = name
|
|
36
|
-
self.transform_filter = create_filter(context, config)
|
|
37
|
-
|
|
38
|
-
def execute(self, input: ekd.FieldList) -> ekd.FieldList:
|
|
39
|
-
"""Execute the transformation filter.
|
|
40
|
-
|
|
41
|
-
Parameters
|
|
42
|
-
----------
|
|
43
|
-
input : ekd.FieldList
|
|
44
|
-
The input data to be transformed.
|
|
45
|
-
|
|
46
|
-
Returns
|
|
47
|
-
-------
|
|
48
|
-
ekd.FieldList
|
|
49
|
-
The transformed data.
|
|
50
|
-
"""
|
|
51
|
-
return self.transform_filter.forward(input)
|