climate-ref-esmvaltool 0.6.6__py3-none-any.whl → 0.7.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 (22) hide show
  1. climate_ref_esmvaltool/diagnostics/base.py +74 -44
  2. climate_ref_esmvaltool/diagnostics/climate_at_global_warming_levels.py +37 -14
  3. climate_ref_esmvaltool/diagnostics/climate_drivers_for_fire.py +37 -15
  4. climate_ref_esmvaltool/diagnostics/cloud_radiative_effects.py +24 -18
  5. climate_ref_esmvaltool/diagnostics/cloud_scatterplots.py +19 -7
  6. climate_ref_esmvaltool/diagnostics/ecs.py +23 -2
  7. climate_ref_esmvaltool/diagnostics/enso.py +78 -8
  8. climate_ref_esmvaltool/diagnostics/example.py +11 -10
  9. climate_ref_esmvaltool/diagnostics/regional_historical_changes.py +116 -21
  10. climate_ref_esmvaltool/diagnostics/sea_ice_area_basic.py +58 -3
  11. climate_ref_esmvaltool/diagnostics/sea_ice_sensitivity.py +26 -12
  12. climate_ref_esmvaltool/diagnostics/tcr.py +14 -1
  13. climate_ref_esmvaltool/diagnostics/tcre.py +8 -10
  14. climate_ref_esmvaltool/diagnostics/zec.py +15 -1
  15. climate_ref_esmvaltool/recipe.py +9 -5
  16. {climate_ref_esmvaltool-0.6.6.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/METADATA +1 -1
  17. climate_ref_esmvaltool-0.7.0.dist-info/RECORD +30 -0
  18. climate_ref_esmvaltool-0.6.6.dist-info/RECORD +0 -30
  19. {climate_ref_esmvaltool-0.6.6.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/WHEEL +0 -0
  20. {climate_ref_esmvaltool-0.6.6.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/entry_points.txt +0 -0
  21. {climate_ref_esmvaltool-0.6.6.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/licenses/LICENCE +0 -0
  22. {climate_ref_esmvaltool-0.6.6.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/licenses/NOTICE +0 -0
@@ -93,10 +93,11 @@ class ESMValToolDiagnostic(CommandLineDiagnostic):
93
93
  }
94
94
  recipe = load_recipe(self.base_recipe)
95
95
  self.update_recipe(recipe, input_files)
96
-
96
+ recipe_txt = yaml.safe_dump(recipe, sort_keys=False)
97
+ logger.info(f"Using ESMValTool recipe:\n{recipe_txt}")
97
98
  recipe_path = definition.to_output_path("recipe.yml")
98
99
  with recipe_path.open("w", encoding="utf-8") as file:
99
- yaml.safe_dump(recipe, file, sort_keys=False)
100
+ file.write(recipe_txt)
100
101
  return recipe_path
101
102
 
102
103
  def build_cmd(self, definition: ExecutionDefinition) -> Iterable[str]:
@@ -166,8 +167,10 @@ class ESMValToolDiagnostic(CommandLineDiagnostic):
166
167
 
167
168
  config_dir = definition.to_output_path("config")
168
169
  config_dir.mkdir()
170
+ config_txt = yaml.safe_dump(config)
171
+ logger.info(f"Using ESMValTool configuration:\n{config_txt}")
169
172
  with (config_dir / "config.yml").open("w", encoding="utf-8") as file:
170
- yaml.safe_dump(config, file)
173
+ file.write(config_txt)
171
174
 
172
175
  return [
173
176
  "esmvaltool",
@@ -198,12 +201,16 @@ class ESMValToolDiagnostic(CommandLineDiagnostic):
198
201
  metric_args = CMECMetric.create_template()
199
202
  output_args = CMECOutput.create_template()
200
203
 
204
+ # Input selectors for the datasets used in the diagnostic.
205
+ # TODO: Better handling of multiple source types
206
+ if SourceDatasetType.CMIP6 in definition.datasets:
207
+ input_selectors = definition.datasets[SourceDatasetType.CMIP6].selector_dict()
208
+ elif SourceDatasetType.obs4MIPs in definition.datasets:
209
+ input_selectors = definition.datasets[SourceDatasetType.obs4MIPs].selector_dict()
210
+ else:
211
+ input_selectors = {}
212
+
201
213
  # Add the plots and data files
202
- variable_attributes = (
203
- "long_name",
204
- "standard_name",
205
- "units",
206
- )
207
214
  series = []
208
215
  plot_suffixes = {".png", ".jpg", ".pdf", ".ps"}
209
216
  for metadata_file in result_dir.glob("run/*/*/diagnostic_provenance.yml"):
@@ -220,41 +227,11 @@ class ESMValToolDiagnostic(CommandLineDiagnostic):
220
227
  OutputCV.LONG_NAME.value: caption,
221
228
  OutputCV.DESCRIPTION.value: "",
222
229
  }
223
- for series_def in definition.diagnostic.series:
224
- if fnmatch.fnmatch(str(relative_path), f"executions/*/{series_def.file_pattern}"):
225
- dataset = xr.open_dataset(
226
- filename, decode_times=xr.coders.CFDatetimeCoder(use_cftime=True)
227
- )
228
- dataset = dataset.sel(series_def.sel)
229
- attributes = {
230
- attr: dataset.attrs[attr]
231
- for attr in series_def.attributes
232
- if attr in dataset.attrs
233
- }
234
- attributes["caption"] = caption
235
- attributes["values_name"] = series_def.values_name
236
- attributes["index_name"] = series_def.index_name
237
- for attr in variable_attributes:
238
- if attr in dataset[series_def.values_name].attrs:
239
- attributes[f"value_{attr}"] = dataset[series_def.values_name].attrs[attr]
240
- if attr in dataset[series_def.index_name].attrs:
241
- attributes[f"index_{attr}"] = dataset[series_def.index_name].attrs[attr]
242
- index = dataset[series_def.index_name].values.tolist()
243
- if hasattr(index[0], "calendar"):
244
- attributes["calendar"] = index[0].calendar
245
- if hasattr(index[0], "isoformat"):
246
- # Convert time objects to strings.
247
- index = [v.isoformat() for v in index]
248
-
249
- series.append(
250
- SeriesMetricValue(
251
- dimensions=series_def.dimensions,
252
- values=dataset[series_def.values_name].values.tolist(),
253
- index=index,
254
- index_name=series_def.index_name,
255
- attributes=attributes,
256
- )
257
- )
230
+ series.extend(
231
+ self._extract_series_from_file(
232
+ definition, filename, relative_path, caption=caption, input_selectors=input_selectors
233
+ )
234
+ )
258
235
 
259
236
  # Add the index.html file
260
237
  index_html = f"{result_dir}/index.html"
@@ -278,7 +255,6 @@ class ESMValToolDiagnostic(CommandLineDiagnostic):
278
255
 
279
256
  # Add the extra information from the groupby operations
280
257
  if len(metric_bundle.DIMENSIONS[MetricCV.JSON_STRUCTURE.value]):
281
- input_selectors = definition.datasets[SourceDatasetType.CMIP6].selector_dict()
282
258
  metric_bundle = metric_bundle.prepend_dimensions(input_selectors)
283
259
 
284
260
  return ExecutionResult.build_from_output_bundle(
@@ -287,3 +263,57 @@ class ESMValToolDiagnostic(CommandLineDiagnostic):
287
263
  cmec_metric_bundle=metric_bundle,
288
264
  series=series,
289
265
  )
266
+
267
+ def _extract_series_from_file(
268
+ self,
269
+ definition: ExecutionDefinition,
270
+ filename: Path,
271
+ relative_path: Path,
272
+ caption: str,
273
+ input_selectors: dict[str, str],
274
+ ) -> list[SeriesMetricValue]:
275
+ """
276
+ Extract series data from a file if it matches any of the series definitions.
277
+ """
278
+ variable_attributes = (
279
+ "long_name",
280
+ "standard_name",
281
+ "units",
282
+ )
283
+
284
+ series = []
285
+ for series_def in definition.diagnostic.series:
286
+ if fnmatch.fnmatch(
287
+ str(relative_path),
288
+ f"executions/*/{series_def.file_pattern.format(**input_selectors)}",
289
+ ):
290
+ dataset = xr.open_dataset(filename, decode_times=xr.coders.CFDatetimeCoder(use_cftime=True))
291
+ dataset = dataset.sel(series_def.sel)
292
+ attributes = {
293
+ attr: dataset.attrs[attr] for attr in series_def.attributes if attr in dataset.attrs
294
+ }
295
+ attributes["caption"] = caption
296
+ attributes["values_name"] = series_def.values_name
297
+ attributes["index_name"] = series_def.index_name
298
+ for attr in variable_attributes:
299
+ if attr in dataset[series_def.values_name].attrs:
300
+ attributes[f"value_{attr}"] = dataset[series_def.values_name].attrs[attr]
301
+ if attr in dataset[series_def.index_name].attrs:
302
+ attributes[f"index_{attr}"] = dataset[series_def.index_name].attrs[attr]
303
+ index = dataset[series_def.index_name].values.tolist()
304
+ if hasattr(index[0], "calendar"):
305
+ attributes["calendar"] = index[0].calendar
306
+ if hasattr(index[0], "isoformat"):
307
+ # Convert time objects to strings.
308
+ index = [v.isoformat() for v in index]
309
+
310
+ series.append(
311
+ SeriesMetricValue(
312
+ dimensions={**input_selectors, **series_def.dimensions},
313
+ values=dataset[series_def.values_name].values.tolist(),
314
+ index=index,
315
+ index_name=series_def.index_name,
316
+ attributes=attributes,
317
+ )
318
+ )
319
+ return series
@@ -2,8 +2,9 @@ import pandas
2
2
 
3
3
  from climate_ref_core.constraints import (
4
4
  AddSupplementaryDataset,
5
- RequireContiguousTimerange,
5
+ PartialDateTime,
6
6
  RequireFacets,
7
+ RequireTimerange,
7
8
  )
8
9
  from climate_ref_core.datasets import FacetFilter, SourceDatasetType
9
10
  from climate_ref_core.diagnostics import DataRequirement
@@ -26,6 +27,14 @@ class ClimateAtGlobalWarmingLevels(ESMValToolDiagnostic):
26
27
  "tas",
27
28
  )
28
29
 
30
+ matching_facets = (
31
+ "source_id",
32
+ "member_id",
33
+ "grid_label",
34
+ "table_id",
35
+ "variable_id",
36
+ )
37
+
29
38
  data_requirements = (
30
39
  DataRequirement(
31
40
  source_type=SourceDatasetType.CMIP6,
@@ -39,25 +48,32 @@ class ClimateAtGlobalWarmingLevels(ESMValToolDiagnostic):
39
48
  "ssp370",
40
49
  "ssp585",
41
50
  ),
51
+ "table_id": "Amon",
42
52
  },
43
53
  ),
44
54
  ),
45
55
  group_by=("experiment_id",),
46
56
  constraints=(
47
- RequireFacets("variable_id", variables),
48
57
  AddSupplementaryDataset(
49
58
  supplementary_facets={"experiment_id": "historical"},
50
- matching_facets=(
51
- "source_id",
52
- "member_id",
53
- "grid_label",
54
- "table_id",
55
- "variable_id",
56
- ),
59
+ matching_facets=matching_facets,
57
60
  optional_matching_facets=tuple(),
58
61
  ),
59
- RequireFacets("experiment_id", ("historical",)),
60
- RequireContiguousTimerange(group_by=("instance_id",)),
62
+ RequireTimerange(
63
+ group_by=matching_facets,
64
+ start=PartialDateTime(year=1850, month=1),
65
+ end=PartialDateTime(year=2100, month=12),
66
+ ),
67
+ RequireFacets(
68
+ "experiment_id",
69
+ required_facets=("historical",),
70
+ group_by=matching_facets,
71
+ ),
72
+ RequireFacets(
73
+ "variable_id",
74
+ required_facets=variables,
75
+ group_by=("experiment_id", "source_id", "member_id", "grid_label", "table_id"),
76
+ ),
61
77
  AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
62
78
  ),
63
79
  ),
