anemoi-datasets 0.3.10__py3-none-any.whl → 0.4.2__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 (61) hide show
  1. anemoi/datasets/_version.py +2 -2
  2. anemoi/datasets/commands/compare.py +59 -0
  3. anemoi/datasets/commands/create.py +84 -3
  4. anemoi/datasets/commands/inspect.py +9 -9
  5. anemoi/datasets/commands/scan.py +4 -4
  6. anemoi/datasets/compute/recentre.py +14 -9
  7. anemoi/datasets/create/__init__.py +44 -17
  8. anemoi/datasets/create/check.py +6 -5
  9. anemoi/datasets/create/chunks.py +1 -1
  10. anemoi/datasets/create/config.py +6 -27
  11. anemoi/datasets/create/functions/__init__.py +3 -3
  12. anemoi/datasets/create/functions/filters/empty.py +4 -4
  13. anemoi/datasets/create/functions/filters/rename.py +14 -6
  14. anemoi/datasets/create/functions/filters/rotate_winds.py +16 -60
  15. anemoi/datasets/create/functions/filters/unrotate_winds.py +14 -64
  16. anemoi/datasets/create/functions/sources/__init__.py +39 -0
  17. anemoi/datasets/create/functions/sources/accumulations.py +38 -56
  18. anemoi/datasets/create/functions/sources/constants.py +11 -4
  19. anemoi/datasets/create/functions/sources/empty.py +2 -2
  20. anemoi/datasets/create/functions/sources/forcings.py +3 -3
  21. anemoi/datasets/create/functions/sources/grib.py +8 -4
  22. anemoi/datasets/create/functions/sources/hindcasts.py +32 -364
  23. anemoi/datasets/create/functions/sources/mars.py +57 -26
  24. anemoi/datasets/create/functions/sources/netcdf.py +2 -60
  25. anemoi/datasets/create/functions/sources/opendap.py +3 -2
  26. anemoi/datasets/create/functions/sources/source.py +3 -3
  27. anemoi/datasets/create/functions/sources/tendencies.py +7 -7
  28. anemoi/datasets/create/functions/sources/xarray/__init__.py +73 -0
  29. anemoi/datasets/create/functions/sources/xarray/coordinates.py +234 -0
  30. anemoi/datasets/create/functions/sources/xarray/field.py +109 -0
  31. anemoi/datasets/create/functions/sources/xarray/fieldlist.py +171 -0
  32. anemoi/datasets/create/functions/sources/xarray/flavour.py +330 -0
  33. anemoi/datasets/create/functions/sources/xarray/grid.py +46 -0
  34. anemoi/datasets/create/functions/sources/xarray/metadata.py +161 -0
  35. anemoi/datasets/create/functions/sources/xarray/time.py +98 -0
  36. anemoi/datasets/create/functions/sources/xarray/variable.py +198 -0
  37. anemoi/datasets/create/functions/sources/xarray_kerchunk.py +42 -0
  38. anemoi/datasets/create/functions/sources/xarray_zarr.py +15 -0
  39. anemoi/datasets/create/functions/sources/zenodo.py +40 -0
  40. anemoi/datasets/create/input.py +309 -191
  41. anemoi/datasets/create/loaders.py +155 -77
  42. anemoi/datasets/create/patch.py +17 -14
  43. anemoi/datasets/create/persistent.py +1 -1
  44. anemoi/datasets/create/size.py +4 -5
  45. anemoi/datasets/create/statistics/__init__.py +51 -17
  46. anemoi/datasets/create/template.py +11 -61
  47. anemoi/datasets/create/trace.py +91 -0
  48. anemoi/datasets/create/utils.py +5 -52
  49. anemoi/datasets/create/zarr.py +24 -10
  50. anemoi/datasets/data/dataset.py +4 -4
  51. anemoi/datasets/data/misc.py +9 -37
  52. anemoi/datasets/data/stores.py +37 -14
  53. anemoi/datasets/dates/__init__.py +7 -1
  54. anemoi/datasets/dates/groups.py +3 -0
  55. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/METADATA +24 -8
  56. anemoi_datasets-0.4.2.dist-info/RECORD +86 -0
  57. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/WHEEL +1 -1
  58. anemoi_datasets-0.3.10.dist-info/RECORD +0 -73
  59. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/LICENSE +0 -0
  60. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/entry_points.txt +0 -0
  61. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,9 @@
