climate-ref-pmp 0.5.5__py3-none-any.whl → 0.6.1__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.
- climate_ref_pmp/__init__.py +10 -2
- climate_ref_pmp/diagnostics/__init__.py +2 -0
- climate_ref_pmp/diagnostics/annual_cycle.py +51 -42
- climate_ref_pmp/diagnostics/enso.py +245 -0
- climate_ref_pmp/diagnostics/variability_modes.py +70 -7
- climate_ref_pmp/drivers/enso_driver.py +458 -0
- climate_ref_pmp/requirements/conda-lock.yml +1809 -2362
- climate_ref_pmp/requirements/environment.yml +1 -0
- {climate_ref_pmp-0.5.5.dist-info → climate_ref_pmp-0.6.1.dist-info}/METADATA +3 -3
- climate_ref_pmp-0.6.1.dist-info/RECORD +20 -0
- climate_ref_pmp-0.5.5.dist-info/RECORD +0 -18
- {climate_ref_pmp-0.5.5.dist-info → climate_ref_pmp-0.6.1.dist-info}/WHEEL +0 -0
- {climate_ref_pmp-0.5.5.dist-info → climate_ref_pmp-0.6.1.dist-info}/licenses/LICENCE +0 -0
- {climate_ref_pmp-0.5.5.dist-info → climate_ref_pmp-0.6.1.dist-info}/licenses/NOTICE +0 -0
climate_ref_pmp/__init__.py
CHANGED
|
@@ -6,7 +6,7 @@ import importlib.metadata
|
|
|
6
6
|
|
|
7
7
|
from climate_ref_core.dataset_registry import DATASET_URL, dataset_registry_manager
|
|
8
8
|
from climate_ref_core.providers import CondaDiagnosticProvider
|
|
9
|
-
from climate_ref_pmp.diagnostics import AnnualCycle, ExtratropicalModesOfVariability
|
|
9
|
+
from climate_ref_pmp.diagnostics import ENSO, AnnualCycle, ExtratropicalModesOfVariability
|
|
10
10
|
|
|
11
11
|
__version__ = importlib.metadata.version("climate-ref-pmp")
|
|
12
12
|
|
|
@@ -14,6 +14,15 @@ __version__ = importlib.metadata.version("climate-ref-pmp")
|
|
|
14
14
|
# PMP uses a conda environment to run the diagnostics
|
|
15
15
|
provider = CondaDiagnosticProvider("PMP", __version__)
|
|
16
16
|
|
|
17
|
+
# Annual cycle diagnostics and metrics
|
|
18
|
+
provider.register(AnnualCycle())
|
|
19
|
+
|
|
20
|
+
# ENSO diagnostics and metrics
|
|
21
|
+
# provider.register(ENSO("ENSO_perf")) # Assigned to ESMValTool
|
|
22
|
+
provider.register(ENSO("ENSO_tel"))
|
|
23
|
+
provider.register(ENSO("ENSO_proc"))
|
|
24
|
+
|
|
25
|
+
# Extratropical modes of variability diagnostics and metrics
|
|
17
26
|
provider.register(ExtratropicalModesOfVariability("PDO"))
|
|
18
27
|
provider.register(ExtratropicalModesOfVariability("NPGO"))
|
|
19
28
|
provider.register(ExtratropicalModesOfVariability("NAO"))
|
|
@@ -21,7 +30,6 @@ provider.register(ExtratropicalModesOfVariability("NAM"))
|
|
|
21
30
|
provider.register(ExtratropicalModesOfVariability("PNA"))
|
|
22
31
|
provider.register(ExtratropicalModesOfVariability("NPO"))
|
|
23
32
|
provider.register(ExtratropicalModesOfVariability("SAM"))
|
|
24
|
-
provider.register(AnnualCycle())
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
dataset_registry_manager.register(
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""PMP diagnostics."""
|
|
2
2
|
|
|
3
3
|
from climate_ref_pmp.diagnostics.annual_cycle import AnnualCycle
|
|
4
|
+
from climate_ref_pmp.diagnostics.enso import ENSO
|
|
4
5
|
from climate_ref_pmp.diagnostics.variability_modes import ExtratropicalModesOfVariability
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
8
|
+
"ENSO",
|
|
7
9
|
"AnnualCycle",
|
|
8
10
|
"ExtratropicalModesOfVariability",
|
|
9
11
|
]
|
|
@@ -15,6 +15,44 @@ from climate_ref_core.pycmec.metric import remove_dimensions
|
|
|
15
15
|
from climate_ref_pmp.pmp_driver import build_glob_pattern, build_pmp_command, process_json_result
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def make_data_requirement(variable_id: str, obs_source: str) -> tuple[DataRequirement, DataRequirement]:
|
|
19
|
+
"""
|
|
20
|
+
Create a data requirement for the annual cycle diagnostic.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
variable_id : str
|
|
25
|
+
The variable ID to filter the data requirement.
|
|
26
|
+
obs_source : str
|
|
27
|
+
The observation source ID to filter the data requirement.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
DataRequirement
|
|
32
|
+
A DataRequirement object containing the necessary filters and groupings.
|
|
33
|
+
"""
|
|
34
|
+
return (
|
|
35
|
+
DataRequirement(
|
|
36
|
+
source_type=SourceDatasetType.PMPClimatology,
|
|
37
|
+
filters=(FacetFilter(facets={"source_id": (obs_source,), "variable_id": (variable_id,)}),),
|
|
38
|
+
group_by=("variable_id", "source_id"),
|
|
39
|
+
),
|
|
40
|
+
DataRequirement(
|
|
41
|
+
source_type=SourceDatasetType.CMIP6,
|
|
42
|
+
filters=(
|
|
43
|
+
FacetFilter(
|
|
44
|
+
facets={
|
|
45
|
+
"frequency": "mon",
|
|
46
|
+
"experiment_id": ("amip", "historical", "hist-GHG", "piControl"),
|
|
47
|
+
"variable_id": (variable_id,),
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
group_by=("variable_id", "source_id", "experiment_id", "member_id", "grid_label"),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
18
56
|
class AnnualCycle(CommandLineDiagnostic):
|
|
19
57
|
"""
|
|
20
58
|
Calculate the annual cycle for a dataset
|
|
@@ -32,49 +70,20 @@ class AnnualCycle(CommandLineDiagnostic):
|
|
|
32
70
|
"statistic",
|
|
33
71
|
"season",
|
|
34
72
|
)
|
|
73
|
+
|
|
35
74
|
data_requirements = (
|
|
36
|
-
|
|
37
|
-
(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"frequency": "mon",
|
|
49
|
-
"experiment_id": ("amip", "historical", "hist-GHG", "piControl"),
|
|
50
|
-
"variable_id": ("ts",),
|
|
51
|
-
}
|
|
52
|
-
),
|
|
53
|
-
),
|
|
54
|
-
group_by=("variable_id", "source_id", "experiment_id", "member_id"),
|
|
55
|
-
),
|
|
56
|
-
),
|
|
57
|
-
# Precipitation
|
|
58
|
-
(
|
|
59
|
-
DataRequirement(
|
|
60
|
-
source_type=SourceDatasetType.PMPClimatology,
|
|
61
|
-
filters=(FacetFilter(facets={"source_id": ("GPCP-Monthly-3-2",), "variable_id": ("pr",)}),),
|
|
62
|
-
group_by=("variable_id", "source_id"),
|
|
63
|
-
),
|
|
64
|
-
DataRequirement(
|
|
65
|
-
source_type=SourceDatasetType.CMIP6,
|
|
66
|
-
filters=(
|
|
67
|
-
FacetFilter(
|
|
68
|
-
facets={
|
|
69
|
-
"frequency": "mon",
|
|
70
|
-
"experiment_id": ("amip", "historical", "hist-GHG", "piControl"),
|
|
71
|
-
"variable_id": ("pr",),
|
|
72
|
-
}
|
|
73
|
-
),
|
|
74
|
-
),
|
|
75
|
-
group_by=("variable_id", "source_id", "experiment_id", "member_id"),
|
|
76
|
-
),
|
|
77
|
-
),
|
|
75
|
+
make_data_requirement("ts", "ERA-5"),
|
|
76
|
+
make_data_requirement("uas", "ERA-5"),
|
|
77
|
+
make_data_requirement("vas", "ERA-5"),
|
|
78
|
+
make_data_requirement("psl", "ERA-5"),
|
|
79
|
+
make_data_requirement("pr", "GPCP-Monthly-3-2"),
|
|
80
|
+
make_data_requirement("rlds", "CERES-EBAF-4-2"),
|
|
81
|
+
make_data_requirement("rlus", "CERES-EBAF-4-2"),
|
|
82
|
+
make_data_requirement("rlut", "CERES-EBAF-4-2"),
|
|
83
|
+
make_data_requirement("rsds", "CERES-EBAF-4-2"),
|
|
84
|
+
make_data_requirement("rsdt", "CERES-EBAF-4-2"),
|
|
85
|
+
make_data_requirement("rsus", "CERES-EBAF-4-2"),
|
|
86
|
+
make_data_requirement("rsut", "CERES-EBAF-4-2"),
|
|
78
87
|
)
|
|
79
88
|
|
|
80
89
|
def __init__(self) -> None:
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import Collection, Iterable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from climate_ref_core.constraints import AddSupplementaryDataset
|
|
9
|
+
from climate_ref_core.datasets import DatasetCollection, FacetFilter, SourceDatasetType
|
|
10
|
+
from climate_ref_core.diagnostics import (
|
|
11
|
+
CommandLineDiagnostic,
|
|
12
|
+
DataRequirement,
|
|
13
|
+
ExecutionDefinition,
|
|
14
|
+
ExecutionResult,
|
|
15
|
+
)
|
|
16
|
+
from climate_ref_pmp.pmp_driver import _get_resource, process_json_result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ENSO(CommandLineDiagnostic):
|
|
20
|
+
"""
|
|
21
|
+
Calculate the ENSO performance metrics for a dataset
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
facets = ("source_id", "member_id", "grid_label", "experiment_id", "metric", "reference_datasets")
|
|
25
|
+
|
|
26
|
+
def __init__(self, metrics_collection: str, experiments: Collection[str] = ("historical",)) -> None:
|
|
27
|
+
self.name = metrics_collection
|
|
28
|
+
self.slug = metrics_collection.lower()
|
|
29
|
+
self.metrics_collection = metrics_collection
|
|
30
|
+
self.parameter_file = "pmp_param_enso.py"
|
|
31
|
+
self.obs_sources: tuple[str, ...]
|
|
32
|
+
self.model_variables: tuple[str, ...]
|
|
33
|
+
|
|
34
|
+
if metrics_collection == "ENSO_perf": # pragma: no cover
|
|
35
|
+
self.model_variables = ("pr", "ts", "tauu")
|
|
36
|
+
self.obs_sources = ("GPCP-Monthly-3-2", "TropFlux-1-0", "HadISST-1-1")
|
|
37
|
+
elif metrics_collection == "ENSO_tel":
|
|
38
|
+
self.model_variables = ("pr", "ts")
|
|
39
|
+
self.obs_sources = ("GPCP-Monthly-3-2", "TropFlux-1-0", "HadISST-1-1")
|
|
40
|
+
elif metrics_collection == "ENSO_proc":
|
|
41
|
+
self.model_variables = ("ts", "tauu", "hfls", "hfss", "rlds", "rlus", "rsds", "rsus")
|
|
42
|
+
self.obs_sources = (
|
|
43
|
+
"GPCP-Monthly-3-2",
|
|
44
|
+
"TropFlux-1-0",
|
|
45
|
+
"HadISST-1-1",
|
|
46
|
+
"CERES-EBAF-4-2",
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Unknown metrics collection: {metrics_collection}. "
|
|
51
|
+
"Valid options are: ENSO_perf, ENSO_tel, ENSO_proc"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self.data_requirements = self._get_data_requirements(experiments)
|
|
55
|
+
|
|
56
|
+
def _get_data_requirements(
|
|
57
|
+
self,
|
|
58
|
+
experiments: Collection[str] = ("historical",),
|
|
59
|
+
) -> tuple[DataRequirement, DataRequirement]:
|
|
60
|
+
filters = [
|
|
61
|
+
FacetFilter(
|
|
62
|
+
facets={
|
|
63
|
+
"frequency": "mon",
|
|
64
|
+
"experiment_id": tuple(experiments),
|
|
65
|
+
"variable_id": self.model_variables,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
DataRequirement(
|
|
72
|
+
source_type=SourceDatasetType.obs4MIPs,
|
|
73
|
+
filters=(
|
|
74
|
+
FacetFilter(facets={"source_id": self.obs_sources, "variable_id": self.model_variables}),
|
|
75
|
+
),
|
|
76
|
+
group_by=("activity_id",),
|
|
77
|
+
),
|
|
78
|
+
DataRequirement(
|
|
79
|
+
source_type=SourceDatasetType.CMIP6,
|
|
80
|
+
filters=tuple(filters),
|
|
81
|
+
group_by=("source_id", "experiment_id", "member_id", "grid_label"),
|
|
82
|
+
constraints=(
|
|
83
|
+
AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
|
|
84
|
+
AddSupplementaryDataset.from_defaults("sftlf", SourceDatasetType.CMIP6),
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def build_cmd(self, definition: ExecutionDefinition) -> Iterable[str]:
|
|
90
|
+
"""
|
|
91
|
+
Run the diagnostic on the given configuration.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
definition : ExecutionDefinition
|
|
96
|
+
The configuration to run the diagnostic on.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
:
|
|
101
|
+
The result of running the diagnostic.
|
|
102
|
+
"""
|
|
103
|
+
mc_name = self.metrics_collection
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------
|
|
106
|
+
# Get the input datasets information for the model
|
|
107
|
+
# ------------------------------------------------
|
|
108
|
+
input_datasets = definition.datasets[SourceDatasetType.CMIP6]
|
|
109
|
+
input_selectors = input_datasets.selector_dict()
|
|
110
|
+
source_id = input_selectors["source_id"]
|
|
111
|
+
member_id = input_selectors["member_id"]
|
|
112
|
+
experiment_id = input_selectors["experiment_id"]
|
|
113
|
+
variable_ids = set(input_datasets["variable_id"].unique()) - {"areacella", "sftlf"}
|
|
114
|
+
mod_run = f"{source_id}_{member_id}"
|
|
115
|
+
|
|
116
|
+
# We only need one entry for the model run
|
|
117
|
+
dict_mod: dict[str, dict[str, Any]] = {mod_run: {}}
|
|
118
|
+
|
|
119
|
+
def extract_variable(dc: DatasetCollection, variable: str) -> list[str]:
|
|
120
|
+
return dc.datasets[input_datasets["variable_id"] == variable]["path"].to_list() # type: ignore
|
|
121
|
+
|
|
122
|
+
# TO DO: Get the path to the files per variable
|
|
123
|
+
for variable in variable_ids:
|
|
124
|
+
list_files = extract_variable(input_datasets, variable)
|
|
125
|
+
list_areacella = extract_variable(input_datasets, "areacella")
|
|
126
|
+
list_sftlf = extract_variable(input_datasets, "sftlf")
|
|
127
|
+
|
|
128
|
+
if len(list_files) > 0:
|
|
129
|
+
dict_mod[mod_run][variable] = {
|
|
130
|
+
"path + filename": list_files,
|
|
131
|
+
"varname": variable,
|
|
132
|
+
"path + filename_area": list_areacella,
|
|
133
|
+
"areaname": "areacella",
|
|
134
|
+
"path + filename_landmask": list_sftlf,
|
|
135
|
+
"landmaskname": "sftlf",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# -------------------------------------------------------
|
|
139
|
+
# Get the input datasets information for the observations
|
|
140
|
+
# -------------------------------------------------------
|
|
141
|
+
reference_dataset = definition.datasets[SourceDatasetType.obs4MIPs]
|
|
142
|
+
reference_dataset_names = reference_dataset["source_id"].unique()
|
|
143
|
+
|
|
144
|
+
dict_obs: dict[str, dict[str, Any]] = {}
|
|
145
|
+
|
|
146
|
+
# TO DO: Get the path to the files per variable and per source
|
|
147
|
+
for obs_name in reference_dataset_names:
|
|
148
|
+
dict_obs[obs_name] = {}
|
|
149
|
+
for variable in variable_ids:
|
|
150
|
+
# Get the list of files for the current variable and observation source
|
|
151
|
+
list_files = reference_dataset.datasets[
|
|
152
|
+
(reference_dataset["variable_id"] == variable)
|
|
153
|
+
& (reference_dataset["source_id"] == obs_name)
|
|
154
|
+
]["path"].to_list()
|
|
155
|
+
# If the list is not empty, add it to the dictionary
|
|
156
|
+
if len(list_files) > 0:
|
|
157
|
+
dict_obs[obs_name][variable] = {
|
|
158
|
+
"path + filename": list_files,
|
|
159
|
+
"varname": variable,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Create input directory
|
|
163
|
+
dict_datasets = {
|
|
164
|
+
"model": dict_mod,
|
|
165
|
+
"observations": dict_obs,
|
|
166
|
+
"metricsCollection": mc_name,
|
|
167
|
+
"experiment_id": experiment_id,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Create JSON file for dictDatasets
|
|
171
|
+
json_file = os.path.join(
|
|
172
|
+
definition.output_directory, f"input_{mc_name}_{source_id}_{experiment_id}_{member_id}.json"
|
|
173
|
+
)
|
|
174
|
+
with open(json_file, "w") as f:
|
|
175
|
+
json.dump(dict_datasets, f, indent=4)
|
|
176
|
+
logger.debug(f"JSON file created: {json_file}")
|
|
177
|
+
|
|
178
|
+
driver_file = _get_resource("climate_ref_pmp.drivers", "enso_driver.py", use_resources=True)
|
|
179
|
+
return [
|
|
180
|
+
"python",
|
|
181
|
+
driver_file,
|
|
182
|
+
"--metrics_collection",
|
|
183
|
+
mc_name,
|
|
184
|
+
"--experiment_id",
|
|
185
|
+
experiment_id,
|
|
186
|
+
"--input_json_path",
|
|
187
|
+
json_file,
|
|
188
|
+
"--output_directory",
|
|
189
|
+
str(definition.output_directory),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
def build_execution_result(self, definition: ExecutionDefinition) -> ExecutionResult:
|
|
193
|
+
"""
|
|
194
|
+
Build a diagnostic result from the output of the PMP driver
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
definition
|
|
199
|
+
Definition of the diagnostic execution
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
Result of the diagnostic execution
|
|
204
|
+
"""
|
|
205
|
+
input_datasets = definition.datasets[SourceDatasetType.CMIP6]
|
|
206
|
+
source_id = input_datasets["source_id"].unique()[0]
|
|
207
|
+
experiment_id = input_datasets["experiment_id"].unique()[0]
|
|
208
|
+
member_id = input_datasets["member_id"].unique()[0]
|
|
209
|
+
mc_name = self.metrics_collection
|
|
210
|
+
pattern = f"{mc_name}_{source_id}_{experiment_id}_{member_id}"
|
|
211
|
+
|
|
212
|
+
# Find the results files
|
|
213
|
+
results_files = list(definition.output_directory.glob(f"{pattern}_cmec.json"))
|
|
214
|
+
logger.debug(f"Results files: {results_files}")
|
|
215
|
+
|
|
216
|
+
if len(results_files) != 1: # pragma: no cover
|
|
217
|
+
logger.warning(f"A single cmec output file not found: {results_files}")
|
|
218
|
+
return ExecutionResult.build_from_failure(definition)
|
|
219
|
+
|
|
220
|
+
# Find the other outputs
|
|
221
|
+
png_files = [definition.as_relative_path(f) for f in definition.output_directory.glob("*.png")]
|
|
222
|
+
data_files = [definition.as_relative_path(f) for f in definition.output_directory.glob("*.nc")]
|
|
223
|
+
|
|
224
|
+
cmec_output, cmec_metric = process_json_result(results_files[0], png_files, data_files)
|
|
225
|
+
|
|
226
|
+
input_selectors = definition.datasets[SourceDatasetType.CMIP6].selector_dict()
|
|
227
|
+
cmec_metric_bundle = cmec_metric.remove_dimensions(
|
|
228
|
+
[
|
|
229
|
+
"model",
|
|
230
|
+
"realization",
|
|
231
|
+
],
|
|
232
|
+
).prepend_dimensions(
|
|
233
|
+
{
|
|
234
|
+
"source_id": input_selectors["source_id"],
|
|
235
|
+
"member_id": input_selectors["member_id"],
|
|
236
|
+
"grid_label": input_selectors["grid_label"],
|
|
237
|
+
"experiment_id": input_selectors["experiment_id"],
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return ExecutionResult.build_from_output_bundle(
|
|
242
|
+
definition,
|
|
243
|
+
cmec_output_bundle=cmec_output,
|
|
244
|
+
cmec_metric_bundle=cmec_metric_bundle,
|
|
245
|
+
)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from collections.abc import Iterable
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Union
|
|
2
4
|
|
|
3
5
|
from loguru import logger
|
|
4
6
|
|
|
@@ -37,10 +39,10 @@ class ExtratropicalModesOfVariability(CommandLineDiagnostic):
|
|
|
37
39
|
self.name = f"Extratropical modes of variability: {mode_id}"
|
|
38
40
|
self.slug = f"extratropical-modes-of-variability-{mode_id.lower()}"
|
|
39
41
|
|
|
40
|
-
def
|
|
42
|
+
def _get_data_requirements(
|
|
41
43
|
obs_source: str,
|
|
42
44
|
obs_variable: str,
|
|
43
|
-
|
|
45
|
+
model_variable: str,
|
|
44
46
|
extra_experiments: str | tuple[str, ...] | list[str] = (),
|
|
45
47
|
) -> tuple[DataRequirement, DataRequirement]:
|
|
46
48
|
filters = [
|
|
@@ -48,7 +50,7 @@ class ExtratropicalModesOfVariability(CommandLineDiagnostic):
|
|
|
48
50
|
facets={
|
|
49
51
|
"frequency": "mon",
|
|
50
52
|
"experiment_id": ("historical", "hist-GHG", "piControl", *extra_experiments),
|
|
51
|
-
"variable_id":
|
|
53
|
+
"variable_id": model_variable,
|
|
52
54
|
}
|
|
53
55
|
)
|
|
54
56
|
]
|
|
@@ -64,17 +66,16 @@ class ExtratropicalModesOfVariability(CommandLineDiagnostic):
|
|
|
64
66
|
DataRequirement(
|
|
65
67
|
source_type=SourceDatasetType.CMIP6,
|
|
66
68
|
filters=tuple(filters),
|
|
67
|
-
|
|
68
|
-
group_by=("source_id", "experiment_id", "variant_label", "member_id"),
|
|
69
|
+
group_by=("source_id", "experiment_id", "member_id", "grid_label"),
|
|
69
70
|
),
|
|
70
71
|
)
|
|
71
72
|
|
|
72
73
|
if self.mode_id in self.ts_modes:
|
|
73
74
|
self.parameter_file = "pmp_param_MoV-ts.py"
|
|
74
|
-
self.data_requirements =
|
|
75
|
+
self.data_requirements = _get_data_requirements("HadISST-1-1", "ts", "ts")
|
|
75
76
|
elif self.mode_id in self.psl_modes:
|
|
76
77
|
self.parameter_file = "pmp_param_MoV-psl.py"
|
|
77
|
-
self.data_requirements =
|
|
78
|
+
self.data_requirements = _get_data_requirements("20CR", "psl", "psl", extra_experiments=("amip",))
|
|
78
79
|
else:
|
|
79
80
|
raise ValueError(
|
|
80
81
|
f"Unknown mode_id '{self.mode_id}'. Must be one of {self.ts_modes + self.psl_modes}"
|
|
@@ -172,6 +173,8 @@ class ExtratropicalModesOfVariability(CommandLineDiagnostic):
|
|
|
172
173
|
logger.warning(f"A single cmec output file not found: {results_files}")
|
|
173
174
|
return ExecutionResult.build_from_failure(definition)
|
|
174
175
|
|
|
176
|
+
clean_up_json(results_files[0])
|
|
177
|
+
|
|
175
178
|
# Find the other outputs
|
|
176
179
|
png_files = [definition.as_relative_path(f) for f in definition.output_directory.glob("*.png")]
|
|
177
180
|
data_files = [definition.as_relative_path(f) for f in definition.output_directory.glob("*.nc")]
|
|
@@ -201,3 +204,63 @@ class ExtratropicalModesOfVariability(CommandLineDiagnostic):
|
|
|
201
204
|
cmec_output_bundle=cmec_output_bundle,
|
|
202
205
|
cmec_metric_bundle=cmec_metric_bundle,
|
|
203
206
|
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def clean_up_json(json_file: Union[str, Path]) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Clean up the JSON file by removing unnecessary fields.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
json_file : str or Path
|
|
216
|
+
Path to the JSON file to clean up.
|
|
217
|
+
"""
|
|
218
|
+
import json
|
|
219
|
+
|
|
220
|
+
with open(str(json_file)) as f:
|
|
221
|
+
data = json.load(f)
|
|
222
|
+
|
|
223
|
+
# Remove null values from the JSON data
|
|
224
|
+
data = remove_null_values(data)
|
|
225
|
+
|
|
226
|
+
with open(str(json_file), "w") as f:
|
|
227
|
+
json.dump(data, f, indent=4)
|
|
228
|
+
|
|
229
|
+
# Log the cleanup action
|
|
230
|
+
logger.debug(f"Cleaned up JSON file: {json_file}")
|
|
231
|
+
logger.info("JSON file cleaned up successfully.")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def remove_null_values(data: Union[dict[Any, Any], list[Any], Any]) -> Union[dict[Any, Any], list[Any], Any]:
|
|
235
|
+
"""
|
|
236
|
+
Recursively removes keys with null (None) values from a dictionary or list.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
data : dict, list, or Any
|
|
241
|
+
The JSON-like data structure to process. It can be a dictionary, a list,
|
|
242
|
+
or any other type of data.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
dict, list, or Any
|
|
247
|
+
A new data structure with null values removed. If the input is a dictionary,
|
|
248
|
+
keys with `None` values are removed. If the input is a list, items are
|
|
249
|
+
recursively processed to remove `None` values. For other types, the input
|
|
250
|
+
is returned unchanged.
|
|
251
|
+
|
|
252
|
+
Examples
|
|
253
|
+
--------
|
|
254
|
+
>>> data = {
|
|
255
|
+
... "key1": None,
|
|
256
|
+
... "key2": {"subkey1": 123, "subkey2": None},
|
|
257
|
+
... "key3": [None, 456, {"subkey3": None}],
|
|
258
|
+
... }
|
|
259
|
+
>>> remove_null_values(data)
|
|
260
|
+
{'key2': {'subkey1': 123}, 'key3': [456, {}]}
|
|
261
|
+
"""
|
|
262
|
+
if isinstance(data, dict):
|
|
263
|
+
return {key: remove_null_values(value) for key, value in data.items() if value is not None}
|
|
264
|
+
if isinstance(data, list):
|
|
265
|
+
return [remove_null_values(item) for item in data if item is not None]
|
|
266
|
+
return data
|