@@ -74,13 +90,20 @@ class ClimateAtGlobalWarmingLevels(ESMValToolDiagnostic):
74
90
  diagnostics = recipe["diagnostics"]
75
91
  for diagnostic in diagnostics.values():
76
92
  diagnostic.pop("additional_datasets")
77
- recipe_variables = dataframe_to_recipe(input_files[SourceDatasetType.CMIP6])
93
+ recipe_variables = dataframe_to_recipe(
94
+ input_files[SourceDatasetType.CMIP6],
95
+ group_by=(
96
+ "source_id",
97
+ "member_id",
98
+ "grid_label",
99
+ "table_id",
100
+ "variable_id",
101
+ ),
102
+ )
78
103
  datasets = recipe_variables["tas"]["additional_datasets"]
79
104
  datasets = [ds for ds in datasets if ds["exp"] != "historical"]
80
105
  for dataset in datasets:
81
106
  dataset.pop("timerange")
82
- dataset["activity"] = ["CMIP", dataset["activity"]]
83
- dataset["exp"] = ["historical", dataset["exp"]]
84
107
  recipe["datasets"] = datasets
85
108
 
86
109
  # Specify the timeranges
@@ -2,8 +2,9 @@ import pandas
2
2
 
3
3
  from climate_ref_core.constraints import (
4
4
  AddSupplementaryDataset,
5
+ PartialDateTime,
5
6
  RequireFacets,
6
- RequireOverlappingTimerange,
7
+ RequireTimerange,
7
8
  )
