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.
@@ -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
File without changes