climate-ref-esmvaltool 0.6.5__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 (27) hide show
  1. climate_ref_esmvaltool/dataset_registry/data.txt +4 -0
  2. climate_ref_esmvaltool/diagnostics/__init__.py +22 -0
  3. climate_ref_esmvaltool/diagnostics/base.py +112 -14
  4. climate_ref_esmvaltool/diagnostics/climate_at_global_warming_levels.py +41 -15
  5. climate_ref_esmvaltool/diagnostics/climate_drivers_for_fire.py +90 -0
  6. climate_ref_esmvaltool/diagnostics/cloud_radiative_effects.py +26 -20
  7. climate_ref_esmvaltool/diagnostics/cloud_scatterplots.py +200 -0
  8. climate_ref_esmvaltool/diagnostics/ecs.py +32 -20
  9. climate_ref_esmvaltool/diagnostics/enso.py +88 -12
  10. climate_ref_esmvaltool/diagnostics/example.py +17 -3
  11. climate_ref_esmvaltool/diagnostics/regional_historical_changes.py +435 -0
  12. climate_ref_esmvaltool/diagnostics/sea_ice_area_basic.py +63 -5
  13. climate_ref_esmvaltool/diagnostics/sea_ice_sensitivity.py +31 -14
  14. climate_ref_esmvaltool/diagnostics/tcr.py +23 -19
  15. climate_ref_esmvaltool/diagnostics/tcre.py +13 -12
  16. climate_ref_esmvaltool/diagnostics/zec.py +20 -3
  17. climate_ref_esmvaltool/recipe.py +55 -10
  18. climate_ref_esmvaltool/recipes.txt +16 -11
  19. climate_ref_esmvaltool/requirements/conda-lock.yml +4081 -3770
  20. climate_ref_esmvaltool/requirements/environment.yml +1 -0
  21. {climate_ref_esmvaltool-0.6.5.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/METADATA +1 -1
  22. climate_ref_esmvaltool-0.7.0.dist-info/RECORD +30 -0
  23. climate_ref_esmvaltool-0.6.5.dist-info/RECORD +0 -27
  24. {climate_ref_esmvaltool-0.6.5.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/WHEEL +0 -0
  25. {climate_ref_esmvaltool-0.6.5.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/entry_points.txt +0 -0
  26. {climate_ref_esmvaltool-0.6.5.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/licenses/LICENCE +0 -0
  27. {climate_ref_esmvaltool-0.6.5.dist-info → climate_ref_esmvaltool-0.7.0.dist-info}/licenses/NOTICE +0 -0
@@ -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
@@ -39,22 +40,37 @@ class TransientClimateResponse(ESMValToolDiagnostic):
39
40
  facets={
40
41
  "variable_id": ("tas",),
41
42
  "experiment_id": experiments,
43
+ "table_id": "Amon",
42
44
  },
43
45
  ),
44
46
  ),
45
47
  group_by=("source_id", "member_id", "grid_label"),
46
48
  constraints=(
47
- RequireFacets("experiment_id", experiments),
48
49
  RequireContiguousTimerange(group_by=("instance_id",)),
49
50
  RequireOverlappingTimerange(group_by=("instance_id",)),
51
+ RequireFacets("experiment_id", experiments),
50
52
  AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
51
53
  ),
52
54
  ),
53
55
  )
54
56
  facets = ("grid_label", "member_id", "source_id", "region", "metric")
57
+ series = (
58
+ SeriesDefinition(
59
+ file_pattern="tcr/calculate/{source_id}*.nc",
60
+ dimensions={
61
+ "statistic": "global annual mean tas anomaly relative to linear fit of piControl run",
62
+ },
63
+ values_name="tas_anomaly",
64
+ index_name="time",
65
+ attributes=[],
66
+ ),
67
+ )
55
68
 
56
69
  @staticmethod
57
- def update_recipe(recipe: Recipe, input_files: pandas.DataFrame) -> None:
70
+ def update_recipe(
71
+ recipe: Recipe,
72
+ input_files: dict[SourceDatasetType, pandas.DataFrame],
73
+ ) -> None:
58
74
  """Update the recipe."""
59
75
  # Only run the diagnostic that computes TCR for a single model.