8
9
  from climate_ref_core.datasets import FacetFilter, SourceDatasetType
9
10
  from climate_ref_core.diagnostics import DataRequirement
@@ -21,32 +22,53 @@ class ClimateDriversForFire(ESMValToolDiagnostic):
21
22
  slug = "climate-drivers-for-fire"
22
23
  base_recipe = "ref/recipe_ref_fire.yml"
23
24
 
24
- variables = (
25
- "cVeg",
26
- "hurs",
27
- "pr",
28
- "tas",
29
- "tasmax",
30
- "treeFrac",
31
- "vegFrac",
32
- )
33
25
  data_requirements = (
34
26
  DataRequirement(
35
27
  source_type=SourceDatasetType.CMIP6,
36
28
  filters=(
37
29
  FacetFilter(
38
- facets={
39
- "variable_id": variables,
40
- "frequency": "mon",
30
+ {
31
+ "variable_id": ("hurs", "pr", "tas", "tasmax"),
32
+ "experiment_id": "historical",
33
+ "table_id": "Amon",
34
+ }
35
+ ),
36
+ FacetFilter(
37
+ {
38
+ "variable_id": ("cVeg", "treeFrac"),
41
39
  "experiment_id": "historical",
40
+ "table_id": "Lmon",
41
+ }
42
+ ),
43
+ FacetFilter(
44
+ {
45
+ "variable_id": "vegFrac",
46
+ "experiment_id": "historical",
47
+ "table_id": "Emon",
42
48
  }
43
49
  ),
44
50
  ),
45
51
  group_by=("source_id", "member_id", "grid_label"),
46
52
  constraints=(
47
- RequireFacets("variable_id", variables),
48
- RequireOverlappingTimerange(group_by=("instance_id",)),
53
+ RequireTimerange(
54
+ group_by=("instance_id",),
55
+ start=PartialDateTime(2013, 1),
56
+ end=PartialDateTime(2014, 12),
57
+ ),
49
58
  AddSupplementaryDataset.from_defaults("sftlf", SourceDatasetType.CMIP6),
59
+ RequireFacets(
60
+ "variable_id",
61
+ (
62
+ "cVeg",
63
+ "hurs",
64
+ "pr",
65
+ "tas",
66
+ "tasmax",
67
+ "sftlf",
68
+ "treeFrac",
69
+ "vegFrac",
70
+ ),
71
+ ),
50
72
  ),
51
73
  ),
52
74
  )
@@ -2,12 +2,14 @@ import pandas
2
2
 
3
3
  from climate_ref_core.constraints import (
4
4
  AddSupplementaryDataset,
5
- RequireContiguousTimerange,
5
+ PartialDateTime,
6
6
  RequireFacets,
7
7
  RequireOverlappingTimerange,
8
+ RequireTimerange,
8
9
  )
9
10
  from climate_ref_core.datasets import FacetFilter, SourceDatasetType
10
11
  from climate_ref_core.diagnostics import DataRequirement
12
+ from climate_ref_core.metric_values.typing import SeriesDefinition
11
13
  from climate_ref_esmvaltool.diagnostics.base import ESMValToolDiagnostic
12
14
  from climate_ref_esmvaltool.recipe import dataframe_to_recipe
13
15
  from climate_ref_esmvaltool.types import Recipe
@@ -22,8 +24,6 @@ class CloudRadiativeEffects(ESMValToolDiagnostic):
22
24
  slug = "cloud-radiative-effects"
23
25
  base_recipe = "ref/recipe_ref_cre.yml"
24
26
 
25
- facets = ()
26
-
27
27
  variables = (
28
28
  "rlut",
29
29
  "rlutcs",
@@ -37,40 +37,46 @@ class CloudRadiativeEffects(ESMValToolDiagnostic):
37
37
  FacetFilter(
38
38
  facets={
39
39
  "variable_id": variables,
40
- "experiment_id": ("historical",),
40
+ "experiment_id": "historical",
41
+ "table_id": "Amon",
41
42
  }
42
43
  ),
43
44
  ),
44
45
  group_by=("source_id", "member_id", "grid_label"),
45
46
  constraints=(
46
- RequireFacets("variable_id", variables),
47
- RequireContiguousTimerange(group_by=("instance_id",)),
47
+ RequireTimerange(
48
+ group_by=("instance_id",),
49
+ start=PartialDateTime(1996, 1),
50
+ end=PartialDateTime(2014, 12),
51
+ ),
48
52
  RequireOverlappingTimerange(group_by=("instance_id",)),
53
+ RequireFacets("variable_id", variables),
49
54
  AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
50
55
  ),
51
56
  ),
