+
+ +

Source code for CIME.SystemTests.pgn

+"""
+Perturbation Growth New (PGN) - The CESM/ACME model's
+multi-instance capability is used to conduct an ensemble
+of simulations starting from different initial conditions.
+
+This class inherits from SystemTestsCommon.
+
+"""
+
+from __future__ import division
+
+import os
+import re
+import json
+import shutil
+import logging
+
+from collections import OrderedDict
+from shutil import copytree
+
+import pandas as pd
+import numpy as np
+
+
+import CIME.test_status
+import CIME.utils
+from CIME.status import append_testlog
+from CIME.SystemTests.system_tests_common import SystemTestsCommon
+from CIME.case.case_setup import case_setup
+from CIME.XML.machines import Machines
+
+import evv4esm  # pylint: disable=import-error
+from evv4esm.extensions import pg  # pylint: disable=import-error
+from evv4esm.__main__ import main as evv  # pylint: disable=import-error
+
+evv_lib_dir = os.path.abspath(os.path.dirname(evv4esm.__file__))
+
+logger = logging.getLogger(__name__)
+
+NUMBER_INITIAL_CONDITIONS = 6
+PERTURBATIONS = OrderedDict(
+    [
+        ("woprt", 0.0),
+        ("posprt", 1.0e-14),
+        ("negprt", -1.0e-14),
+    ]
+)
+FCLD_NC = "cam.h0.cloud.nc"
+INIT_COND_FILE_TEMPLATE = (
+    "20240305.v3p0p0.F2010.ne4pg2_oQU480.chrysalis.{}.{}.0002-{:02d}-01-00000.nc"
+)
+INSTANCE_FILE_TEMPLATE = "{}{}_{:04d}.h0.0001-01-01-00000{}.nc"
+
+
+
+[docs] +class PGN(SystemTestsCommon): + def __init__(self, case, **kwargs): + """ + initialize an object interface to the PGN test + """ + super(PGN, self).__init__(case, **kwargs) + if self._case.get_value("MODEL") == "e3sm": + self.atmmod = "eam" + self.lndmod = "elm" + self.atmmodIC = "eam" + self.lndmodIC = "elm" + else: + self.atmmod = "cam" + self.lndmod = "clm" + self.atmmodIC = "cam" + self.lndmodIC = "clm2" + +
+[docs] + def build_phase(self, sharedlib_only=False, model_only=False): + ninst = NUMBER_INITIAL_CONDITIONS * len(PERTURBATIONS) + logger.debug("PGN_INFO: number of instance: " + str(ninst)) + + default_ninst = self._case.get_value("NINST_ATM") + + if default_ninst == 1: # if multi-instance is not already set + # Only want this to happen once. It will impact the sharedlib build + # so it has to happen here. + if not model_only: + # Lay all of the components out concurrently + logger.debug( + "PGN_INFO: Updating NINST for multi-instance in env_mach_pes.xml" + ) + for comp in ["ATM", "OCN", "WAV", "GLC", "ICE", "ROF", "LND"]: + ntasks = self._case.get_value("NTASKS_{}".format(comp)) + self._case.set_value("ROOTPE_{}".format(comp), 0) + self._case.set_value("NINST_{}".format(comp), ninst) + self._case.set_value("NTASKS_{}".format(comp), ntasks * ninst) + + self._case.set_value("ROOTPE_CPL", 0) + self._case.set_value("NTASKS_CPL", ntasks * ninst) + self._case.flush() + + case_setup(self._case, test_mode=False, reset=True) + + logger.debug("PGN_INFO: Updating user_nl_* files") + + csmdata_root = self._case.get_value("DIN_LOC_ROOT") + csmdata_atm = os.path.join(csmdata_root, "atm/cam/inic/homme/ne4pg2_v3_init") + csmdata_lnd = os.path.join(csmdata_root, "lnd/clm2/initdata/ne4pg2_v3_init") + + iinst = 1 + for icond in range(1, NUMBER_INITIAL_CONDITIONS + 1): + fatm_in = os.path.join( + csmdata_atm, INIT_COND_FILE_TEMPLATE.format(self.atmmodIC, "i", icond) + ) + flnd_in = os.path.join( + csmdata_lnd, INIT_COND_FILE_TEMPLATE.format(self.lndmodIC, "r", icond) + ) + for iprt in PERTURBATIONS.values(): + with open( + "user_nl_{}_{:04d}".format(self.atmmod, iinst), "w" + ) as atmnlfile, open( + "user_nl_{}_{:04d}".format(self.lndmod, iinst), "w" + ) as lndnlfile: + + atmnlfile.write("ncdata = '{}' \n".format(fatm_in)) + lndnlfile.write("finidat = '{}' \n".format(flnd_in)) + + atmnlfile.write("avgflag_pertape = 'I' \n") + atmnlfile.write("nhtfrq = 1 \n") + atmnlfile.write("mfilt = 2 \n") + atmnlfile.write("ndens = 1 \n") + atmnlfile.write("pergro_mods = .true. \n") + atmnlfile.write("pergro_test_active = .true. \n") + + if iprt != 0.0: + atmnlfile.write("pertlim = {} \n".format(iprt)) + + iinst += 1 + + self._case.set_value("STOP_N", "1") + self._case.set_value("STOP_OPTION", "nsteps") + self.build_indv(sharedlib_only=sharedlib_only, model_only=model_only)
+ + +
+[docs] + def get_var_list(self): + """ + Get variable list for pergro specific output vars + """ + rundir = self._case.get_value("RUNDIR") + prg_fname = "pergro_ptend_names.txt" + var_file = os.path.join(rundir, prg_fname) + CIME.utils.expect( + os.path.isfile(var_file), + "File {} does not exist in: {}".format(prg_fname, rundir), + ) + + with open(var_file, "r") as fvar: + var_list = fvar.readlines() + + return list(map(str.strip, var_list))
+ + + def _compare_baseline(self): + """ + Compare baselines in the pergro test sense. That is, + compare PGE from the test simulation with the baseline + cloud + """ + with self._test_status: + self._test_status.set_status( + CIME.test_status.BASELINE_PHASE, CIME.test_status.TEST_FAIL_STATUS + ) + + logger.debug("PGN_INFO:BASELINE COMPARISON STARTS") + + run_dir = self._case.get_value("RUNDIR") + case_name = self._case.get_value("CASE") + base_dir = os.path.join( + self._case.get_value("BASELINE_ROOT"), + self._case.get_value("BASECMP_CASE"), + ) + + var_list = self.get_var_list() + + test_name = "{}".format(case_name.split(".")[-1]) + evv_config = { + test_name: { + "module": os.path.join(evv_lib_dir, "extensions", "pg.py"), + "test-case": case_name, + "test-name": "Test", + "test-dir": run_dir, + "ref-name": "Baseline", + "ref-dir": base_dir, + "variables": var_list, + "perturbations": PERTURBATIONS, + "pge-cld": FCLD_NC, + "ninit": NUMBER_INITIAL_CONDITIONS, + "init-file-template": INIT_COND_FILE_TEMPLATE, + "instance-file-template": INSTANCE_FILE_TEMPLATE, + "init-model": "cam", + "component": self.atmmod, + } + } + + json_file = os.path.join(run_dir, ".".join([case_name, "json"])) + with open(json_file, "w") as config_file: + json.dump(evv_config, config_file, indent=4) + + evv_out_dir = os.path.join(run_dir, ".".join([case_name, "evv"])) + evv(["-e", json_file, "-o", evv_out_dir]) + + with open(os.path.join(evv_out_dir, "index.json"), "r") as evv_f: + evv_status = json.load(evv_f) + + comments = "" + for evv_ele in evv_status["Page"]["elements"]: + if "Table" in evv_ele: + comments = "; ".join( + "{}: {}".format(key, val[0]) + for key, val in evv_ele["Table"]["data"].items() + ) + if evv_ele["Table"]["data"]["Test status"][0].lower() == "pass": + self._test_status.set_status( + CIME.test_status.BASELINE_PHASE, + CIME.test_status.TEST_PASS_STATUS, + ) + break + + status = self._test_status.get_status(CIME.test_status.BASELINE_PHASE) + mach_name = self._case.get_value("MACH") + mach_obj = Machines(machine=mach_name) + htmlroot = CIME.utils.get_htmlroot(mach_obj) + urlroot = CIME.utils.get_urlroot(mach_obj) + if htmlroot is not None: + with CIME.utils.SharedArea(): + copytree( + evv_out_dir, + os.path.join(htmlroot, "evv", case_name), + ) + if urlroot is None: + urlroot = "[{}_URL]".format(mach_name.capitalize()) + viewing = "{}/evv/{}/index.html".format(urlroot, case_name) + else: + viewing = ( + "{}\n" + " EVV viewing instructions can be found at: " + " https://github.com/ESMCI/CIME/blob/master/scripts/" + "climate_reproducibility/README.md#test-passfail-and-extended-output" + "".format(evv_out_dir) + ) + comments = ( + "{} {} for test '{}'.\n" + " {}\n" + " EVV results can be viewed at:\n" + " {}".format( + CIME.test_status.BASELINE_PHASE, + status, + test_name, + comments, + viewing, + ) + ) + + append_testlog(comments, self._orig_caseroot) + +
+[docs] + def run_phase(self): + logger.debug("PGN_INFO: RUN PHASE") + + self.run_indv() + + # Here were are in case directory, we need to go to the run directory + # and rename files + rundir = self._case.get_value("RUNDIR") + casename = self._case.get_value("CASE") + logger.debug("PGN_INFO: Case name is:{}".format(casename)) + + for icond in range(NUMBER_INITIAL_CONDITIONS): + for iprt, ( + prt_name, + prt_value, # pylint: disable=unused-variable + ) in enumerate(PERTURBATIONS.items()): + iinst = pg._sub2instance(icond, iprt, len(PERTURBATIONS)) + fname = os.path.join( + rundir, + INSTANCE_FILE_TEMPLATE.format( + casename + ".", self.atmmod, iinst, "" + ), + ) + renamed_fname = re.sub(r"\.nc$", "_{}.nc".format(prt_name), fname) + + logger.debug("PGN_INFO: fname to rename:{}".format(fname)) + logger.debug("PGN_INFO: Renamed file:{}".format(renamed_fname)) + try: + shutil.move(fname, renamed_fname) + except IOError: + CIME.utils.expect( + os.path.isfile(renamed_fname), + "ERROR: File {} does not exist".format(renamed_fname), + ) + logger.debug( + "PGN_INFO: Renamed file already exists:" + "{}".format(renamed_fname) + ) + + logger.debug("PGN_INFO: RUN PHASE ENDS")
+ + + def _generate_baseline(self): + super(PGN, self)._generate_baseline() + + basegen_dir = os.path.join( + self._case.get_value("BASELINE_ROOT"), self._case.get_value("BASEGEN_CASE") + ) + + rundir = self._case.get_value("RUNDIR") + casename = self._case.get_value("CASE") + + var_list = self.get_var_list() + nvar = len(var_list) + nprt = len(PERTURBATIONS) + rmse_prototype = {} + for icond in range(NUMBER_INITIAL_CONDITIONS): + prt_rmse = {} + for iprt, prt_name in enumerate(PERTURBATIONS): + if prt_name == "woprt": + continue + iinst_ctrl = pg._sub2instance(icond, 0, nprt) + ifile_ctrl = os.path.join( + rundir, + INSTANCE_FILE_TEMPLATE.format( + casename + ".", self.atmmod, iinst_ctrl, "_woprt" + ), + ) + + iinst_test = pg._sub2instance(icond, iprt, nprt) + ifile_test = os.path.join( + rundir, + INSTANCE_FILE_TEMPLATE.format( + casename + ".", self.atmmod, iinst_test, "_" + prt_name + ), + ) + + prt_rmse[prt_name] = pg.variables_rmse( + ifile_test, ifile_ctrl, var_list, "t_" + ) + rmse_prototype[icond] = pd.concat(prt_rmse) + rmse = pd.concat(rmse_prototype) + cld_rmse = np.reshape( + rmse.RMSE.values, (NUMBER_INITIAL_CONDITIONS, nprt - 1, nvar) + ) + + pg.rmse_writer( + os.path.join(rundir, FCLD_NC), + cld_rmse, + list(PERTURBATIONS.keys()), + var_list, + INIT_COND_FILE_TEMPLATE, + "cam", + ) + + logger.debug("PGN_INFO:copy:{} to {}".format(FCLD_NC, basegen_dir)) + shutil.copy(os.path.join(rundir, FCLD_NC), basegen_dir)
+ +
+ +
+