7
7
  # nor does it submit to any jurisdiction.
8
8
  #
9
9
  import datetime
10
+ import itertools
10
11
  import logging
12
+ import math
11
13
  import time
12
14
  from collections import defaultdict
13
15
  from copy import deepcopy
@@ -15,8 +17,11 @@ from functools import cached_property
15
17
  from functools import wraps
16
18
 
17
19
  import numpy as np
18
- from climetlab.core.order import build_remapping
19
- from climetlab.indexing.fieldset import FieldSet
20
+ from anemoi.utils.humanize import seconds_to_human
21
+ from anemoi.utils.humanize import shorten_list
22
+ from earthkit.data.core.fieldlist import FieldList
23
+ from earthkit.data.core.fieldlist import MultiFieldList
24
+ from earthkit.data.core.order import build_remapping
20
25
 
21
26
  from anemoi.datasets.dates import Dates
22
27
 
@@ -25,29 +30,33 @@ from .template import Context
25
30
  from .template import notify_result
26
31
  from .template import resolve
27
32
  from .template import substitute
28
- from .template import trace
29
- from .template import trace_datasource
30
- from .template import trace_select
31
- from .utils import seconds
33
+ from .trace import trace
34
+ from .trace import trace_datasource
35
+ from .trace import trace_select
32
36
 
33
37
  LOG = logging.getLogger(__name__)
34
38
 
35
39
 
36
40
  def parse_function_name(name):
37
- if "-" in name:
38
- name, delta = name.split("-")
39
- sign = -1
40
41
 
41
- elif "+" in name:
42
- name, delta = name.split("+")
43
- sign = 1
42
+ if name.endswith("h") and name[:-1].isdigit():
44
43
 
45
- else:
46
- return name, None
44
+ if "-" in name:
45
+ name, delta = name.split("-")
46
+ sign = -1
47
47
 
48
- assert delta[-1] == "h", (name, delta)
49
- delta = sign * int(delta[:-1])
50
- return name, delta
48
+ elif "+" in name:
49
+ name, delta = name.split("+")
50
+ sign = 1
51
+
52
+ else:
53
+ return name, None
54
+
55
+ assert delta[-1] == "h", (name, delta)
56
+ delta = sign * int(delta[:-1])
57
+ return name, delta
58
+
59
+ return name, None
51
60
 
52
61
 
53
62
  def time_delta_to_string(delta):
@@ -75,18 +84,18 @@ def is_function(name, kind):
75
84
  return False
76
85
 
77
86
 
78
- def assert_fieldset(method):
87
+ def assert_fieldlist(method):
79
88
  @wraps(method)
80
89
  def wrapper(self, *args, **kwargs):
81
90
  result = method(self, *args, **kwargs)
82
- assert isinstance(result, FieldSet), type(result)
91
+ assert isinstance(result, FieldList), type(result)
83
92
  return result
84
93
 
85
94
  return wrapper
86
95
 
87
96
 
88
- def assert_is_fieldset(obj):
89
- assert isinstance(obj, FieldSet), type(obj)
97
+ def assert_is_fieldlist(obj):
98
+ assert isinstance(obj, FieldList), type(obj)
90
99
 
91
100
 
92
101
  def _data_request(data):
@@ -101,12 +110,12 @@ def _data_request(data):
101
110
  continue
102
111
 
103
112
  if date is None:
104
- date = field.valid_datetime()
113
+ date = field.datetime()["valid_time"]
105
114
 
106
- if field.valid_datetime() != date:
115
+ if field.datetime()["valid_time"] != date:
107
116
  continue
108
117
 
109
- as_mars = field.as_mars()
118
+ as_mars = field.metadata(namespace="mars")
110
119
  step = as_mars.get("step")
111
120
  levtype = as_mars.get("levtype", "sfc")
112
121
  param = as_mars["param"]
