climate-ref-pmp 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.
- climate_ref_pmp/__init__.py +32 -0
- climate_ref_pmp/dataset_registry/pmp_climatology.txt +25 -0
- climate_ref_pmp/diagnostics/__init__.py +9 -0
- climate_ref_pmp/diagnostics/annual_cycle.py +337 -0
- climate_ref_pmp/diagnostics/variability_modes.py +174 -0
- climate_ref_pmp/params/pmp_param_MoV-psl.py +90 -0
- climate_ref_pmp/params/pmp_param_MoV-ts.py +94 -0
- climate_ref_pmp/params/pmp_param_annualcycle_1-clims.py +21 -0
- climate_ref_pmp/params/pmp_param_annualcycle_2-metrics.py +54 -0
- climate_ref_pmp/pmp_driver.py +258 -0
- climate_ref_pmp/py.typed +0 -0
- climate_ref_pmp/requirements/conda-lock.yml +11790 -0
- climate_ref_pmp/requirements/environment.yml +6 -0
- climate_ref_pmp-0.5.0.dist-info/METADATA +68 -0
- climate_ref_pmp-0.5.0.dist-info/RECORD +18 -0
- climate_ref_pmp-0.5.0.dist-info/WHEEL +4 -0
- climate_ref_pmp-0.5.0.dist-info/licenses/LICENCE +201 -0
- climate_ref_pmp-0.5.0.dist-info/licenses/NOTICE +3 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rapid evaluating CMIP data
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
|
|
7
|
+
from climate_ref_core.dataset_registry import dataset_registry_manager
|
|
8
|
+
from climate_ref_core.providers import CondaDiagnosticProvider
|
|
9
|
+
from climate_ref_pmp.diagnostics import AnnualCycle, ExtratropicalModesOfVariability
|
|
10
|
+
|
|
11
|
+
__version__ = importlib.metadata.version("climate-ref-pmp")
|
|
12
|
+
|
|
13
|
+
# Create the PMP diagnostics provider
|
|
14
|
+
# PMP uses a conda environment to run the diagnostics
|
|
15
|
+
provider = CondaDiagnosticProvider("PMP", __version__)
|
|
16
|
+
|
|
17
|
+
provider.register(ExtratropicalModesOfVariability("PDO"))
|
|
18
|
+
provider.register(ExtratropicalModesOfVariability("NPGO"))
|
|
19
|
+
provider.register(ExtratropicalModesOfVariability("NAO"))
|
|
20
|
+
provider.register(ExtratropicalModesOfVariability("NAM"))
|
|
21
|
+
provider.register(ExtratropicalModesOfVariability("PNA"))
|
|
22
|
+
provider.register(ExtratropicalModesOfVariability("NPO"))
|
|
23
|
+
provider.register(ExtratropicalModesOfVariability("SAM"))
|
|
24
|
+
provider.register(AnnualCycle())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
dataset_registry_manager.register(
|
|
28
|
+
"pmp-climatology",
|
|
29
|
+
"https://pub-b093171261094c4ea9adffa01f94ee06.r2.dev/",
|
|
30
|
+
package="climate_ref_pmp.dataset_registry",
|
|
31
|
+
resource="pmp_climatology.txt",
|
|
32
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
PMP_obs4MIPsClims/pr/gr/v20250211/pr_mon_GPCP-Monthly-3-2_RSS_gr_198301-200412_AC_v20250211_interp_2.5x2.5.nc md5:02661bfe30f79b162619a960de08784d
|
|
2
|
+
PMP_obs4MIPsClims/psl/gr/v20250224/psl_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250224_2.5x2.5.nc md5:098fddaadcc4b5b72771dc493293f68c
|
|
3
|
+
PMP_obs4MIPsClims/rldscs/gr/v20250213/rldscs_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:f6ce612860eed79c04577f6bf53de6b8
|
|
4
|
+
PMP_obs4MIPsClims/rlds/gr/v20250213/rlds_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:fe229671795fffc1209e364cdde6bc58
|
|
5
|
+
PMP_obs4MIPsClims/rltcre/gr/v20250213/rltcre_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:28a2ea1301454f1c65381aaf18dc927f
|
|
6
|
+
PMP_obs4MIPsClims/rlus/gr/v20250213/rlus_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:c29d0182a759aeaf62a741b8b277bd31
|
|
7
|
+
PMP_obs4MIPsClims/rlutcs/gr/v20250213/rlutcs_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:d8255cb36020f616131e6abc3623c0ff
|
|
8
|
+
PMP_obs4MIPsClims/rlut/gr/v20250213/rlut_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:92427d58e789c28e69d4e7ad7f10f78a
|
|
9
|
+
PMP_obs4MIPsClims/rsdscs/gr/v20250213/rsdscs_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:4f7693ee9c739f8891d38430030d5d0f
|
|
10
|
+
PMP_obs4MIPsClims/rsds/gr/v20250213/rsds_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:631b4d9168fd4b558f1db8f3dc7d9fd8
|
|
11
|
+
PMP_obs4MIPsClims/rsdt/gr/v20250213/rsdt_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:6908e55d39dce5c696c2016c7a668759
|
|
12
|
+
PMP_obs4MIPsClims/rstcre/gr/v20250213/rstcre_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:dcb2ec36efcac91fb1060b4f53b9dc3a
|
|
13
|
+
PMP_obs4MIPsClims/rsuscs/gr/v20250213/rsuscs_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:0e06fdbab1f44a14d85774a65d3a97ab
|
|
14
|
+
PMP_obs4MIPsClims/rsus/gr/v20250213/rsus_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:6ff6cf0d715bb12a4cceb5c05531682b
|
|
15
|
+
PMP_obs4MIPsClims/rsutcs/gr/v20250213/rsutcs_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:982914ff7f625f43d6a86f5ab3dbf951
|
|
16
|
+
PMP_obs4MIPsClims/rsut/gr/v20250213/rsut_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:36aee3f4ce60e468e0a5027482960e3b
|
|
17
|
+
PMP_obs4MIPsClims/rt/gr/v20250213/rt_mon_CERES-EBAF-4-2_RSS_gr_200101-200412_AC_v20250213_2.5x2.5.nc md5:8904dfea492b121e7e4252ca340e782f
|
|
18
|
+
PMP_obs4MIPsClims/ta/gr/v20250224/ta_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250224_2.5x2.5.nc md5:185f9c4382e1585bf4c91f30af371471
|
|
19
|
+
PMP_obs4MIPsClims/tas/gr/v20250224/tas_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250224_2.5x2.5.nc md5:e9b825a3fcb8d246537dbf7533d0a85d
|
|
20
|
+
PMP_obs4MIPsClims/ts/gr/v20250224/ts_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250224_2.5x2.5.nc md5:66df004506ddab80d980a37ede4b6298
|
|
21
|
+
PMP_obs4MIPsClims/ua/gr/v20250224/ua_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250224_2.5x2.5.nc md5:267cf48858a079a500b1b6fc348f6411
|
|
22
|
+
PMP_obs4MIPsClims/uas/gr/v20250224/uas_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250224_2.5x2.5.nc md5:7a31d4b16710d698805c601af4977065
|
|
23
|
+
PMP_obs4MIPsClims/va/gr/v20250225/va_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250225_2.5x2.5.nc md5:f8ad5d3d6c7e31f4c76619ea3242f1ff
|
|
24
|
+
PMP_obs4MIPsClims/vas/gr/v20250225/vas_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250225_2.5x2.5.nc md5:f009b3fd04e65f80e3336f2be8c99e05
|
|
25
|
+
PMP_obs4MIPsClims/zg/gr/v20250225/zg_mon_ERA-5_PCMDI_gr_198101-200412_AC_v20250225_2.5x2.5.nc md5:6d20c4e668cc5c31920162cab0cb5579
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from climate_ref_core.datasets import FacetFilter, SourceDatasetType
|
|
9
|
+
from climate_ref_core.diagnostics import (
|
|
10
|
+
CommandLineDiagnostic,
|
|
11
|
+
DataRequirement,
|
|
12
|
+
ExecutionDefinition,
|
|
13
|
+
ExecutionResult,
|
|
14
|
+
)
|
|
15
|
+
from climate_ref_pmp.pmp_driver import build_glob_pattern, build_pmp_command, process_json_result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AnnualCycle(CommandLineDiagnostic):
|
|
19
|
+
"""
|
|
20
|
+
Calculate the annual cycle for a dataset
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name = "Annual Cycle"
|
|
24
|
+
slug = "annual-cycle"
|
|
25
|
+
facets = ("model", "realization", "reference", "mode", "season", "method", "statistic")
|
|
26
|
+
data_requirements = (
|
|
27
|
+
# Surface temperature
|
|
28
|
+
(
|
|
29
|
+
DataRequirement(
|
|
30
|
+
source_type=SourceDatasetType.PMPClimatology,
|
|
31
|
+
filters=(FacetFilter(facets={"source_id": ("ERA-5",), "variable_id": ("ts",)}),),
|
|
32
|
+
group_by=("variable_id", "source_id"),
|
|
33
|
+
),
|
|
34
|
+
DataRequirement(
|
|
35
|
+
source_type=SourceDatasetType.CMIP6,
|
|
36
|
+
filters=(
|
|
37
|
+
FacetFilter(
|
|
38
|
+
facets={
|
|
39
|
+
"frequency": "mon",
|
|
40
|
+
"experiment_id": ("amip", "historical", "hist-GHG", "piControl"),
|
|
41
|
+
"variable_id": ("ts",),
|
|
42
|
+
}
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
group_by=("variable_id", "source_id", "experiment_id", "member_id"),
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
# Precipitation
|
|
49
|
+
(
|
|
50
|
+
DataRequirement(
|
|
51
|
+
source_type=SourceDatasetType.PMPClimatology,
|
|
52
|
+
filters=(FacetFilter(facets={"source_id": ("GPCP-Monthly-3-2",), "variable_id": ("pr",)}),),
|
|
53
|
+
group_by=("variable_id", "source_id"),
|
|
54
|
+
),
|
|
55
|
+
DataRequirement(
|
|
56
|
+
source_type=SourceDatasetType.CMIP6,
|
|
57
|
+
filters=(
|
|
58
|
+
FacetFilter(
|
|
59
|
+
facets={
|
|
60
|
+
"frequency": "mon",
|
|
61
|
+
"experiment_id": ("amip", "historical", "hist-GHG", "piControl"),
|
|
62
|
+
"variable_id": ("pr",),
|
|
63
|
+
}
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
group_by=("variable_id", "source_id", "experiment_id", "member_id"),
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def __init__(self) -> None:
|
|
72
|
+
self.parameter_file_1 = "pmp_param_annualcycle_1-clims.py"
|
|
73
|
+
self.parameter_file_2 = "pmp_param_annualcycle_2-metrics.py"
|
|
74
|
+
|
|
75
|
+
def build_cmd(self, definition: ExecutionDefinition) -> Iterable[str]:
|
|
76
|
+
"""
|
|
77
|
+
Build the command to run the diagnostic
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
definition
|
|
82
|
+
Definition of the diagnostic execution
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
Command arguments to execute in the PMP environment
|
|
87
|
+
"""
|
|
88
|
+
raise NotImplementedError("Function not required")
|
|
89
|
+
|
|
90
|
+
def build_cmds(self, definition: ExecutionDefinition) -> list[list[str]]:
|
|
91
|
+
"""
|
|
92
|
+
Build the command to run the diagnostic
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
definition
|
|
97
|
+
Definition of the diagnostic execution
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
Command arguments to execute in the PMP environment
|
|
102
|
+
"""
|
|
103
|
+
input_datasets = definition.datasets[SourceDatasetType.CMIP6]
|
|
104
|
+
source_id = input_datasets["source_id"].unique()[0]
|
|
105
|
+
experiment_id = input_datasets["experiment_id"].unique()[0]
|
|
106
|
+
member_id = input_datasets["member_id"].unique()[0]
|
|
107
|
+
variable_id = input_datasets["variable_id"].unique()[0]
|
|
108
|
+
|
|
109
|
+
logger.debug(f"input_datasets['source_id'].unique(): {input_datasets['source_id'].unique()}")
|
|
110
|
+
logger.debug(f"input_datasets['experiment_id'].unique(): {input_datasets['experiment_id'].unique()}")
|
|
111
|
+
logger.debug(f"input_datasets['member_id'].unique(): {input_datasets['member_id'].unique()}")
|
|
112
|
+
logger.debug(f"input_datasets['variable_id'].unique(): {input_datasets['variable_id'].unique()}")
|
|
113
|
+
|
|
114
|
+
model_files_raw = input_datasets.path.to_list()
|
|
115
|
+
if len(model_files_raw) == 1:
|
|
116
|
+
model_files = model_files_raw[0] # If only one file, use it directly
|
|
117
|
+
elif len(model_files_raw) > 1:
|
|
118
|
+
model_files = build_glob_pattern(model_files_raw) # If multiple files, build a glob pattern
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError("No model files found")
|
|
121
|
+
|
|
122
|
+
logger.debug("build_cmd start")
|
|
123
|
+
|
|
124
|
+
logger.debug(f"input_datasets: {input_datasets}")
|
|
125
|
+
logger.debug(f"input_datasets.keys(): {input_datasets.keys()}")
|
|
126
|
+
logger.debug(f"input_datasets['variable_id']: {input_datasets['variable_id']}")
|
|
127
|
+
|
|
128
|
+
logger.debug(f"source_id: {source_id}")
|
|
129
|
+
logger.debug(f"experiment_id: {experiment_id}")
|
|
130
|
+
logger.debug(f"member_id: {member_id}")
|
|
131
|
+
logger.debug(f"variable_id: {variable_id}")
|
|
132
|
+
|
|
133
|
+
reference_dataset = definition.datasets[SourceDatasetType.PMPClimatology]
|
|
134
|
+
reference_dataset_name = reference_dataset["source_id"].unique()[0]
|
|
135
|
+
reference_dataset_path = reference_dataset.datasets.iloc[0]["path"]
|
|
136
|
+
|
|
137
|
+
logger.debug(f"reference_dataset.datasets: {reference_dataset.datasets}")
|
|
138
|
+
logger.debug(f"reference_dataset['source_id']: {reference_dataset['source_id']}")
|
|
139
|
+
logger.debug(
|
|
140
|
+
f"reference_dataset.datasets.iloc[0]['path']: {reference_dataset.datasets.iloc[0]['path']}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger.debug(f"reference_dataset_name: {reference_dataset_name}")
|
|
144
|
+
logger.debug(f"reference_dataset_path: {reference_dataset_path}")
|
|
145
|
+
|
|
146
|
+
output_directory_path = str(definition.output_directory)
|
|
147
|
+
|
|
148
|
+
cmds = []
|
|
149
|
+
|
|
150
|
+
# ----------------------------------------------
|
|
151
|
+
# PART 1: Build the command to get climatologies
|
|
152
|
+
# ----------------------------------------------
|
|
153
|
+
# Model
|
|
154
|
+
data_name = f"{source_id}_{experiment_id}_{member_id}"
|
|
155
|
+
data_path = model_files
|
|
156
|
+
params = {
|
|
157
|
+
"driver_file": "mean_climate/pcmdi_compute_climatologies.py",
|
|
158
|
+
"parameter_file": self.parameter_file_1,
|
|
159
|
+
"vars": variable_id,
|
|
160
|
+
"infile": data_path,
|
|
161
|
+
"outfile": f"{output_directory_path}/{variable_id}_{data_name}_clims.nc",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
cmds.append(build_pmp_command(**params))
|
|
165
|
+
|
|
166
|
+
# ----------------------------------------------
|
|
167
|
+
# PART 2: Build the command to calculate diagnostics
|
|
168
|
+
# ----------------------------------------------
|
|
169
|
+
|
|
170
|
+
# Reference
|
|
171
|
+
obs_dict = {
|
|
172
|
+
variable_id: {
|
|
173
|
+
reference_dataset_name: {
|
|
174
|
+
"template": reference_dataset_path,
|
|
175
|
+
},
|
|
176
|
+
"default": reference_dataset_name,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Generate a JSON file based on the obs_dict
|
|
181
|
+
with open(f"{output_directory_path}/obs_dict.json", "w") as f:
|
|
182
|
+
json.dump(obs_dict, f)
|
|
183
|
+
|
|
184
|
+
date = datetime.datetime.now().strftime("%Y%m%d")
|
|
185
|
+
|
|
186
|
+
params = {
|
|
187
|
+
"driver_file": "mean_climate/mean_climate_driver.py",
|
|
188
|
+
"parameter_file": self.parameter_file_2,
|
|
189
|
+
"vars": variable_id,
|
|
190
|
+
"custom_observations": f"{output_directory_path}/obs_dict.json",
|
|
191
|
+
"test_data_path": output_directory_path,
|
|
192
|
+
"test_data_set": source_id,
|
|
193
|
+
"realization": member_id,
|
|
194
|
+
"filename_template": f"{variable_id}_{data_name}_clims.198101-200512.AC.v{date}.nc",
|
|
195
|
+
"metrics_output_path": output_directory_path,
|
|
196
|
+
"cmec": "",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
cmds.append(build_pmp_command(**params))
|
|
200
|
+
|
|
201
|
+
return cmds
|
|
202
|
+
|
|
203
|
+
def build_execution_result(self, definition: ExecutionDefinition) -> ExecutionResult:
|
|
204
|
+
"""
|
|
205
|
+
Build a diagnostic result from the output of the PMP driver
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
definition
|
|
210
|
+
Definition of the diagnostic execution
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
Result of the diagnostic execution
|
|
215
|
+
"""
|
|
216
|
+
input_datasets = definition.datasets[SourceDatasetType.CMIP6]
|
|
217
|
+
variable_id = input_datasets["variable_id"].unique()[0]
|
|
218
|
+
|
|
219
|
+
results_directory = definition.output_directory
|
|
220
|
+
png_directory = results_directory / variable_id
|
|
221
|
+
data_directory = results_directory / variable_id
|
|
222
|
+
|
|
223
|
+
logger.debug(f"results_directory: {results_directory}")
|
|
224
|
+
logger.debug(f"png_directory: {png_directory}")
|
|
225
|
+
logger.debug(f"data_directory: {data_directory}")
|
|
226
|
+
|
|
227
|
+
# Find the executions file
|
|
228
|
+
results_files = list(results_directory.glob("*_cmec.json"))
|
|
229
|
+
if len(results_files) != 1: # pragma: no cover
|
|
230
|
+
return ExecutionResult.build_from_failure(definition)
|
|
231
|
+
else:
|
|
232
|
+
results_file = results_files[0]
|
|
233
|
+
logger.debug(f"results_file: {results_file}")
|
|
234
|
+
|
|
235
|
+
# Rewrite executions file for compatibility
|
|
236
|
+
with open(results_file) as f:
|
|
237
|
+
results = json.load(f)
|
|
238
|
+
results_transformed = _transform_results(results)
|
|
239
|
+
|
|
240
|
+
# Get the stem (filename without extension)
|
|
241
|
+
stem = results_file.stem
|
|
242
|
+
|
|
243
|
+
# Create the new filename
|
|
244
|
+
results_file_transformed = results_file.with_name(f"{stem}_transformed.json")
|
|
245
|
+
|
|
246
|
+
with open(results_file_transformed, "w") as f:
|
|
247
|
+
# Write the transformed executions back to the file
|
|
248
|
+
json.dump(results_transformed, f, indent=4)
|
|
249
|
+
logger.debug(f"Transformed executions written to {results_file_transformed}")
|
|
250
|
+
|
|
251
|
+
# Find the other outputs
|
|
252
|
+
png_files = list(png_directory.glob("*.png"))
|
|
253
|
+
data_files = list(data_directory.glob("*.nc"))
|
|
254
|
+
|
|
255
|
+
cmec_output, cmec_metric = process_json_result(results_file_transformed, png_files, data_files)
|
|
256
|
+
|
|
257
|
+
return ExecutionResult.build_from_output_bundle(
|
|
258
|
+
definition,
|
|
259
|
+
cmec_output_bundle=cmec_output,
|
|
260
|
+
cmec_metric_bundle=cmec_metric,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def run(self, definition: ExecutionDefinition) -> ExecutionResult:
|
|
264
|
+
"""
|
|
265
|
+
Run the diagnostic on the given configuration.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
definition : ExecutionDefinition
|
|
270
|
+
The configuration to run the diagnostic on.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
:
|
|
275
|
+
The result of running the diagnostic.
|
|
276
|
+
"""
|
|
277
|
+
logger.debug("PMP annual cycle run start")
|
|
278
|
+
cmds = self.build_cmds(definition)
|
|
279
|
+
|
|
280
|
+
runs = [self.provider.run(cmd) for cmd in cmds]
|
|
281
|
+
logger.debug(f"runs: {runs}")
|
|
282
|
+
|
|
283
|
+
return self.build_execution_result(definition)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _transform_results(data: dict[str, Any]) -> dict[str, Any]:
|
|
287
|
+
"""
|
|
288
|
+
Transform the executions dictionary to match the expected structure.
|
|
289
|
+
|
|
290
|
+
Parameters
|
|
291
|
+
----------
|
|
292
|
+
data : dict
|
|
293
|
+
The original executions dictionary.
|
|
294
|
+
|
|
295
|
+
Returns
|
|
296
|
+
-------
|
|
297
|
+
dict
|
|
298
|
+
The transformed executions dictionary.
|
|
299
|
+
"""
|
|
300
|
+
# Remove the "CalendarMonths" key from the nested structure
|
|
301
|
+
if "RESULTS" in data:
|
|
302
|
+
models = list(data["RESULTS"].keys())
|
|
303
|
+
for model in models:
|
|
304
|
+
if "default" in data["RESULTS"][model]:
|
|
305
|
+
realizations = list(data["RESULTS"][model]["default"].keys())
|
|
306
|
+
if "attributes" in realizations:
|
|
307
|
+
realizations.remove("attributes")
|
|
308
|
+
for realization in realizations:
|
|
309
|
+
regions = list(data["RESULTS"][model]["default"][realization].keys())
|
|
310
|
+
for region in regions:
|
|
311
|
+
stats = list(data["RESULTS"][model]["default"][realization][region].keys())
|
|
312
|
+
for stat in stats:
|
|
313
|
+
if (
|
|
314
|
+
"CalendarMonths"
|
|
315
|
+
in data["RESULTS"][model]["default"][realization][region][stat]
|
|
316
|
+
):
|
|
317
|
+
calendar_months = data["RESULTS"][model]["default"][realization][region][
|
|
318
|
+
stat
|
|
319
|
+
].pop("CalendarMonths")
|
|
320
|
+
for i, value in enumerate(calendar_months):
|
|
321
|
+
key_name = f"CalendarMonth-{i + 1:02d}"
|
|
322
|
+
data["RESULTS"][model]["default"][realization][region][stat][key_name] = (
|
|
323
|
+
value
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Remove the "CalendarMonths" key from the nested structure in "DIMENSIONS"
|
|
327
|
+
if (
|
|
328
|
+
"DIMENSIONS" in data
|
|
329
|
+
and "season" in data["DIMENSIONS"]
|
|
330
|
+
and "CalendarMonths" in data["DIMENSIONS"]["season"]
|
|
331
|
+
):
|
|
332
|
+
calendar_months = data["DIMENSIONS"]["season"].pop("CalendarMonths")
|
|
333
|
+
for i in range(1, 13):
|
|
334
|
+
key_name = f"CalendarMonth-{i:02d}"
|
|
335
|
+
data["DIMENSIONS"]["season"][key_name] = {}
|
|
336
|
+
|
|
337
|
+
return data
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
from climate_ref_core.datasets import FacetFilter, SourceDatasetType
|
|
6
|
+
from climate_ref_core.diagnostics import (
|
|
7
|
+
CommandLineDiagnostic,
|
|
8
|
+
DataRequirement,
|
|
9
|
+
ExecutionDefinition,
|
|
10
|
+
ExecutionResult,
|
|
11
|
+
)
|
|
12
|
+
from climate_ref_pmp.pmp_driver import build_pmp_command, process_json_result
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExtratropicalModesOfVariability(CommandLineDiagnostic):
|
|
16
|
+
"""
|
|
17
|
+
Calculate the extratropical modes of variability for a given area
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
ts_modes = ("PDO", "NPGO", "AMO")
|
|
21
|
+
psl_modes = ("NAO", "NAM", "PNA", "NPO", "SAM")
|
|
22
|
+
|
|
23
|
+
facets = ("model", "realization", "reference", "mode", "season", "method", "statistic")
|
|
24
|
+
|
|
25
|
+
def __init__(self, mode_id: str):
|
|
26
|
+
self.mode_id = mode_id.upper()
|
|
27
|
+
self.name = f"Extratropical modes of variability: {mode_id}"
|
|
28
|
+
self.slug = f"extratropical-modes-of-variability-{mode_id.lower()}"
|
|
29
|
+
|
|
30
|
+
def get_data_requirements(
|
|
31
|
+
obs_source: str,
|
|
32
|
+
obs_variable: str,
|
|
33
|
+
cmip_variable: str,
|
|
34
|
+
extra_experiments: str | tuple[str, ...] | list[str] = (),
|
|
35
|
+
remove_experiments: str | tuple[str, ...] | list[str] = (),
|
|
36
|
+
) -> tuple[DataRequirement, DataRequirement]:
|
|
37
|
+
filters = [
|
|
38
|
+
FacetFilter(
|
|
39
|
+
facets={
|
|
40
|
+
"frequency": "mon",
|
|
41
|
+
"experiment_id": ("historical", "hist-GHG", "piControl", *extra_experiments),
|
|
42
|
+
"variable_id": cmip_variable,
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
DataRequirement(
|
|
49
|
+
source_type=SourceDatasetType.obs4MIPs,
|
|
50
|
+
filters=(
|
|
51
|
+
FacetFilter(facets={"source_id": (obs_source,), "variable_id": (obs_variable,)}),
|
|
52
|
+
),
|
|
53
|
+
group_by=("source_id", "variable_id"),
|
|
54
|
+
),
|
|
55
|
+
DataRequirement(
|
|
56
|
+
source_type=SourceDatasetType.CMIP6,
|
|
57
|
+
filters=tuple(filters),
|
|
58
|
+
group_by=("source_id", "experiment_id", "variant_label", "member_id"),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if self.mode_id in self.ts_modes:
|
|
63
|
+
self.parameter_file = "pmp_param_MoV-ts.py"
|
|
64
|
+
self.data_requirements = get_data_requirements("HadISST-1-1", "ts", "ts")
|
|
65
|
+
elif self.mode_id in self.psl_modes:
|
|
66
|
+
self.parameter_file = "pmp_param_MoV-psl.py"
|
|
67
|
+
self.data_requirements = get_data_requirements("20CR", "psl", "psl", extra_experiments=("amip",))
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Unknown mode_id '{self.mode_id}'. Must be one of {self.ts_modes + self.psl_modes}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def build_cmd(self, definition: ExecutionDefinition) -> Iterable[str]:
|
|
74
|
+
"""
|
|
75
|
+
Build the command to run the diagnostic
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
definition
|
|
80
|
+
Definition of the diagnostic execution
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
Command arguments to execute in the PMP environment
|
|
85
|
+
"""
|
|
86
|
+
input_datasets = definition.datasets[SourceDatasetType.CMIP6]
|
|
87
|
+
source_id = input_datasets["source_id"].unique()[0]
|
|
88
|
+
experiment_id = input_datasets["experiment_id"].unique()[0]
|
|
89
|
+
member_id = input_datasets["member_id"].unique()[0]
|
|
90
|
+
|
|
91
|
+
logger.debug(f"input_datasets: {input_datasets}")
|
|
92
|
+
logger.debug(f"source_id: {source_id}")
|
|
93
|
+
logger.debug(f"experiment_id: {experiment_id}")
|
|
94
|
+
logger.debug(f"member_id: {member_id}")
|
|
95
|
+
|
|
96
|
+
reference_dataset = definition.datasets[SourceDatasetType.obs4MIPs]
|
|
97
|
+
reference_dataset_name = reference_dataset["source_id"].unique()[0]
|
|
98
|
+
# reference_dataset_path = reference_dataset.datasets[0]["path"]
|
|
99
|
+
reference_dataset_path = reference_dataset.datasets.iloc[0]["path"]
|
|
100
|
+
|
|
101
|
+
logger.debug(f"reference_dataset: {reference_dataset}")
|
|
102
|
+
logger.debug(f"reference_dataset_name: {reference_dataset_name}")
|
|
103
|
+
logger.debug(f"reference_dataset_path: {reference_dataset_path}")
|
|
104
|
+
|
|
105
|
+
model_files = input_datasets.path.to_list()
|
|
106
|
+
|
|
107
|
+
if len(model_files) != 1:
|
|
108
|
+
# Have some logic to replace the dates in the filename with a wildcard
|
|
109
|
+
raise NotImplementedError("Only one model file is supported at this time.")
|
|
110
|
+
|
|
111
|
+
if isinstance(model_files, list):
|
|
112
|
+
modpath = " ".join([str(p) for p in model_files])
|
|
113
|
+
else:
|
|
114
|
+
modpath = model_files
|
|
115
|
+
|
|
116
|
+
if isinstance(reference_dataset_path, list):
|
|
117
|
+
reference_data_path = " ".join([str(p) for p in reference_dataset_path])
|
|
118
|
+
else:
|
|
119
|
+
reference_data_path = reference_dataset_path
|
|
120
|
+
|
|
121
|
+
# Build the command to run the PMP driver script
|
|
122
|
+
params = {
|
|
123
|
+
"driver_file": "variability_mode/variability_modes_driver.py",
|
|
124
|
+
"parameter_file": self.parameter_file,
|
|
125
|
+
"variability_mode": self.mode_id,
|
|
126
|
+
"modpath": modpath,
|
|
127
|
+
"modpath_lf": "none",
|
|
128
|
+
"exp": experiment_id,
|
|
129
|
+
"realization": member_id,
|
|
130
|
+
"modnames": source_id,
|
|
131
|
+
"reference_data_name": reference_dataset_name,
|
|
132
|
+
"reference_data_path": reference_data_path,
|
|
133
|
+
"results_dir": str(definition.output_directory),
|
|
134
|
+
"cmec": None,
|
|
135
|
+
"no_provenance": None,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Add conditional parameters
|
|
139
|
+
if self.mode_id in ["SAM"]: # pragma: no cover
|
|
140
|
+
params["osyear"] = 1950
|
|
141
|
+
params["oeyear"] = 2005
|
|
142
|
+
|
|
143
|
+
# Pass the parameters using **kwargs
|
|
144
|
+
return build_pmp_command(**params)
|
|
145
|
+
|
|
146
|
+
def build_execution_result(self, definition: ExecutionDefinition) -> ExecutionResult:
|
|
147
|
+
"""
|
|
148
|
+
Build a diagnostic result from the output of the PMP driver
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
definition
|
|
153
|
+
Definition of the diagnostic execution
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
Result of the diagnostic execution
|
|
158
|
+
"""
|
|
159
|
+
results_files = list(definition.output_directory.glob("*_cmec.json"))
|
|
160
|
+
if len(results_files) != 1: # pragma: no cover
|
|
161
|
+
logger.warning(f"A single cmec output file not found: {results_files}")
|
|
162
|
+
return ExecutionResult.build_from_failure(definition)
|
|
163
|
+
|
|
164
|
+
# Find the other outputs
|
|
165
|
+
png_files = [definition.as_relative_path(f) for f in definition.output_directory.glob("*.png")]
|
|
166
|
+
data_files = [definition.as_relative_path(f) for f in definition.output_directory.glob("*.nc")]
|
|
167
|
+
|
|
168
|
+
cmec_output, cmec_metric = process_json_result(results_files[0], png_files, data_files)
|
|
169
|
+
|
|
170
|
+
return ExecutionResult.build_from_output_bundle(
|
|
171
|
+
definition,
|
|
172
|
+
cmec_output_bundle=cmec_output,
|
|
173
|
+
cmec_metric_bundle=cmec_metric,
|
|
174
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
# =================================================
|
|
5
|
+
# Background Information
|
|
6
|
+
# -------------------------------------------------
|
|
7
|
+
mip = "cmip6"
|
|
8
|
+
exp = "historical"
|
|
9
|
+
frequency = "mo"
|
|
10
|
+
realm = "atm"
|
|
11
|
+
|
|
12
|
+
# =================================================
|
|
13
|
+
# Analysis Options
|
|
14
|
+
# -------------------------------------------------
|
|
15
|
+
variability_mode = "NAM" # Available domains: NAM, NAO, SAM, PNA, PDO
|
|
16
|
+
seasons = [
|
|
17
|
+
"DJF",
|
|
18
|
+
"MAM",
|
|
19
|
+
"JJA",
|
|
20
|
+
"SON",
|
|
21
|
+
] # Available seasons: DJF, MAM, JJA, SON, monthly, yearly
|
|
22
|
+
|
|
23
|
+
ConvEOF = True # Calculate conventioanl EOF for model
|
|
24
|
+
CBF = True # Calculate Common Basis Function (CBF) for model
|
|
25
|
+
|
|
26
|
+
# =================================================
|
|
27
|
+
# Miscellaneous
|
|
28
|
+
# -------------------------------------------------
|
|
29
|
+
update_json = False
|
|
30
|
+
debug = False
|
|
31
|
+
|
|
32
|
+
# =================================================
|
|
33
|
+
# Observation
|
|
34
|
+
# -------------------------------------------------
|
|
35
|
+
reference_data_name = "NOAA-CIRES_20CR"
|
|
36
|
+
reference_data_path = os.path.join(
|
|
37
|
+
"/p/user_pub/PCMDIobs/obs4MIPs/NOAA-ESRL-PSD/20CR/mon/psl/gn/latest",
|
|
38
|
+
"psl_mon_20CR_PCMDI_gn_187101-201212.nc",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
varOBS = "psl"
|
|
42
|
+
ObsUnitsAdjust = (True, "divide", 100.0) # Pa to hPa; or (False, 0, 0)
|
|
43
|
+
|
|
44
|
+
osyear = 1900
|
|
45
|
+
oeyear = 2005
|
|
46
|
+
eofn_obs = 1
|
|
47
|
+
|
|
48
|
+
# =================================================
|
|
49
|
+
# Models
|
|
50
|
+
# -------------------------------------------------
|
|
51
|
+
modpath = os.path.join(
|
|
52
|
+
"/p/css03/cmip5_css02/data/cmip5/output1/CSIRO-BOM/ACCESS1-0/historical/mon/atmos/Amon/r1i1p1/psl/1",
|
|
53
|
+
"psl_Amon_ACCESS1-0_historical_r1i1p1_185001-200512.nc",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
modnames = ["ACCESS1-0"]
|
|
57
|
+
|
|
58
|
+
realization = "r1i1p1f1"
|
|
59
|
+
|
|
60
|
+
varModel = "psl"
|
|
61
|
+
ModUnitsAdjust = (True, "divide", 100.0) # Pa to hPa
|
|
62
|
+
|
|
63
|
+
msyear = 1900
|
|
64
|
+
meyear = 2005
|
|
65
|
+
eofn_mod = 1
|
|
66
|
+
|
|
67
|
+
# =================================================
|
|
68
|
+
# Output
|
|
69
|
+
# -------------------------------------------------
|
|
70
|
+
case_id = f"{datetime.datetime.now():v%Y%m%d}"
|
|
71
|
+
pmprdir = "/p/user_pub/pmp/pmp_results/"
|
|
72
|
+
|
|
73
|
+
results_dir = os.path.join(
|
|
74
|
+
pmprdir,
|
|
75
|
+
"%(output_type)",
|
|
76
|
+
"variability_modes",
|
|
77
|
+
"%(mip)",
|
|
78
|
+
"%(exp)",
|
|
79
|
+
"%(case_id)",
|
|
80
|
+
"%(variability_mode)",
|
|
81
|
+
"%(reference_data_name)",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Output for obs
|
|
85
|
+
plot_obs = True # Create map graphics
|
|
86
|
+
nc_out_obs = True # Write output in NetCDF
|
|
87
|
+
|
|
88
|
+
# Output for models
|
|
89
|
+
nc_out = True
|
|
90
|
+
plot = True
|