52
57
  # TODO: Use CERES-EBAF, ESACCI-CLOUD, and ISCCP-FH from obs4MIPs once available.
53
58
  )
54
59
 
60
+ facets = ()
61
+ series = tuple(
62
+ SeriesDefinition(
63
+ file_pattern=f"plot_profiles/plot/variable_vs_lat_{var_name}_*.nc",
64
+ sel={"dim0": 0}, # Select the model and not the observations.
65
+ dimensions={"statistic": f"{var_name} zonal mean"},
66
+ values_name=var_name,
67
+ index_name="lat",
68
+ attributes=[],
69
+ )
70
+ for var_name in ["lwcre", "swcre"]
71
+ )
72
+
55
73
  @staticmethod
56
74
  def update_recipe(recipe: Recipe, input_files: dict[SourceDatasetType, pandas.DataFrame]) -> None:
57
75
  """Update the recipe."""
58
76
  recipe_variables = dataframe_to_recipe(input_files[SourceDatasetType.CMIP6])
59
77
  recipe_variables = {k: v for k, v in recipe_variables.items() if k != "areacella"}
60
78
 
61
- # Select a timerange covered by all datasets.
62
- start_times, end_times = [], []
63
- for variable in recipe_variables.values():
64
- for dataset in variable["additional_datasets"]:
65
- start, end = dataset["timerange"].split("/")
66
- start_times.append(start)
67
- end_times.append(end)
68
- start_time = max(start_times)
69
- start_time = max(start_time, "20010101T000000") # Earliest observational dataset availability
70
- timerange = f"{start_time}/{min(end_times)}"
71
-
72
79
  datasets = recipe_variables["rsut"]["additional_datasets"]