@@ -134,141 +143,6 @@ def _data_request(data):
134
143
  return dict(param_level=params_levels, param_step=params_steps, area=area, grid=grid)
135
144
 
136
145
 
137
- class Coords:
138
- def __init__(self, owner):
139
- self.owner = owner
140
-
141
- @cached_property
142
- def _build_coords(self):
143
- from_data = self.owner.get_cube().user_coords
144
- from_config = self.owner.context.order_by
145
-
146
- keys_from_config = list(from_config.keys())
147
- keys_from_data = list(from_data.keys())
148
- assert (
149
- keys_from_data == keys_from_config
150
- ), f"Critical error: {keys_from_data=} != {keys_from_config=}. {self.owner=}"
151
-
152
- variables_key = list(from_config.keys())[1]
153
- ensembles_key = list(from_config.keys())[2]
154
-
155
- if isinstance(from_config[variables_key], (list, tuple)):
156
- assert all([v == w for v, w in zip(from_data[variables_key], from_config[variables_key])]), (
157
- from_data[variables_key],
158
- from_config[variables_key],
159
- )
160
-
161
- self._variables = from_data[variables_key] # "param_level"
162
- self._ensembles = from_data[ensembles_key] # "number"
163
-
164
- first_field = self.owner.datasource[0]
165
- grid_points = first_field.grid_points()
166
-
167
- lats, lons = grid_points
168
- north = np.amax(lats)
169
- south = np.amin(lats)
170
- east = np.amax(lons)
171
- west = np.amin(lons)
172
-
173
- assert -90 <= south <= north <= 90, (south, north, first_field)
174
- assert (-180 <= west <= east <= 180) or (0 <= west <= east <= 360), (
175
- west,
176
- east,
177
- first_field,
178
- )
179
-
180
- grid_values = list(range(len(grid_points[0])))
181
-
182
- self._grid_points = grid_points
183
- self._resolution = first_field.resolution
184
- self._grid_values = grid_values
185
- self._field_shape = first_field.shape
186
- self._proj_string = first_field.proj_string if hasattr(first_field, "proj_string") else None
187
-
188
- @cached_property
189
- def variables(self):
190
- self._build_coords
191
- return self._variables
192
-
193
- @cached_property
194
- def ensembles(self):
195
- self._build_coords
196
- return self._ensembles
197
-
198
- @cached_property
199
- def resolution(self):
200
- self._build_coords
201
- return self._resolution
202
-
203
- @cached_property
204
- def grid_values(self):
205
- self._build_coords
206
- return self._grid_values
207
-
208
- @cached_property
209
- def grid_points(self):
210
- self._build_coords
211
- return self._grid_points
212
-
213
- @cached_property
214
- def field_shape(self):
215
- self._build_coords
216
- return self._field_shape
217
-
218
- @cached_property
219
- def proj_string(self):
220
- self._build_coords
221
- return self._proj_string
222
-
223
-
224
- class HasCoordsMixin:
225
- @cached_property
226
- def variables(self):
227
- return self._coords.variables
228
-
229
- @cached_property
230
- def ensembles(self):
231
- return self._coords.ensembles
232
-
233
- @cached_property
234
- def resolution(self):
235
- return self._coords.resolution
236
-
237
- @cached_property
238
- def grid_values(self):
239
- return self._coords.grid_values
240
-
241
- @cached_property
242
- def grid_points(self):
243
- return self._coords.grid_points
244
-
245
- @cached_property
246
- def field_shape(self):
247
- return self._coords.field_shape
248
-
249
- @cached_property
250
- def proj_string(self):
251
- return self._coords.proj_string
252
-
253
- @cached_property
254
- def shape(self):
255
- return [
256
- len(self.dates),
257
- len(self.variables),
258
- len(self.ensembles),
259
- len(self.grid_values),
260
- ]
261
-
262
- @cached_property
263
- def coords(self):
264
- return {
265
- "dates": self.dates,
266
- "variables": self.variables,
267
- "ensembles": self.ensembles,
268
- "values": self.grid_values,
269
- }
270
-
271
-
272
146
  class Action:
273
147
  def __init__(self, context, action_path, /, *args, **kwargs):
