"""
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 distutils import dir_util
import pandas as pd
import numpy as np
import CIME.test_status
import CIME.utils
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 = "20210915.v2.ne4_oQU240.F2010.{}.{}.0002-{:02d}-01-00000.nc"
INSTANCE_FILE_TEMPLATE = "{}{}_{:04d}.h0.0001-01-01-00000{}.nc"
[docs]class PGN(SystemTestsCommon):
def __init__(self, case):
"""
initialize an object interface to the PGN test
"""
super(PGN, self).__init__(case)
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/ne4_v2_init")
csmdata_lnd = os.path.join(csmdata_root, "lnd/clm2/initdata/ne4_oQU240_v2_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():
dir_util.copy_tree(
evv_out_dir,
os.path.join(htmlroot, "evv", case_name),
preserve_mode=False,
)
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/E3SM-Project/E3SM/blob/master/cime/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,
)
)
CIME.utils.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)