73
80
  for dataset in datasets:
74
81
  dataset.pop("timerange")
75
82
  recipe["datasets"] = datasets
76
- recipe["timerange_for_models"] = timerange
@@ -4,9 +4,9 @@ import pandas
4
4
 
5
5
  from climate_ref_core.constraints import (
6
6
  AddSupplementaryDataset,
7
- RequireContiguousTimerange,
7
+ PartialDateTime,
8
8
  RequireFacets,
9
- RequireOverlappingTimerange,
9
+ RequireTimerange,
10
10
  )
11
11
  from climate_ref_core.datasets import FacetFilter, SourceDatasetType
12
12
  from climate_ref_core.diagnostics import DataRequirement
@@ -25,15 +25,18 @@ def get_cmip6_data_requirements(variables: tuple[str, ...]) -> tuple[DataRequire
25
25
  facets={
26
26
  "variable_id": variables,
27
27
  "experiment_id": "historical",
28
+ "table_id": "Amon",
28
29
  },
29
30
  ),
30
31
  ),
31
32
  group_by=("source_id", "experiment_id", "member_id", "frequency", "grid_label"),
32
33
  constraints=(
34
+ RequireTimerange(
35
+ group_by=("instance_id",),
36
+ start=PartialDateTime(1996, 1),
37
+ end=PartialDateTime(2014, 12),
38
+ ),
33
39
  RequireFacets("variable_id", variables),
34
- RequireContiguousTimerange(group_by=("instance_id",)),
35
- RequireOverlappingTimerange(group_by=("instance_id",)),
36
- # TODO: Add a RequireTimeRange constraint to match reference datasets?
37
40
  AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
38
41
  ),