60
76
  recipe["diagnostics"] = {
@@ -77,21 +93,11 @@ class TransientClimateResponse(ESMValToolDiagnostic):
77
93
  # Prepare updated datasets section in recipe. It contains two
78
94
  # datasets, one for the "1pctCO2" and one for the "piControl"
79
95
  # experiment.
80
- recipe_variables = dataframe_to_recipe(input_files)
81
- recipe_variables = {k: v for k, v in recipe_variables.items() if k != "areacella"}
82
-
83
- # Select a timerange covered by all datasets.
84
- start_times, end_times = [], []
85
- for variable in recipe_variables.values():
86
- for dataset in variable["additional_datasets"]:
87
- start, end = dataset["timerange"].split("/")
88
- start_times.append(start)
89
- end_times.append(end)
90
- timerange = f"{max(start_times)}/{min(end_times)}"
91
-
92
- datasets = recipe_variables["tas"]["additional_datasets"]
93
- for dataset in datasets:
94
- dataset["timerange"] = timerange
96
+ recipe_variables = dataframe_to_recipe(
97
+ input_files[SourceDatasetType.CMIP6],
98
+ equalize_timerange=True,
99
+ )
100
+ recipe["datasets"] = recipe_variables["tas"]["additional_datasets"]
95
101
 
96
102
  # Remove keys from the recipe that are only used for YAML anchors
97
103
  keys_to_remove = [
@@ -102,8 +108,6 @@ class TransientClimateResponse(ESMValToolDiagnostic):
102
108
  for key in keys_to_remove:
103
109
  recipe.pop(key, None)
104
110
 
105
- recipe["datasets"] = datasets
106
-
107
111
  @staticmethod
108
112
  def format_result(
109
113
  result_dir: Path,
@@ -27,10 +27,6 @@ class TransientClimateResponseEmissions(ESMValToolDiagnostic):
27
27
  slug = "transient-climate-response-emissions"
28
28
  base_recipe = "recipe_tcre.yml"
29
29
 
30
- experiments = (
31
- "esm-1pctCO2",
32
- "esm-piControl",
33
- )
34
30
  variables = (
35
31
  "tas",
36
32
  "fco2antt",
@@ -42,37 +38,42 @@ class TransientClimateResponseEmissions(ESMValToolDiagnostic):
42
38
  FacetFilter(
43
39
  facets={
44
40
  "variable_id": variables,
45
- "frequency": "mon",
46
- "experiment_id": experiments,
41
+ "experiment_id": "esm-1pctCO2",
42
+ "table_id": "Amon",
47
43
  },
48
44
  ),
49
45
  FacetFilter(
50
46
  facets={
51
- "variable_id": "fco2antt",
47
+ "variable_id": "tas",
52
48
  "experiment_id": "esm-piControl",
49
+ "table_id": "Amon",
53
50
  },
54
- keep=False,
55
51
  ),
56
52
  ),
57
53
  group_by=("source_id", "member_id", "grid_label"),
58
54
  constraints=(
59
- RequireFacets("experiment_id", experiments),
60
- RequireFacets("variable_id", variables),
61
55
  RequireContiguousTimerange(group_by=("instance_id",)),
62
56
  RequireOverlappingTimerange(group_by=("instance_id",)),
57
+ RequireFacets("experiment_id", ("esm-1pctCO2", "esm-piControl")),
58
+ RequireFacets("variable_id", variables),
63
59
  AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
64
60
  ),
65
61
  ),
66
62
  )
67
63
  facets = ("grid_label", "member_id", "source_id", "region", "metric")
64
+ # TODO: the ESMValTool diagnostic script does not save the data for the timeseries.
65
+ series = tuple()
68
66
 
69
67
  @staticmethod
70
- def update_recipe(recipe: Recipe, input_files: pandas.DataFrame) -> None:
68
+ def update_recipe(
69
+ recipe: Recipe,
70
+ input_files: dict[SourceDatasetType, pandas.DataFrame],
71
+ ) -> None:
71
72
  """Update the recipe."""
72
73
  # Prepare updated datasets section in recipe. It contains three
73
74
  # datasets, "tas" and "fco2antt" for the "esm-1pctCO2" and just "tas"
74
75
  # for the "esm-piControl" experiment.
75
- recipe_variables = dataframe_to_recipe(input_files)
76
+ recipe_variables = dataframe_to_recipe(input_files[SourceDatasetType.CMIP6])
76
77
  tas_esm_1pctCO2 = next(
77
78
  ds for ds in recipe_variables["tas"]["additional_datasets"] if ds["exp"] == "esm-1pctCO2"
78
79
  )
@@ -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
@@ -39,27 +40,43 @@ class ZeroEmissionCommitment(ESMValToolDiagnostic):
39
40
  facets={
40
41
  "variable_id": ("tas",),
41
42
  "experiment_id": experiments,
43
+ "table_id": "Amon",
42
44
  },
43
45
  ),
