anemoi-datasets 0.4.4__py3-none-any.whl → 0.5.0__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/cleanup.py +44 -0
- anemoi/datasets/commands/create.py +52 -21
- anemoi/datasets/commands/finalise-additions.py +45 -0
- anemoi/datasets/commands/finalise.py +39 -0
- anemoi/datasets/commands/init-additions.py +45 -0
- anemoi/datasets/commands/init.py +67 -0
- anemoi/datasets/commands/inspect.py +1 -1
- anemoi/datasets/commands/load-additions.py +47 -0
- anemoi/datasets/commands/load.py +47 -0
- anemoi/datasets/commands/patch.py +39 -0
- anemoi/datasets/create/__init__.py +959 -146
- anemoi/datasets/create/check.py +5 -3
- anemoi/datasets/create/config.py +54 -2
- anemoi/datasets/create/functions/filters/pressure_level_relative_humidity_to_specific_humidity.py +57 -0
- anemoi/datasets/create/functions/filters/pressure_level_specific_humidity_to_relative_humidity.py +57 -0
- anemoi/datasets/create/functions/filters/single_level_dewpoint_to_relative_humidity.py +54 -0
- anemoi/datasets/create/functions/filters/single_level_relative_humidity_to_dewpoint.py +59 -0
- anemoi/datasets/create/functions/filters/single_level_relative_humidity_to_specific_humidity.py +115 -0
- anemoi/datasets/create/functions/filters/single_level_specific_humidity_to_relative_humidity.py +390 -0
- anemoi/datasets/create/functions/filters/speeddir_to_uv.py +77 -0
- anemoi/datasets/create/functions/filters/uv_to_speeddir.py +55 -0
- anemoi/datasets/create/functions/sources/grib.py +86 -1
- anemoi/datasets/create/functions/sources/hindcasts.py +14 -73
- anemoi/datasets/create/functions/sources/mars.py +9 -3
- anemoi/datasets/create/functions/sources/xarray/__init__.py +12 -2
- anemoi/datasets/create/functions/sources/xarray/coordinates.py +7 -0
- anemoi/datasets/create/functions/sources/xarray/field.py +8 -2
- anemoi/datasets/create/functions/sources/xarray/fieldlist.py +0 -2
- anemoi/datasets/create/functions/sources/xarray/flavour.py +21 -1
- anemoi/datasets/create/functions/sources/xarray/metadata.py +40 -40
- anemoi/datasets/create/functions/sources/xarray/time.py +63 -30
- anemoi/datasets/create/functions/sources/xarray/variable.py +15 -38
- anemoi/datasets/create/input.py +62 -39
- anemoi/datasets/create/persistent.py +1 -1
- anemoi/datasets/create/statistics/__init__.py +39 -23
- anemoi/datasets/create/utils.py +6 -2
- anemoi/datasets/data/__init__.py +1 -0
- anemoi/datasets/data/concat.py +46 -2
- anemoi/datasets/data/dataset.py +119 -34
- anemoi/datasets/data/debug.py +5 -1
- anemoi/datasets/data/forwards.py +17 -8
- anemoi/datasets/data/grids.py +17 -3
- anemoi/datasets/data/interpolate.py +133 -0
- anemoi/datasets/data/masked.py +2 -2
- anemoi/datasets/data/misc.py +56 -66
- anemoi/datasets/data/missing.py +240 -0
- anemoi/datasets/data/rescale.py +147 -0
- anemoi/datasets/data/select.py +7 -1
- anemoi/datasets/data/stores.py +23 -10
- anemoi/datasets/data/subset.py +47 -5
- anemoi/datasets/data/unchecked.py +20 -22
- anemoi/datasets/data/xy.py +125 -0
- anemoi/datasets/dates/__init__.py +124 -95
- anemoi/datasets/dates/groups.py +85 -20
- anemoi/datasets/grids.py +66 -48
- {anemoi_datasets-0.4.4.dist-info → anemoi_datasets-0.5.0.dist-info}/METADATA +8 -17
- anemoi_datasets-0.5.0.dist-info/RECORD +105 -0
- {anemoi_datasets-0.4.4.dist-info → anemoi_datasets-0.5.0.dist-info}/WHEEL +1 -1
- anemoi/datasets/create/loaders.py +0 -936
- anemoi_datasets-0.4.4.dist-info/RECORD +0 -86
- {anemoi_datasets-0.4.4.dist-info → anemoi_datasets-0.5.0.dist-info}/LICENSE +0 -0
- {anemoi_datasets-0.4.4.dist-info → anemoi_datasets-0.5.0.dist-info}/entry_points.txt +0 -0
- {anemoi_datasets-0.4.4.dist-info → anemoi_datasets-0.5.0.dist-info}/top_level.txt +0 -0
anemoi/datasets/create/functions/filters/single_level_specific_humidity_to_relative_humidity.py
ADDED
|
@@ -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
|
|
@@ -11,9 +11,87 @@
|
|
|
11
11
|
import glob
|
|
12
12
|
|
|
13
13
|
from earthkit.data import from_source
|
|
14
|
+
from earthkit.data.indexing.fieldlist import FieldArray
|
|
14
15
|
from earthkit.data.utils.patterns import Pattern
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def _load(context, name, record):
|
|
19
|
+
ds = None
|
|
20
|
+
|
|
21
|
+
param = record["param"]
|
|
22
|
+
|
|
23
|
+
if "path" in record:
|
|
24
|
+
context.info(f"Using {name} from {record['path']} (param={param})")
|
|
25
|
+
ds = from_source("file", record["path"])
|
|
26
|
+
|
|
27
|
+
if "url" in record:
|
|
28
|
+
context.info(f"Using {name} from {record['url']} (param={param})")
|
|
29
|
+
ds = from_source("url", record["url"])
|
|
30
|
+
|
|
31
|
+
ds = ds.sel(param=param)
|
|
32
|
+
|
|
33
|
+
assert len(ds) == 1, f"{name} {param}, expected one field, got {len(ds)}"
|
|
34
|
+
ds = ds[0]
|
|
35
|
+
|
|
36
|
+
return ds.to_numpy(flatten=True), ds.metadata("uuidOfHGrid")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Geography:
|
|
40
|
+
"""This class retrieve the latitudes and longitudes of unstructured grids,
|
|
41
|
+
and checks if the fields are compatible with the grid.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, context, latitudes, longitudes):
|
|
45
|
+
|
|
46
|
+
latitudes, uuidOfHGrid_lat = _load(context, "latitudes", latitudes)
|
|
47
|
+
longitudes, uuidOfHGrid_lon = _load(context, "longitudes", longitudes)
|
|
48
|
+
|
|
49
|
+
assert (
|
|
50
|
+
uuidOfHGrid_lat == uuidOfHGrid_lon
|
|
51
|
+
), f"uuidOfHGrid mismatch: lat={uuidOfHGrid_lat} != lon={uuidOfHGrid_lon}"
|
|
52
|
+
|
|
53
|
+
context.info(f"Latitudes: {len(latitudes)}, Longitudes: {len(longitudes)}")
|
|
54
|
+
assert len(latitudes) == len(longitudes)
|
|
55
|
+
|
|
56
|
+
self.uuidOfHGrid = uuidOfHGrid_lat
|
|
57
|
+
self.latitudes = latitudes
|
|
58
|
+
self.longitudes = longitudes
|
|
59
|
+
self.first = True
|
|
60
|
+
|
|
61
|
+
def check(self, field):
|
|
62
|
+
if self.first:
|
|
63
|
+
# We only check the first field, for performance reasons
|
|
64
|
+
assert (
|
|
65
|
+
field.metadata("uuidOfHGrid") == self.uuidOfHGrid
|
|
66
|
+
), f"uuidOfHGrid mismatch: {field.metadata('uuidOfHGrid')} != {self.uuidOfHGrid}"
|
|
67
|
+
self.first = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AddGrid:
|
|
71
|
+
"""An earth-kit.data.Field wrapper that adds grid information."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, field, geography):
|
|
74
|
+
self._field = field
|
|
75
|
+
|
|
76
|
+
geography.check(field)
|
|
77
|
+
|
|
78
|
+
self._latitudes = geography.latitudes
|
|
79
|
+
self._longitudes = geography.longitudes
|
|
80
|
+
|
|
81
|
+
def __getattr__(self, name):
|
|
82
|
+
return getattr(self._field, name)
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return repr(self._field)
|
|
86
|
+
|
|
87
|
+
def grid_points(self):
|
|
88
|
+
return self._latitudes, self._longitudes
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def resolution(self):
|
|
92
|
+
return "unknown"
|
|
93
|
+
|
|
94
|
+
|
|
17
95
|
def check(ds, paths, **kwargs):
|
|
18
96
|
count = 1
|
|
19
97
|
for k, v in kwargs.items():
|
|
@@ -34,9 +112,13 @@ def _expand(paths):
|
|
|
34
112
|
yield path
|
|
35
113
|
|
|
36
114
|
|
|
37
|
-
def execute(context, dates, path, *args, **kwargs):
|
|
115
|
+
def execute(context, dates, path, latitudes=None, longitudes=None, *args, **kwargs):
|
|
38
116
|
given_paths = path if isinstance(path, list) else [path]
|
|
39
117
|
|
|
118
|
+
geography = None
|
|
119
|
+
if latitudes is not None and longitudes is not None:
|
|
120
|
+
geography = Geography(context, latitudes, longitudes)
|
|
121
|
+
|
|
40
122
|
ds = from_source("empty")
|
|
41
123
|
dates = [d.isoformat() for d in dates]
|
|
42
124
|
|
|
@@ -56,4 +138,7 @@ def execute(context, dates, path, *args, **kwargs):
|
|
|
56
138
|
if kwargs:
|
|
57
139
|
check(ds, given_paths, valid_datetime=dates, **kwargs)
|
|
58
140
|
|
|
141
|
+
if geography is not None:
|
|
142
|
+
ds = FieldArray([AddGrid(_, geography) for _ in ds])
|
|
143
|
+
|
|
59
144
|
return ds
|