39
42
  ),
@@ -47,13 +50,16 @@ def update_recipe(
47
50
  var_y: str,
48
51
  ) -> None:
49
52
  """Update the recipe."""
50
- recipe_variables = dataframe_to_recipe(input_files[SourceDatasetType.CMIP6], equalize_timerange=True)
53
+ recipe_variables = dataframe_to_recipe(input_files[SourceDatasetType.CMIP6])
51
54
  diagnostics = recipe["diagnostics"]
52
55
  diagnostic_name = f"plot_joint_{var_x}_{var_y}_model"
53
56
  diagnostic = diagnostics.pop(diagnostic_name)
54
57
  diagnostics.clear()
55
58
  diagnostics[diagnostic_name] = diagnostic
59
+ recipe_variables = {k: v for k, v in recipe_variables.items() if k != "areacella"}
56
60
  datasets = next(iter(recipe_variables.values()))["additional_datasets"]
61
+ for dataset in datasets:
62
+ dataset["timerange"] = "1996/2014"
57
63
  diagnostic["additional_datasets"] = datasets
58
64
  suptitle = "CMIP6 {dataset} {ensemble} {grid} {timerange}".format(**datasets[0])
59
65
  diagnostic["scripts"]["plot"]["suptitle"] = suptitle
@@ -135,7 +141,13 @@ class CloudScatterplotsReference(ESMValToolDiagnostic):
135
141
  ),
136
142
  ),
137
143
  group_by=("instance_id",),
138
- constraints=(RequireContiguousTimerange(group_by=("instance_id",)),),
144
+ constraints=(
145
+ RequireTimerange(
146
+ group_by=("instance_id",),
147
+ start=PartialDateTime(2007, 1),
148
+ end=PartialDateTime(2014, 12),
149
+ ),
150
+ ),
139
151
  # TODO: Add obs4MIPs datasets once available and working:
140
152
  #