274
148
  if "args" in kwargs and "kwargs" in kwargs:
@@ -323,15 +197,15 @@ def shorten(dates):
323
197
  return dates
324
198
 
325
199
 
326
- class Result(HasCoordsMixin):
200
+ class Result:
327
201
  empty = False
202
+ _coords_already_built = False
328
203
 
329
204
  def __init__(self, context, action_path, dates):
330
205
  assert isinstance(context, ActionContext), type(context)
331
206
  assert isinstance(action_path, list), action_path
332
207
 
333
208
  self.context = context
334
- self._coords = Coords(self)
335
209
  self.dates = dates
336
210
  self.action_path = action_path
337
211
 
@@ -353,19 +227,142 @@ class Result(HasCoordsMixin):
353
227
  order_by = self.context.order_by
354
228
  flatten_grid = self.context.flatten_grid
355
229
  start = time.time()
356
- LOG.info("Sorting dataset %s %s", order_by, remapping)
230
+ LOG.debug("Sorting dataset %s %s", dict(order_by), remapping)
357
231
  assert order_by, order_by
358
- cube = ds.cube(
359
- order_by,
360
- remapping=remapping,
361
- flatten_values=flatten_grid,
362
- patches={"number": {None: 0}},
363
- )
364
- cube = cube.squeeze()
365
- LOG.info(f"Sorting done in {seconds(time.time()-start)}.")
232
+
233
+ patches = {"number": {None: 0}}
234
+
235
+ try:
236
+ cube = ds.cube(
237
+ order_by,
238
+ remapping=remapping,
239
+ flatten_values=flatten_grid,
240
+ patches=patches,
241
+ )
242
+ cube = cube.squeeze()
243
+ LOG.debug(f"Sorting done in {seconds_to_human(time.time()-start)}.")
244
+ except ValueError:
245
+ self.explain(ds, order_by, remapping=remapping, patches=patches)
246
+ # raise ValueError(f"Error in {self}")
247
+ exit(1)
248
+
249
+ if LOG.isEnabledFor(logging.DEBUG):
250
+ LOG.debug("Cube shape: %s", cube)
251
+ for k, v in cube.user_coords.items():
252
+ LOG.debug(" %s %s", k, shorten_list(v, max_length=10))
366
253
 
367
254
  return cube
368
255
 
