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