141
153
  # obs4MIPs datasets with issues:
@@ -11,6 +11,7 @@ from climate_ref_core.constraints import (
11
11
  )
12
12
  from climate_ref_core.datasets import ExecutionDatasetCollection, FacetFilter, SourceDatasetType
13
13
  from climate_ref_core.diagnostics import DataRequirement
14
+ from climate_ref_core.metric_values.typing import SeriesDefinition
14
15
  from climate_ref_core.pycmec.metric import CMECMetric, MetricCV
15
16
  from climate_ref_core.pycmec.output import CMECOutput
16
17
  from climate_ref_esmvaltool.diagnostics.base import ESMValToolDiagnostic
@@ -45,20 +46,40 @@ class EquilibriumClimateSensitivity(ESMValToolDiagnostic):
45
46
  facets={
46
47
  "variable_id": variables,
47
48
  "experiment_id": experiments,
49
+ "table_id": "Amon",
48
50
  },
49
51
  ),
50
52
  ),
51
53
  group_by=("source_id", "member_id", "grid_label"),
52
54
  constraints=(
53
- RequireFacets("variable_id", variables),
54
- RequireFacets("experiment_id", experiments),
55
55
  RequireContiguousTimerange(group_by=("instance_id",)),
56
56
  RequireOverlappingTimerange(group_by=("instance_id",)),
57
+ RequireFacets(
58
+ "variable_id",
59
+ required_facets=variables,
60
+ group_by=("source_id", "member_id", "grid_label", "experiment_id"),
61
+ ),
62
+ RequireFacets(
63
+ "experiment_id",
64
+ required_facets=experiments,
65
+ group_by=("source_id", "member_id", "grid_label", "variable_id"),
66
+ ),
57
67
  AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
58
68
  ),
59
69
  ),
60
70
  )
61
71
  facets = ("grid_label", "member_id", "source_id", "region", "metric")
72
+ series = (
73
+ SeriesDefinition(
74
+ file_pattern="ecs/calculate/ecs_regression_*.nc",
75
+ dimensions={
76
+ "statistic": ("global annual mean anomaly of rtnt vs tas"),
77
+ },
78
+ values_name="rtnt_anomaly",
79
+ index_name="tas_anomaly",
80
+ attributes=[],
81
+ ),
82
+ )
62
83
 
63
84
  @staticmethod