256
+ def explain(self, ds, *args, remapping, patches):
257
+
258
+ METADATA = (
259
+ "date",
260
+ "time",
261
+ "step",
262
+ "hdate",
263
+ "valid_datetime",
264
+ "levtype",
265
+ "levelist",
266
+ "number",
267
+ "level",
268
+ "shortName",
269
+ "paramId",
270
+ "variable",
271
+ )
272
+
273
+ # We redo the logic here
274
+ print()
275
+ print("❌" * 40)
276
+ print()
277
+ if len(args) == 1 and isinstance(args[0], (list, tuple)):
278
+ args = args[0]
279
+
280
+ names = []
281
+ for a in args:
282
+ if isinstance(a, str):
283
+ names.append(a)
284
+ elif isinstance(a, dict):
285
+ names += list(a.keys())
286
+
287
+ print(f"Building a {len(names)}D hypercube using", names)
288
+
289
+ ds = ds.order_by(*args, remapping=remapping, patches=patches)
290
+ user_coords = ds.unique_values(*names, remapping=remapping, patches=patches)
291
+
292
+ print()
293
+ print("Number of unique values found for each coordinate:")
294
+ for k, v in user_coords.items():
295
+ print(f" {k:20}:", len(v))
296
+ print()
297
+ user_shape = tuple(len(v) for k, v in user_coords.items())
298
+ print("Shape of the hypercube :", user_shape)
299
+ print(
300
+ "Number of expected fields :", math.prod(user_shape), "=", " x ".join([str(i) for i in user_shape])
301
+ )
302
+ print("Number of fields in the dataset :", len(ds))
303
+ print("Difference :", abs(len(ds) - math.prod(user_shape)))
304
+ print()
305
+
306
+ remapping = build_remapping(remapping, patches)
307
+ expected = set(itertools.product(*user_coords.values()))
308
+
309
+ if math.prod(user_shape) > len(ds):
310
+ print(f"This means that all the fields in the datasets do not exists for all combinations of {names}.")
311
+
312
+ for f in ds:
313
+ metadata = remapping(f.metadata)
314
+ expected.remove(tuple(metadata(n) for n in names))
315
+
316
+ print("Missing fields:")
317
+ print()
318
+ for i, f in enumerate(sorted(expected)):
319
+ print(" ", f)
320
+ if i >= 9 and len(expected) > 10:
321
+ print("...", len(expected) - i - 1, "more")
322
+ break
323
+
324
+ print()
325
+ print("To solve this issue, you can:")
326
+ print(
327
+ " - Provide a better selection, like 'step: 0' or 'level: 1000' to "
328
+ "reduce the number of selected fields."
329
+ )
330
+ print(
331
+ " - Split the 'input' part in smaller sections using 'join', "
332
+ "making sure that each section represent a full hypercube."
333
+ )
334
+
335
+ else:
336
+ print(f"More fields in dataset that expected for {names}. " "This means that some fields are duplicated.")
337
+ duplicated = defaultdict(list)
338
+ for f in ds:
339
+ # print(f.metadata(namespace="default"))
340
+ metadata = remapping(f.metadata)
341
+ key = tuple(metadata(n, default=None) for n in names)
342
+ duplicated[key].append(f)
343
+
344
+ print("Duplicated fields:")
345
+ print()
346
+ duplicated = {k: v for k, v in duplicated.items() if len(v) > 1}
347
+ for i, (k, v) in enumerate(sorted(duplicated.items())):
348
+ print(" ", k)
349
+ for f in v:
350
+ x = {k: f.metadata(k, default=None) for k in METADATA if f.metadata(k, default=None) is not None}
351
+ print(" ", f, x)
352
+ if i >= 9 and len(duplicated) > 10:
353
+ print("...", len(duplicated) - i - 1, "more")
354
+ break
355
+
356
+ print()
357
+ print("To solve this issue, you can:")
358
+ print(" - Provide a better selection, like 'step: 0' or 'level: 1000'")
359
+ print(" - Change the way 'param' is computed using 'variable_naming' " "in the 'build' section.")
360
+
361
+ print()
362
+ print("❌" * 40)
363
+ print()
364
+ exit(1)
365
+
369
366
  def __repr__(self, *args, _indent_="\n", **kwargs):
370
367
  more = ",".join([str(a)[:5000] for a in args])
371
368
  more += ",".join([f"{k}={v}"[:5000] for k, v in kwargs.items()])
@@ -391,6 +388,109 @@ class Result(HasCoordsMixin):
391
388
  def _trace_datasource(self, *args, **kwargs):
392
389
  return f"{self.__class__.__name__}({shorten(self.dates)})"
393
390
 
