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)
+
+