44
46
  ),
45
47
  group_by=("source_id", "member_id", "grid_label"),
46
48
  constraints=(
47
- RequireFacets("experiment_id", experiments),
48
49
  RequireContiguousTimerange(group_by=("instance_id",)),
49
50
  RequireOverlappingTimerange(group_by=("instance_id",)),
51
+ RequireFacets("experiment_id", experiments),
50
52
  AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
51
53
  ),
52
54
  ),
53
55
  )
54
56
  facets = ("grid_label", "member_id", "source_id", "region", "metric")
57
+ series = (
58
+ SeriesDefinition(
59
+ file_pattern="work/zec/zec/zec.nc",
60
+ sel={"dim0": 0},
61
+ dimensions={
62
+ "statistic": "zec",
63
+ },
64
+ values_name="zec",
65
+ index_name="time",
66
+ attributes=[],
67
+ ),
68
+ )
55
69
 
56
70
  @staticmethod
57
- def update_recipe(recipe: Recipe, input_files: pandas.DataFrame) -> None:
71
+ def update_recipe(
72
+ recipe: Recipe,
73
+ input_files: dict[SourceDatasetType, pandas.DataFrame],
74
+ ) -> None:
58
75
  """Update the recipe."""
59
76
  # Prepare updated datasets section in recipe. It contains two
60
77
  # datasets, one for the "esm-1pct-brch-1000PgC" and one for the "piControl"
61
78
  # experiment.
62
- datasets = dataframe_to_recipe(input_files)["tas"]["additional_datasets"]
79
+ datasets = dataframe_to_recipe(input_files[SourceDatasetType.CMIP6])["tas"]["additional_datasets"]
63
80
  base_dataset = next(ds for ds in datasets if ds["exp"] == "1pctCO2")
64
81
  dataset = next(ds for ds in datasets if ds["exp"] == "esm-1pct-brch-1000PgC")
65
82
  start = dataset["timerange"].split("/")[0]
@@ -12,6 +12,7 @@ from climate_ref_esmvaltool.types import Recipe
12
12
  if TYPE_CHECKING:
13
13
  import pandas as pd
14
14
 