391
+ def build_coords(self):
392
+ if self._coords_already_built:
393
+ return
394
+ from_data = self.get_cube().user_coords
395
+ from_config = self.context.order_by
396
+
397
+ keys_from_config = list(from_config.keys())
398
+ keys_from_data = list(from_data.keys())
399
+ assert keys_from_data == keys_from_config, f"Critical error: {keys_from_data=} != {keys_from_config=}. {self=}"
400
+
401
+ variables_key = list(from_config.keys())[1]
402
+ ensembles_key = list(from_config.keys())[2]
403
+
404
+ if isinstance(from_config[variables_key], (list, tuple)):
405
+ assert all([v == w for v, w in zip(from_data[variables_key], from_config[variables_key])]), (
406
+ from_data[variables_key],
407
+ from_config[variables_key],
408
+ )
409
+
410
+ self._variables = from_data[variables_key] # "param_level"
411
+ self._ensembles = from_data[ensembles_key] # "number"
412
+
413
+ first_field = self.datasource[0]
414
+ grid_points = first_field.grid_points()
415
+
416
+ lats, lons = grid_points
417
+
418
+ assert len(lats) == len(lons), (len(lats), len(lons), first_field)
419
+ assert len(lats) == math.prod(first_field.shape), (len(lats), first_field.shape, first_field)
420
+
421
+ north = np.amax(lats)
422
+ south = np.amin(lats)
423
+ east = np.amax(lons)
424
+ west = np.amin(lons)
425
+
426
+ assert -90 <= south <= north <= 90, (south, north, first_field)
427
+ assert (-180 <= west <= east <= 180) or (0 <= west <= east <= 360), (
428
+ west,
429
+ east,
430
+ first_field,
431
+ )
432
+
433
+ grid_values = list(range(len(grid_points[0])))
434
+
435
+ self._grid_points = grid_points
436
+ self._resolution = first_field.resolution
437
+ self._grid_values = grid_values
438
+ self._field_shape = first_field.shape
439
+ self._proj_string = first_field.proj_string if hasattr(first_field, "proj_string") else None
440
+
441
+ @property
442
+ def variables(self):
443
+ self.build_coords()
444
+ return self._variables
445
+
446
+ @property
447
+ def ensembles(self):
448
+ self.build_coords()
449
+ return self._ensembles
450
+
451
+ @property
452
+ def resolution(self):
453
+ self.build_coords()
454
+ return self._resolution
455
+
456
+ @property
457
+ def grid_values(self):
458
+ self.build_coords()
459
+ return self._grid_values
460
+
461
+ @property
462
+ def grid_points(self):
463
+ self.build_coords()
464
+ return self._grid_points
465
+
466
+ @property
467
+ def field_shape(self):
468
+ self.build_coords()
469
+ return self._field_shape
470
+
471
+ @property
472
+ def proj_string(self):
473
+ self.build_coords()
474
+ return self._proj_string
475
+
476
+ @cached_property
477
+ def shape(self):
478
+ return [
479
+ len(self.dates),
480
+ len(self.variables),
481
+ len(self.ensembles),
482
+ len(self.grid_values),
483
+ ]
484
+
485
+ @cached_property
486
+ def coords(self):
487
+ return {
488
+ "dates": self.dates,
489
+ "variables": self.variables,
490
+ "ensembles": self.ensembles,
491
+ "values": self.grid_values,
492
+ }
493
+
394
494
 
395
495
  class EmptyResult(Result):
396
496
  empty = True
@@ -399,18 +499,34 @@ class EmptyResult(Result):
399
499
  super().__init__(context, action_path + ["empty"], dates)
400
500
 
401
501
  @cached_property
402
- @assert_fieldset
502
+ @assert_fieldlist
403
503
  @trace_datasource
404
504
  def datasource(self):
405
- from climetlab import load_source
505
+ from earthkit.data import from_source
406
506
 
407
- return load_source("empty")
507
+ return from_source("empty")
408
508
 
409
509
  @property
410
510
  def variables(self):
411
511
  return []
412
512
 
413
513
 
514
+ def _flatten(ds):
515
+ if isinstance(ds, MultiFieldList):
516
+ return [_tidy(f) for s in ds._indexes for f in _flatten(s)]
517
+ return [ds]
518
+
519
+
520
+ def _tidy(ds, indent=0):
521
+ if isinstance(ds, MultiFieldList):
522
+
523
+ sources = [s for s in _flatten(ds) if len(s) > 0]
524
+ if len(sources) == 1:
525
+ return sources[0]
526
+ return MultiFieldList(sources)
527
+ return ds
528
+
529
+
414
530
  class FunctionResult(Result):
415
531
  def __init__(self, context, action_path, dates, action):
416
532
  super().__init__(context, action_path, dates)
@@ -423,14 +539,14 @@ class FunctionResult(Result):
423
539
  return f"{self.action.name}({shorten(self.dates)})"
424
540
 
425
541
  @cached_property
426
- @assert_fieldset
542
+ @assert_fieldlist
427
543
  @notify_result
428
544
  @trace_datasource
429
545
  def datasource(self):
430
546
  args, kwargs = resolve(self.context, (self.args, self.kwargs))
431
547
 
432
548
  try:
433
- return self.action.function(FunctionContext(self), self.dates, *args, **kwargs)
549
+ return _tidy(self.action.function(FunctionContext(self), self.dates, *args, **kwargs))
434
550
  except Exception:
