anemoi-datasets 0.4.5__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.
Files changed (33) hide show
  1. anemoi/datasets/_version.py +2 -2
  2. anemoi/datasets/commands/create.py +3 -2
  3. anemoi/datasets/create/__init__.py +30 -32
  4. anemoi/datasets/create/config.py +4 -3
  5. anemoi/datasets/create/functions/filters/pressure_level_relative_humidity_to_specific_humidity.py +57 -0
  6. anemoi/datasets/create/functions/filters/pressure_level_specific_humidity_to_relative_humidity.py +57 -0
  7. anemoi/datasets/create/functions/filters/single_level_dewpoint_to_relative_humidity.py +54 -0
  8. anemoi/datasets/create/functions/filters/single_level_relative_humidity_to_dewpoint.py +59 -0
  9. anemoi/datasets/create/functions/filters/single_level_relative_humidity_to_specific_humidity.py +115 -0
  10. anemoi/datasets/create/functions/filters/single_level_specific_humidity_to_relative_humidity.py +390 -0
  11. anemoi/datasets/create/functions/filters/speeddir_to_uv.py +77 -0
  12. anemoi/datasets/create/functions/filters/uv_to_speeddir.py +55 -0
  13. anemoi/datasets/create/functions/sources/grib.py +86 -1
  14. anemoi/datasets/create/functions/sources/hindcasts.py +14 -73
  15. anemoi/datasets/create/functions/sources/mars.py +9 -3
  16. anemoi/datasets/create/functions/sources/xarray/field.py +7 -1
  17. anemoi/datasets/create/functions/sources/xarray/metadata.py +13 -11
  18. anemoi/datasets/create/input.py +39 -17
  19. anemoi/datasets/create/persistent.py +1 -1
  20. anemoi/datasets/create/utils.py +3 -0
  21. anemoi/datasets/data/dataset.py +11 -1
  22. anemoi/datasets/data/debug.py +5 -1
  23. anemoi/datasets/data/masked.py +2 -2
  24. anemoi/datasets/data/rescale.py +147 -0
  25. anemoi/datasets/data/stores.py +20 -7
  26. anemoi/datasets/dates/__init__.py +112 -30
  27. anemoi/datasets/dates/groups.py +84 -19
  28. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.0.dist-info}/METADATA +10 -19
  29. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.0.dist-info}/RECORD +33 -24
  30. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.0.dist-info}/WHEEL +1 -1
  31. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.0.dist-info}/LICENSE +0 -0
  32. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.0.dist-info}/entry_points.txt +0 -0
  33. {anemoi_datasets-0.4.5.dist-info → anemoi_datasets-0.5.0.dist-info}/top_level.txt +0 -0
@@ -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