64
85
  def update_recipe(
@@ -11,6 +11,7 @@ from climate_ref_core.constraints import (
11
11
  )
12
12
  from climate_ref_core.datasets import ExecutionDatasetCollection, FacetFilter, SourceDatasetType
13
13
  from climate_ref_core.diagnostics import DataRequirement
14
+ from climate_ref_core.metric_values.typing import SeriesDefinition
14
15
  from climate_ref_core.pycmec.metric import CMECMetric, MetricCV
15
16
  from climate_ref_core.pycmec.output import CMECOutput
16
17
  from climate_ref_esmvaltool.diagnostics.base import ESMValToolDiagnostic
@@ -27,32 +28,97 @@ class ENSOBasicClimatology(ESMValToolDiagnostic):
27
28
  slug = "enso-basic-climatology"
28
29
  base_recipe = "ref/recipe_enso_basicclimatology.yml"
29
30
 
30
- variables = (
31
- "pr",
32
- "tos",
33
- "tauu",
34
- )
35
31
  data_requirements = (
36
32
  DataRequirement(
37
33
  source_type=SourceDatasetType.CMIP6,
38
34
  filters=(
39
35
  FacetFilter(
40
36
  facets={
41
- "variable_id": variables,
37
+ "variable_id": ("pr", "tauu"),
38
+ "experiment_id": "historical",
39
+ "table_id": "Amon",
40
+ },
41
+ ),
42
+ FacetFilter(
43
+ facets={
44
+ "variable_id": "tos",
42
45
  "experiment_id": "historical",
46
+ "table_id": "Omon",
43
47
  },
44
48
  ),
45
49
  ),
46
50
  group_by=("source_id", "member_id", "grid_label"),
47
51
  constraints=(
48
- RequireFacets("variable_id", variables),
49
52
  RequireContiguousTimerange(group_by=("instance_id",)),
50
53
  RequireOverlappingTimerange(group_by=("instance_id",)),
54
+ RequireFacets(
55
+ "variable_id",
56
+ (
57
+ "pr",
58
+ "tauu",
59
+ "tos",
60
+ ),
61
+ ),
51
62
  ),
52
63
  ),
53
64
  )
54
65
  facets = ()
55
66
 
67
+ series = (
68
+ tuple(
69
+ SeriesDefinition(
70
+ file_pattern=f"diagnostic_metrics/plot_script/{{source_id}}_eq_{var_name}_bias.nc",
71
+ dimensions={
72
+ "statistic": (
73
+ f"zonal bias in the time-mean {var_name} structure across the equatorial Pacific"
74
+ ),
75
+ },
76
+ values_name="tos" if var_name == "sst" else var_name,
77
+ index_name="lon",
78
+ attributes=[],
79
+ )
80
+ for var_name in ["pr", "sst", "tauu"]
81
+ )
82
+ + tuple(
83
+ SeriesDefinition(
84
+ file_pattern=f"diagnostic_metrics/plot_script/{{source_id}}_eq_{var_name}_seacycle.nc",
85
+ dimensions={
86
+ "statistic": (
87
+ "zonal bias in the amplitude of the mean seasonal cycle of "
88
+ f"{var_name} in the equatorial Pacific"
89
+ ),
90
+ },
91
+ values_name="tos" if var_name == "sst" else var_name,
92
+ index_name="lon",
93
+ attributes=[],
94
+ )
95
+ for var_name in ["pr", "sst", "tauu"]
96
+ )
97
+ + (
98
+ SeriesDefinition(
99
+ file_pattern="diagnostic_metrics/plot_script/{source_id}_pr_double.nc",
100
+ dimensions={
101
+ "statistic": ("meridional bias in the time-mean pr structure across the eastern Pacific"),
102
+ },
103
+ values_name="pr",
104
+ index_name="lat",
105
+ attributes=[],
106
+ ),
107
+ SeriesDefinition(
108
+ file_pattern="diagnostic_metrics/plot_script/*_pr_double_seacycle.nc",
109
+ dimensions={
110
+ "statistic": (
111
+ "meridional bias in the amplitude of the mean seasonal "
112
+ "pr cycle in the eastern Pacific"
113
+ ),
114
+ },
115
+ values_name="pr",
116
+ index_name="lat",
117
+ attributes=[],
118
+ ),
119
+ )
120
+ )
121
+
56
122
  @staticmethod
57
123
  def update_recipe(
58
124
  recipe: Recipe,
@@ -85,19 +151,23 @@ class ENSOCharacteristics(ESMValToolDiagnostic):
85
151
  facets={
86
152
  "variable_id": "tos",
87
153
  "experiment_id": "historical",
154
+ "table_id": "Omon",
88
155
  },
89
156
  ),
90
157
  ),
91
158
  group_by=("source_id", "member_id", "grid_label"),
92
159
  constraints=(
93
- RequireFacets("variable_id", ("tos",)),
94
160
  RequireContiguousTimerange(group_by=("instance_id",)),
95
161
  RequireOverlappingTimerange(group_by=("instance_id",)),
96
162
  AddSupplementaryDataset.from_defaults("areacello", SourceDatasetType.CMIP6),
163
+ RequireFacets("variable_id", ("tos", "areacello")),
97
164
  ),
98
165
  ),
99
166
  )
100
167
  facets = ("grid_label", "member_id", "source_id", "region", "metric")
168
+ # ENSO pattern and lifecycle are series, but the ESMValTool diagnostic
169
+ # script does not save the values used in the figure.
170
+ series = tuple()
101
171
 
102
172
  @staticmethod
103
173
  def update_recipe(