435
551
  LOG.error(f"Error in {self.action.function.__name__}", exc_info=True)
436
552
  raise
@@ -452,14 +568,14 @@ class JoinResult(Result):
452
568
  self.results = [r for r in results if not r.empty]
453
569
 
454
570
  @cached_property
455
- @assert_fieldset
571
+ @assert_fieldlist
456
572
  @notify_result
457
573
  @trace_datasource
458
574
  def datasource(self):
459
575
  ds = EmptyResult(self.context, self.action_path, self.dates).datasource
460
576
  for i in self.results:
461
577
  ds += i.datasource
462
- return ds
578
+ return _tidy(ds)
463
579
 
464
580
  def __repr__(self):
465
581
  content = "\n".join([str(i) for i in self.results])
@@ -504,11 +620,11 @@ class UnShiftResult(Result):
504
620
  return f"{self.action.delta}({shorten(self.dates)})"
505
621
 
506
622
  @cached_property
507
- @assert_fieldset
623
+ @assert_fieldlist
508
624
  @notify_result
509
625
  @trace_datasource
510
626
  def datasource(self):
511
- from climetlab.indexing.fieldset import FieldArray
627
+ from earthkit.data.indexing.fieldlist import FieldArray
512
628
 
513
629
  class DateShiftedField:
514
630
  def __init__(self, field, delta):
@@ -533,7 +649,7 @@ class UnShiftResult(Result):
533
649
 
534
650
  ds = self.result.datasource
535
651
  ds = FieldArray([DateShiftedField(fs, self.action.delta) for fs in ds])
536
- return ds
652
+ return _tidy(ds)
537
653
 
538
654
 
539
655
  class FunctionAction(Action):
@@ -615,16 +731,18 @@ class StepAction(Action):
615
731
 
616
732
  class StepFunctionResult(StepResult):
617
733
  @cached_property
618
- @assert_fieldset
734
+ @assert_fieldlist
619
735
  @notify_result
620
736
  @trace_datasource
621
737
  def datasource(self):
622
738
  try:
623
- return self.action.function(
624
- FunctionContext(self),
625
- self.upstream_result.datasource,
626
- *self.action.args[1:],
627
- **self.action.kwargs,
739
+ return _tidy(
740
+ self.action.function(
741
+ FunctionContext(self),
742
+ self.upstream_result.datasource,
743
+ *self.action.args[1:],
744
+ **self.action.kwargs,
745
+ )
628
746
  )
629
747
 
630
748
  except Exception:
@@ -638,12 +756,12 @@ class StepFunctionResult(StepResult):
638
756
  class FilterStepResult(StepResult):
639
757
  @property
640
758
  @notify_result
641
- @assert_fieldset
759
+ @assert_fieldlist
642
760
  @trace_datasource
643
761
  def datasource(self):
644
762
  ds = self.upstream_result.datasource
645
763
  ds = ds.sel(**self.action.kwargs)
646
- return ds
764
+ return _tidy(ds)
647
765
 
648
766
 
649
767
  class FilterStepAction(StepAction):
@@ -665,14 +783,14 @@ class ConcatResult(Result):
665
783
  self.results = [r for r in results if not r.empty]
666
784
 
667
785
  @cached_property
668
- @assert_fieldset
786
+ @assert_fieldlist
669
787
  @notify_result
670
788
  @trace_datasource
671
789
  def datasource(self):
672
790
  ds = EmptyResult(self.context, self.action_path, self.dates).datasource
673
791
  for i in self.results:
674
792
  ds += i.datasource
675
- return ds
793
+ return _tidy(ds)
676
794
 
677
795
  @property
678
796
  def variables(self):
@@ -708,7 +826,7 @@ class DataSourcesResult(Result):
708
826
  self.context.notify_result(i.action_path[:-1], i.datasource)
709
827
  # then return the input result
710
828
  # which can use the datasources of the included results
711
- return self.input_result.datasource
829
+ return _tidy(self.input_result.datasource)
712
830
 
713
831
 
714
832
  class DataSourcesAction(Action):