15
+
15
16
  FACETS = {
16
17
  "CMIP6": {
17
18
  "activity": "activity_id",
@@ -23,6 +24,13 @@ FACETS = {
23
24
  "mip": "table_id",
24
25
  "short_name": "variable_id",
25
26
  },
27
+ "obs4MIPs": {
28
+ "dataset": "source_id",
29
+ "frequency": "frequency",
30
+ "grid": "grid_label",
31
+ "institute": "institution_id",
32
+ "short_name": "variable_id",
33
+ },
26
34
  }
27
35
 
28
36
 
@@ -79,42 +87,65 @@ def as_facets(
79
87
 
80
88
  """
81
89
  facets = {}
82
- first_row = group.iloc[0]
83
- project = first_row.instance_id.split(".", 2)[0]
90
+ project = group.iloc[0].instance_id.split(".", 2)[0]
84
91
  facets["project"] = project
85
92
  for esmvaltool_name, ref_name in FACETS[project].items():
86
- facets[esmvaltool_name] = getattr(first_row, ref_name)
93
+ values = group[ref_name].unique().tolist()
94
+ facets[esmvaltool_name] = values if len(values) > 1 else values[0]
87
95
  timerange = as_timerange(group)
88
96
  if timerange is not None:
89
97
  facets["timerange"] = timerange
90
98
  return facets
91
99
 
92
100
 
93
- def dataframe_to_recipe(files: pd.DataFrame) -> dict[str, Any]:
101
+ def dataframe_to_recipe(
102
+ files: pd.DataFrame,
103
+ group_by: tuple[str, ...] = ("instance_id",),
104
+ equalize_timerange: bool = False,
105
+ ) -> dict[str, Any]:
94
106
  """Convert the datasets dataframe to a recipe "variables" section.
95
107
 
96
108
  Parameters
97
109
  ----------
98
110
  files
99
111
  The pandas dataframe describing the input files.
112
+ group_by
113
+ The columns to group the input files by.
114
+ equalize_timerange
115
+ If True, use the timerange that is covered by all datasets.
100
116
 
101
117
  Returns
102
118
  -------
103
119
  A "variables" section that can be used in an ESMValTool recipe.
104
120
  """
105
121
  variables: dict[str, Any] = {}
106
- # TODO: refine to make it possible to combine historical and scenario runs.
107
- for _, group in files.groupby("instance_id"):
122
+ for _, group in files.groupby(list(group_by)):
108
123
  facets = as_facets(group)
109
124
  short_name = facets.pop("short_name")
110
125
  if short_name not in variables:
111
126
  variables[short_name] = {"additional_datasets": []}
112
127
  variables[short_name]["additional_datasets"].append(facets)
128
+
129
+ if equalize_timerange:
130
+ # Select a timerange covered by all datasets.
131
+ start_times, end_times = [], []
132
+ for variable in variables.values():
133
+ for dataset in variable["additional_datasets"]:
134
+ if "timerange" in dataset:
135
+ start, end = dataset["timerange"].split("/")
136
+ start_times.append(start)
137
+ end_times.append(end)
138
+ timerange = f"{max(start_times)}/{min(end_times)}"
139
+ for variable in variables.values():
140
+ for dataset in variable["additional_datasets"]:
141
+ if "timerange" in dataset:
142
+ dataset["timerange"] = timerange
143
+
113
144
  return variables
114
145
 
115
146
 
116
- _ESMVALTOOL_COMMIT = "8f56863a70ba4df76ec501ba0372c571a0af6cf9"
117
- _ESMVALTOOL_VERSION = f"2.13.0.dev120+g{_ESMVALTOOL_COMMIT[:9]}"
147
+ _ESMVALTOOL_COMMIT = "2c438d0e0cc8904790294c72450eb7f06552c52a"
148
+ _ESMVALTOOL_VERSION = f"2.13.0.dev148+g{_ESMVALTOOL_COMMIT[:9]}"
118
149
 
119
150
  _RECIPES = pooch.create(
120
151
  path=pooch.os_cache("climate_ref_esmvaltool"),
@@ -142,7 +173,16 @@ def load_recipe(recipe: str) -> Recipe:
142
173
  The loaded recipe.
143
174
  """
144
175
  filename = _RECIPES.fetch(recipe)
145
- return yaml.safe_load(Path(filename).read_text(encoding="utf-8")) # type: ignore[no-any-return]
176
+
177
+ def normalize(obj: Any) -> Any:
178
+ # Ensure objects in the recipe are not shared.
179
+ if isinstance(obj, dict):
180
+ return {k: normalize(v) for k, v in obj.items()}
181
+ if isinstance(obj, list):
182
+ return [normalize(item) for item in obj]
183
+ return obj
184
+
185
+ return normalize(yaml.safe_load(Path(filename).read_text(encoding="utf-8"))) # type: ignore[no-any-return]
146
186
 
147
187
 
148
188
  def prepare_climate_data(datasets: pd.DataFrame, climate_data_dir: Path) -> None:
@@ -165,6 +205,11 @@ def prepare_climate_data(datasets: pd.DataFrame, climate_data_dir: Path) -> None
165
205
  if not isinstance(row.path, str): # pragma: no branch
166
206
  msg = f"Invalid path encountered in {row}"
167
207
  raise ValueError(msg)
168
- tgt = climate_data_dir.joinpath(*row.instance_id.split(".")) / Path(row.path).name
208
+ if row.instance_id.startswith("obs4MIPs."):
209
+ version = row.instance_id.split(".")[-1]
210
+ subdirs: list[str] = ["obs4MIPs", row.source_id, version] # type: ignore[list-item]
211
+ else:
212
+ subdirs = row.instance_id.split(".")
213
+ tgt = climate_data_dir.joinpath(*subdirs) / Path(row.path).name
169
214
  tgt.parent.mkdir(parents=True, exist_ok=True)
170
215
  tgt.symlink_to(row.path)
@@ -1,11 +1,16 @@
1
- examples/recipe_python.yml ab3f06d269bb2c1368f4dc39da9bcb232fb2adb1fa556ba769e6c16294ffb4a3
2
- recipe_calculate_gwl_exceedance_stats.yml 5aa266abc9a8029649b689a2b369a47623b0935d609354332ff4148994642d6b
3
- recipe_ecs.yml 0cc57034fcb64e32015b4ff949ece5df8cdb8c6f493618b50ceded119fb37918
4
- recipe_seaice_sensitivity.yml f7247c076e161c582d422947c8155f3ca98549e6f2e4c3b1c76414786d7e50c5
5
- recipe_tcr.yml 35f9ef035a4e71aff5cac5dd26c49da2162fc00291bf3b0bd16b661b7b2f606b
6
- recipe_tcre.yml 48fc9e3baf541bbcef7491853ea3a774053771dca33352b41466425faeaa38af
7
- recipe_zec.yml b0af7f789b7610ab3f29a6617124aa40c40866ead958204fc199eaf82863de51
8
- ref/recipe_enso_basicclimatology.yml 9ea7deb7ee668e39ac44618b96496d898bd82285c22dcee4fce4695e0c9fa82b
9
- ref/recipe_enso_characteristics.yml 34c2518b138068ac96d212910b979d54a8fcedee2c0089b5acd56a42c41dc3e4
10
- ref/recipe_ref_cre.yml 4375f262479c3b3e1b348b71080a6d758e195bda76516a591182045a3a29aa32
11
- ref/recipe_ref_sea_ice_area_basic.yml 7d01a8527880663ca28284772f83a8356d9972fb4f022a4000e50a56ce044b09
1
+ examples/recipe_python.yml ab3f06d269bb2c1368f4dc39da9bcb232fb2adb1fa556ba769e6c16294ffb4a3
2
+ recipe_calculate_gwl_exceedance_stats.yml 5aa266abc9a8029649b689a2b369a47623b0935d609354332ff4148994642d6b
3
+ recipe_ecs.yml 0cc57034fcb64e32015b4ff949ece5df8cdb8c6f493618b50ceded119fb37918
4
+ recipe_seaice_sensitivity.yml f7247c076e161c582d422947c8155f3ca98549e6f2e4c3b1c76414786d7e50c5
5
+ recipe_tcr.yml 35f9ef035a4e71aff5cac5dd26c49da2162fc00291bf3b0bd16b661b7b2f606b
6
+ recipe_tcre.yml 48fc9e3baf541bbcef7491853ea3a774053771dca33352b41466425faeaa38af
7
+ recipe_zec.yml b0af7f789b7610ab3f29a6617124aa40c40866ead958204fc199eaf82863de51
8
+ ref/recipe_enso_basicclimatology.yml 9ea7deb7ee668e39ac44618b96496d898bd82285c22dcee4fce4695e0c9fa82b
9
+ ref/recipe_enso_characteristics.yml 34c2518b138068ac96d212910b979d54a8fcedee2c0089b5acd56a42c41dc3e4
10
+ ref/recipe_ref_annual_cycle_region.yml 64ebc687789dad6c45a2361b45218cb5a0ad0e38c516840c65fc7e8bf7b5ace7
11
+ ref/recipe_ref_cre.yml 4375f262479c3b3e1b348b71080a6d758e195bda76516a591182045a3a29aa32
12
+ ref/recipe_ref_fire.yml 2ad82effaca4e742d8abe6a0aa07bb46e1e92ef0d2d240760f7623b0ba045926
13
+ ref/recipe_ref_sea_ice_area_basic.yml 7d01a8527880663ca28284772f83a8356d9972fb4f022a4000e50a56ce044b09
14
+ ref/recipe_ref_scatterplot.yml b99d1736e16256d161847b025811d7088ad9f892d4887fb009fa99c4079135a0
15
+ ref/recipe_ref_timeseries_region.yml 86f36e442021caba201601d8cf4624f8ce6715ce421670a467c792db2910db22
16
+ ref/recipe_ref_trend_regions.yml 18fe246a51474bd12172ab1ba141efac999a247de7774822f77ae6ef144645fe