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,94 @@
|
|
|
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 = "PDO" # Available domains: NAM, NAO, SAM, PNA, PDO
|
|
16
|
+
seasons = ["monthly"] # Available seasons: DJF, MAM, JJA, SON, monthly, yearly
|
|
17
|
+
|
|
18
|
+
landmask = True # Maskout land region thus consider only ocean grid (default=False)
|
|
19
|
+
|
|
20
|
+
ConvEOF = True # Calculate conventioanl EOF for model
|
|
21
|
+
CBF = True # Calculate Common Basis Function (CBF) for model
|
|
22
|
+
|
|
23
|
+
# =================================================
|
|
24
|
+
# Miscellaneous
|
|
25
|
+
# -------------------------------------------------
|
|
26
|
+
update_json = False # False
|
|
27
|
+
debug = False # False
|
|
28
|
+
|
|
29
|
+
# =================================================
|
|
30
|
+
# Observation
|
|
31
|
+
# -------------------------------------------------
|
|
32
|
+
reference_data_name = "HadISSTv1.1"
|
|
33
|
+
reference_data_path = (
|
|
34
|
+
"/p/user_pub/PCMDIobs/obs4MIPs/MOHC/HadISST-1-1/"
|
|
35
|
+
+ "mon/ts/gn/v20210727/ts_mon_HadISST-1-1_PCMDI_gn_187001-201907.nc"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# varOBS = "sst"
|
|
39
|
+
# ObsUnitsAdjust = (False, 0, 0) # degC
|
|
40
|
+
varOBS = "ts"
|
|
41
|
+
ModUnitsAdjust = (True, "subtract", 273.15) # degK to degC
|
|
42
|
+
|
|
43
|
+
osyear = 1900
|
|
44
|
+
oeyear = 2005
|
|
45
|
+
eofn_obs = 1
|
|
46
|
+
|
|
47
|
+
# =================================================
|
|
48
|
+
# Models
|
|
49
|
+
# -------------------------------------------------
|
|
50
|
+
modpath = os.path.join(
|
|
51
|
+
"/p/css03/cmip5_css02/data/cmip5/output1/CSIRO-BOM/ACCESS1-0/historical/mon/atmos/Amon/r1i1p1/ts/1/",
|
|
52
|
+
"ts_Amon_ACCESS1-0_historical_r1i1p1_185001-200512.nc",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
modpath_lf = os.path.join(
|
|
56
|
+
"/p/css03/cmip5_css02/data/cmip5/output1/CSIRO-BOM/ACCESS1-0/amip/fx/atmos/fx/r0i0p0/sftlf/1/",
|
|
57
|
+
"sftlf_fx_ACCESS1-0_amip_r0i0p0.nc",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
modnames = ["ACCESS1-0"]
|
|
61
|
+
|
|
62
|
+
realization = "r1i1p1f1"
|
|
63
|
+
|
|
64
|
+
varModel = "ts"
|
|
65
|
+
ModUnitsAdjust = (True, "subtract", 273.15) # degK to degC
|
|
66
|
+
|
|
67
|
+
msyear = 1900
|
|
68
|
+
meyear = 2005
|
|
69
|
+
eofn_mod = 1
|
|
70
|
+
|
|
71
|
+
# =================================================
|
|
72
|
+
# Output
|
|
73
|
+
# -------------------------------------------------
|
|
74
|
+
case_id = f"{datetime.datetime.now():v%Y%m%d}"
|
|
75
|
+
pmprdir = "/p/user_pub/pmp/pmp_results/"
|
|
76
|
+
|
|
77
|
+
results_dir = os.path.join(
|
|
78
|
+
pmprdir,
|
|
79
|
+
"%(output_type)",
|
|
80
|
+
"variability_modes",
|
|
81
|
+
"%(mip)",
|
|
82
|
+
"%(exp)",
|
|
83
|
+
"%(case_id)",
|
|
84
|
+
"%(variability_mode)",
|
|
85
|
+
"%(reference_data_name)",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Output for obs
|
|
89
|
+
plot_obs = True # Create map graphics
|
|
90
|
+
nc_out_obs = True # Write output in NetCDF
|
|
91
|
+
|
|
92
|
+
# Output for models
|
|
93
|
+
nc_out = True
|
|
94
|
+
plot = True
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#
|
|
2
|
+
# OPTIONS ARE SET BY USER IN THIS FILE AS INDICATED BELOW BY:
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
# VARIABLES TO USE
|
|
7
|
+
# vars = ["rlut"]
|
|
8
|
+
|
|
9
|
+
# START AND END DATES FOR CLIMATOLOGY
|
|
10
|
+
start = "1981-01"
|
|
11
|
+
end = "2005-12"
|
|
12
|
+
|
|
13
|
+
# INPUT DATASET - CAN BE MODEL OR OBSERVATIONS
|
|
14
|
+
infile = (
|
|
15
|
+
"obs4MIPs_PCMDI_monthly/NASA-LaRC/CERES-EBAF-4-1"
|
|
16
|
+
"/mon/rlut/gn/v20210727"
|
|
17
|
+
"/rlut_mon_CERES-EBAF-4-1_PCMDI_gn_200301-201812.nc"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# DIRECTORY WHERE TO PUT RESULTS
|
|
21
|
+
outfile = "climo/rlut_mon_CERES-EBAF-4-1_BE_gn.nc"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# OPTIONS ARE SET BY USER IN THIS FILE AS INDICATED BELOW BY:
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# RUN IDENTIFICATION
|
|
9
|
+
# DEFINES A SUBDIRECTORY TO METRICS OUTPUT RESULTS SO MULTIPLE CASES CAN
|
|
10
|
+
# BE COMPARED
|
|
11
|
+
case_id = "basicTest"
|
|
12
|
+
|
|
13
|
+
# LIST OF MODEL VERSIONS TO BE TESTED - WHICH ARE EXPECTED TO BE PART OF
|
|
14
|
+
# CLIMATOLOGY FILENAME
|
|
15
|
+
# test_data_set = ["ACCESS1-0", "CanCM4"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# VARIABLES TO USE
|
|
19
|
+
# vars = ["rlut"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Observations to use at the moment "default" or "alternate"
|
|
23
|
+
reference_data_set = ["all"]
|
|
24
|
+
# ext = '.nc'
|
|
25
|
+
|
|
26
|
+
# INTERPOLATION OPTIONS
|
|
27
|
+
target_grid = "2.5x2.5" # OPTIONS: '2.5x2.5' or an actual cdms2 grid object
|
|
28
|
+
regrid_tool = "regrid2" # 'regrid2' # OPTIONS: 'regrid2','esmf'
|
|
29
|
+
# -- OPTIONS: 'linear','conservative', only if tool is esmf
|
|
30
|
+
regrid_method = "linear"
|
|
31
|
+
regrid_tool_ocn = "esmf" # OPTIONS: "regrid2","esmf"
|
|
32
|
+
# -- OPTIONS: 'linear','conservative', only if tool is esmf
|
|
33
|
+
regrid_method_ocn = "linear"
|
|
34
|
+
|
|
35
|
+
# Templates for climatology files
|
|
36
|
+
# %(param) will subsitute param with values in this file
|
|
37
|
+
filename_template = "cmip5.historical.%(model_version).r1i1p1.mon.%(variable).198101-200512.AC.v20200426.nc"
|
|
38
|
+
|
|
39
|
+
# filename template for landsea masks ('sftlf')
|
|
40
|
+
sftlf_filename_template = "sftlf_%(model_version).nc"
|
|
41
|
+
generate_sftlf = True # if land surface type mask cannot be found, generate one
|
|
42
|
+
|
|
43
|
+
# Region
|
|
44
|
+
regions = {"rlut": ["Global"]}
|
|
45
|
+
|
|
46
|
+
# ROOT PATH FOR MODELS CLIMATOLOGIES
|
|
47
|
+
test_data_path = "demo_data_tmp/CMIP5_demo_clims/"
|
|
48
|
+
# ROOT PATH FOR OBSERVATIONS
|
|
49
|
+
# Note that atm/mo/%(variable)/ac will be added to this
|
|
50
|
+
reference_data_path = ""
|
|
51
|
+
custom_observations = "obs_dict.json"
|
|
52
|
+
|
|
53
|
+
# DIRECTORY WHERE TO PUT RESULTS
|
|
54
|
+
metrics_output_path = os.path.join("demo_output_tmp", "%(case_id)")
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import importlib.resources
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from rich.pretty import pretty_repr
|
|
10
|
+
|
|
11
|
+
from climate_ref_core.pycmec.metric import CMECMetric
|
|
12
|
+
from climate_ref_core.pycmec.output import CMECOutput
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _remove_nested_key(data: dict[str, Any], key: str) -> dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Remove a nested key from a dictionary
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
data
|
|
22
|
+
Dictionary to remove the key from
|
|
23
|
+
key
|
|
24
|
+
Key to remove
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
The dictionary with the key removed
|
|
29
|
+
"""
|
|
30
|
+
if key in data:
|
|
31
|
+
data.pop(key)
|
|
32
|
+
for k, v in data.items():
|
|
33
|
+
if isinstance(v, dict):
|
|
34
|
+
data[k] = _remove_nested_key(v, key)
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def process_json_result(
|
|
39
|
+
json_filename: pathlib.Path, png_files: list[pathlib.Path], data_files: list[pathlib.Path]
|
|
40
|
+
) -> tuple[CMECOutput, CMECMetric]:
|
|
41
|
+
"""
|
|
42
|
+
Process a PMP JSON result into the appropriate CMEC bundles
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
json_filename
|
|
47
|
+
Filename of the JSON file that is written out by PMP
|
|
48
|
+
png_files
|
|
49
|
+
List of PNG files to be included in the output
|
|
50
|
+
data_files
|
|
51
|
+
List of data files to be included in the output
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
tuple of CMEC output and diagnostic bundles
|
|
56
|
+
"""
|
|
57
|
+
with open(json_filename) as fh:
|
|
58
|
+
json_result = json.load(fh)
|
|
59
|
+
|
|
60
|
+
cmec_output = CMECOutput.create_template()
|
|
61
|
+
cmec_output["provenance"] = {**cmec_output["provenance"], **json_result["provenance"]}
|
|
62
|
+
|
|
63
|
+
# Add the plots and data files
|
|
64
|
+
for fname in png_files:
|
|
65
|
+
cmec_output["plots"][fname.name] = {
|
|
66
|
+
"filename": str(fname),
|
|
67
|
+
"long_name": "Plot",
|
|
68
|
+
"description": "Plot produced by the diagnostic",
|
|
69
|
+
}
|
|
70
|
+
for fname in data_files:
|
|
71
|
+
cmec_output["data"][fname.name] = {
|
|
72
|
+
"filename": str(fname),
|
|
73
|
+
"long_name": "Output data",
|
|
74
|
+
"description": "Data produced by the diagnostic",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
cmec_metric = CMECMetric.create_template()
|
|
78
|
+
cmec_metric["DIMENSIONS"] = {}
|
|
79
|
+
dimensions = json_result["DIMENSIONS"]
|
|
80
|
+
|
|
81
|
+
if "dimensions" in dimensions: # pragma: no branch
|
|
82
|
+
# Merge the contents of inner "dimensions" into the parent "DIMENSIONS"
|
|
83
|
+
dimensions.update(dimensions["dimensions"])
|
|
84
|
+
del dimensions["dimensions"]
|
|
85
|
+
|
|
86
|
+
if "statistic" in dimensions["json_structure"]: # pragma: no branch
|
|
87
|
+
dimensions["json_structure"].remove("statistic")
|
|
88
|
+
dimensions.pop("statistic")
|
|
89
|
+
|
|
90
|
+
# Remove the "attributes" key from the RESULTS
|
|
91
|
+
# This isn't standard CMEC output, but it is what PMP produces
|
|
92
|
+
results = json_result["RESULTS"]
|
|
93
|
+
|
|
94
|
+
cmec_metric["RESULTS"] = results
|
|
95
|
+
cmec_metric["DIMENSIONS"] = dimensions
|
|
96
|
+
|
|
97
|
+
if "provenance" in json_result: # pragma: no branch
|
|
98
|
+
cmec_metric["provenance"] = json_result["provenance"]
|
|
99
|
+
|
|
100
|
+
logger.info(f"cmec_output: {pretty_repr(cmec_output)}")
|
|
101
|
+
logger.info(f"cmec_metric: {pretty_repr(cmec_metric)}")
|
|
102
|
+
|
|
103
|
+
return CMECOutput(**cmec_output), CMECMetric(**cmec_metric)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _get_resource(package: str, resource_name: str | pathlib.Path, use_resources: bool) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Get the path to a resource within the pcmdi_metric package without importing.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
package: str
|
|
113
|
+
Python package name if use_resources is True, otherwise the distribution name
|
|
114
|
+
(the pypi package name).
|
|
115
|
+
resource_name : str
|
|
116
|
+
The resource path relative to the package.
|
|
117
|
+
use_resources : bool
|
|
118
|
+
If True, use the importlib.resources API, otherwise use importlib.metadata.
|
|
119
|
+
|
|
120
|
+
importlib.resources is the preferred way to access resources because it handles
|
|
121
|
+
packages which have been editably installed,
|
|
122
|
+
but it implictly imports the package.
|
|
123
|
+
|
|
124
|
+
Whereas `importlib.metadata` uses the package metadata to resolve the location of a resource.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
The full path to the target resource.
|
|
129
|
+
"""
|
|
130
|
+
if use_resources:
|
|
131
|
+
resource_path = str(importlib.resources.files(package) / str(resource_name))
|
|
132
|
+
else:
|
|
133
|
+
distribution = importlib.metadata.distribution(package)
|
|
134
|
+
resource_path = str(distribution.locate_file(pathlib.Path(package) / resource_name))
|
|
135
|
+
if not pathlib.Path(resource_path).exists():
|
|
136
|
+
raise FileNotFoundError(f"Resource {resource_name} not found in {package} package.")
|
|
137
|
+
return str(resource_path)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_pmp_command(
|
|
141
|
+
driver_file: str,
|
|
142
|
+
parameter_file: str,
|
|
143
|
+
**kwargs: dict[str, str | int | float | list[str]],
|
|
144
|
+
) -> list[str]:
|
|
145
|
+
"""
|
|
146
|
+
Run a PMP driver script via a conda environment
|
|
147
|
+
|
|
148
|
+
This function runs a PMP driver script using a specific conda environment.
|
|
149
|
+
The driver script is responsible for running the PMP diagnostics and producing output.
|
|
150
|
+
The output consists of a JSON file that contains the executions of the PMP diagnostics,
|
|
151
|
+
and a set of PNG and data files that are produced by the diagnostics.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
driver_file
|
|
156
|
+
Filename of the PMP driver script to run
|
|
157
|
+
parameter_file
|
|
158
|
+
Filename of the parameter file to use
|
|
159
|
+
kwargs
|
|
160
|
+
Additional arguments to pass to the driver script
|
|
161
|
+
"""
|
|
162
|
+
# Note this uses the driver script from the REF env *not* the PMP conda env
|
|
163
|
+
_driver_script = _get_resource("pcmdi_metrics", driver_file, use_resources=False)
|
|
164
|
+
_parameter_file = _get_resource("climate_ref_pmp.params", parameter_file, use_resources=True)
|
|
165
|
+
|
|
166
|
+
# Run the driver script inside the PMP conda environment
|
|
167
|
+
cmd = [
|
|
168
|
+
"python",
|
|
169
|
+
_driver_script,
|
|
170
|
+
"-p",
|
|
171
|
+
_parameter_file,
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
# Loop through additional arguments if they exist
|
|
175
|
+
if kwargs: # pragma: no cover
|
|
176
|
+
for key, value in kwargs.items():
|
|
177
|
+
if value:
|
|
178
|
+
cmd.extend([f"--{key}", str(value)])
|
|
179
|
+
else:
|
|
180
|
+
cmd.extend([f"--{key}"])
|
|
181
|
+
|
|
182
|
+
logger.info("-- PMP command to run --")
|
|
183
|
+
logger.info("[PMP] Command to run:", " ".join(map(str, cmd)))
|
|
184
|
+
logger.info("[PMP] Command generation for the driver completed.")
|
|
185
|
+
|
|
186
|
+
return cmd
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def build_glob_pattern(paths: list[str]) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Generate a glob pattern that matches files based on common path, prefix, and suffix.
|
|
192
|
+
|
|
193
|
+
Generate a glob pattern that matches all files in the given list of paths,
|
|
194
|
+
based on their common directory, filename prefix, and suffix.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
paths : list of str
|
|
199
|
+
A list of full file paths. The paths should point to actual files,
|
|
200
|
+
and should have enough similarity in their structure and naming
|
|
201
|
+
to extract common patterns.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
str
|
|
206
|
+
A glob pattern string that can be used with `glob.glob(pattern, recursive=True)`
|
|
207
|
+
to match all the provided files and others with the same structural pattern.
|
|
208
|
+
|
|
209
|
+
Examples
|
|
210
|
+
--------
|
|
211
|
+
>>> paths = [
|
|
212
|
+
... "/home/user/data/folder1/file1.txt",
|
|
213
|
+
... "/home/user/data/folder1/file2.txt",
|
|
214
|
+
... "/home/user/data/folder2/file3.txt",
|
|
215
|
+
... ]
|
|
216
|
+
>>> pattern = build_glob_pattern(paths)
|
|
217
|
+
>>> print(pattern)
|
|
218
|
+
/home/user/data/**/file*.txt
|
|
219
|
+
"""
|
|
220
|
+
if not paths:
|
|
221
|
+
raise ValueError("The path list is empty.")
|
|
222
|
+
|
|
223
|
+
# Find the common directory path
|
|
224
|
+
common_path = os.path.commonpath(paths)
|
|
225
|
+
|
|
226
|
+
# Extract filenames and parent directories
|
|
227
|
+
filenames = [os.path.basename(path) for path in paths]
|
|
228
|
+
dirnames = [os.path.dirname(path) for path in paths]
|
|
229
|
+
same_dir = all(d == dirnames[0] for d in dirnames)
|
|
230
|
+
|
|
231
|
+
# Helper to find common prefix
|
|
232
|
+
def common_prefix(strings: list[str]) -> str:
|
|
233
|
+
if not strings:
|
|
234
|
+
return ""
|
|
235
|
+
prefix = strings[0]
|
|
236
|
+
for s in strings[1:]:
|
|
237
|
+
while not s.startswith(prefix):
|
|
238
|
+
prefix = prefix[:-1]
|
|
239
|
+
if not prefix:
|
|
240
|
+
break
|
|
241
|
+
return prefix
|
|
242
|
+
|
|
243
|
+
# Helper to find common suffix
|
|
244
|
+
def common_suffix(strings: list[str]) -> str:
|
|
245
|
+
reversed_strings = [s[::-1] for s in strings]
|
|
246
|
+
reversed_suffix = common_prefix(reversed_strings)
|
|
247
|
+
return reversed_suffix[::-1]
|
|
248
|
+
|
|
249
|
+
prefix = common_prefix(filenames)
|
|
250
|
+
suffix = common_suffix(filenames)
|
|
251
|
+
|
|
252
|
+
# Use simpler pattern if all files are in the same directory
|
|
253
|
+
if same_dir:
|
|
254
|
+
pattern = os.path.join(dirnames[0], f"{prefix}*{suffix}")
|
|
255
|
+
else:
|
|
256
|
+
pattern = os.path.join(common_path, "**", f"{prefix}*{suffix}")
|
|
257
|
+
|
|
258
|
+
return pattern
|
climate_ref_pmp/py.typed
ADDED
|
File without changes
|