clm5/python/ctsm/lilac_make_runtime_inputs.py
2024-05-09 15:14:01 +08:00

326 lines
10 KiB
Python

"""Functions implementing LILAC's make_runtime_inputs command"""
import os
import subprocess
import argparse
import logging
from configparser import ConfigParser
from CIME.buildnml import create_namelist_infile # pylint: disable=import-error
from ctsm.ctsm_logging import (
setup_logging_pre_config,
add_logging_args,
process_logging_args,
)
from ctsm.path_utils import path_to_ctsm_root
from ctsm.utils import abort
from ctsm.config_utils import get_config_value
logger = logging.getLogger(__name__)
# ========================================================================
# Define some constants
# ========================================================================
_CONFIG_CACHE_TEMPLATE = """
<?xml version="1.0"?>
<config_definition>
<commandline></commandline>
<entry id="phys" value="{clm_phys}" list="" valid_values="clm4_5,clm5_0,clm5_1,clm6_0">Specifies ctsm physics</entry>
</config_definition>
"""
# Note the following is needed in env_lilac.xml otherwise the following error appears in
# the call to build_namelist
# err=ERROR : CLM build-namelist::CLMBuildNamelist::logical_to_fortran() :
# Unexpected value in logical_to_fortran:
_ENV_LILAC_TEMPLATE = """
<?xml version="1.0"?>
<file id="env_lilac.xml" version="2.0">
<group id="run_glc">
<entry id="GLC_TWO_WAY_COUPLING" value="FALSE">
<type>logical</type>
<valid_values>TRUE,FALSE</valid_values>
</entry>
</group>
</file>
"""
# ========================================================================
# Fake case class that can be used to satisfy the interface of CIME functions that need a
# case object
# ========================================================================
class CaseFake:
"""Fake case class to satisfy interface of CIME functions that need a case object"""
# pylint: disable=too-few-public-methods
def __init__(self):
pass
@staticmethod
def get_resolved_value(value):
"""Make sure get_resolved_value doesn't get called
(since we don't have a real case object to resolve values with)
"""
abort("Cannot resolve value with a '$' variable: {}".format(value))
###############################################################################
def parse_command_line():
###############################################################################
"""Parse the command line, return object holding arguments"""
description = """
Script to create runtime inputs when running CTSM via LILAC
"""
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter, description=description
)
parser.add_argument(
"--rundir",
type=str,
default=os.getcwd(),
help="Full path of the run directory (containing ctsm.cfg & user_nl_ctsm)",
)
add_logging_args(parser)
arguments = parser.parse_args()
# Perform some error checking on arguments
if not os.path.isdir(arguments.rundir):
abort("rundir {} does not exist".format(arguments.rundir))
return arguments
###############################################################################
def determine_bldnml_opts(bgc_mode, crop, vichydro):
###############################################################################
"""Return a string giving bldnml options, given some other inputs"""
bldnml_opts = ""
bldnml_opts += " -bgc {}".format(bgc_mode)
if bgc_mode == "fates":
# BUG(wjs, 2020-06-12, ESCOMP/CTSM#115) For now, FATES is incompatible with MEGAN
bldnml_opts += " -no-megan"
if crop == "on":
if bgc_mode not in ["bgc", "cn"]:
abort("Error: setting crop to 'on' is only compatible with bgc_mode of 'bgc' or 'cn'")
bldnml_opts += " -crop"
if vichydro == "on":
if bgc_mode != "sp":
abort("Error: setting vichydro to 'on' is only compatible with bgc_mode of 'sp'")
bldnml_opts += " -vichydro"
return bldnml_opts
###############################################################################
def buildnml(cime_path, rundir):
###############################################################################
"""Build the ctsm namelist"""
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
ctsm_cfg_path = os.path.join(rundir, "ctsm.cfg")
# read the config file
config = ConfigParser()
config.read(ctsm_cfg_path)
lnd_domain_file = get_config_value(config, "buildnml_input", "lnd_domain_file", ctsm_cfg_path)
fsurdat = get_config_value(
config, "buildnml_input", "fsurdat", ctsm_cfg_path, can_be_unset=True
)
finidat = get_config_value(
config, "buildnml_input", "finidat", ctsm_cfg_path, can_be_unset=True
)
ctsm_phys = get_config_value(
config,
"buildnml_input",
"ctsm_phys",
ctsm_cfg_path,
allowed_values=["clm4_5", "clm5_0", "clm5_1", "clm6_0"],
)
configuration = get_config_value(
config,
"buildnml_input",
"configuration",
ctsm_cfg_path,
allowed_values=["nwp", "clm"],
)
structure = get_config_value(
config,
"buildnml_input",
"structure",
ctsm_cfg_path,
allowed_values=["fast", "standard"],
)
bgc_mode = get_config_value(
config,
"buildnml_input",
"bgc_mode",
ctsm_cfg_path,
allowed_values=["sp", "bgc", "cn", "fates"],
)
crop = get_config_value(
config, "buildnml_input", "crop", ctsm_cfg_path, allowed_values=["off", "on"]
)
vichydro = get_config_value(
config,
"buildnml_input",
"vichydro",
ctsm_cfg_path,
allowed_values=["off", "on"],
)
bldnml_opts = determine_bldnml_opts(bgc_mode=bgc_mode, crop=crop, vichydro=vichydro)
co2_ppmv = get_config_value(config, "buildnml_input", "co2_ppmv", ctsm_cfg_path)
use_case = get_config_value(config, "buildnml_input", "use_case", ctsm_cfg_path)
lnd_tuning_mode = get_config_value(config, "buildnml_input", "lnd_tuning_mode", ctsm_cfg_path)
spinup = get_config_value(
config, "buildnml_input", "spinup", ctsm_cfg_path, allowed_values=["off", "on"]
)
inputdata_path = get_config_value(config, "buildnml_input", "inputdata_path", ctsm_cfg_path)
# Parse the user_nl_ctsm file
infile = os.path.join(rundir, ".namelist")
create_namelist_infile(
case=CaseFake(),
user_nl_file=os.path.join(rundir, "user_nl_ctsm"),
namelist_infile=infile,
)
# create config_cache.xml file
# Note that build-namelist utilizes the contents of the config_cache.xml file in
# the namelist_defaults.xml file to obtain namelist variables
config_cache = os.path.join(rundir, "config_cache.xml")
config_cache_text = _CONFIG_CACHE_TEMPLATE.format(clm_phys=ctsm_phys)
with open(config_cache, "w") as tempfile:
tempfile.write(config_cache_text)
# create temporary env_lilac.xml
env_lilac = os.path.join(rundir, "env_lilac.xml")
env_lilac_text = _ENV_LILAC_TEMPLATE.format()
with open(env_lilac, "w") as tempfile:
tempfile.write(env_lilac_text)
# remove any existing clm.input_data_list file
inputdatalist_path = os.path.join(rundir, "ctsm.input_data_list")
if os.path.exists(inputdatalist_path):
os.remove(inputdatalist_path)
# determine if fsurdat and/or finidat should appear in the -namelist option
extra_namelist_opts = ""
if fsurdat is not None:
# NOTE(wjs, 2020-06-30) With the current logic, fsurdat should never be UNSET
# (ie None here) but it's possible that this will change in the future.
extra_namelist_opts = extra_namelist_opts + " fsurdat = '{}' ".format(fsurdat)
if finidat is not None:
extra_namelist_opts = extra_namelist_opts + " finidat = '{}' ".format(finidat)
# call build-namelist
cmd = os.path.abspath(os.path.join(path_to_ctsm_root(), "bld", "build-namelist"))
command = [
cmd,
"-driver",
"nuopc",
"-cimeroot",
cime_path,
"-infile",
infile,
"-csmdata",
inputdata_path,
"-inputdata",
inputdatalist_path,
# Hard-code start_ymd of year-2000. This is used to set the run type (for
# which a setting of 2000 gives 'startup', which is what we want) and pick
# the initial conditions file (which is pretty much irrelevant when running
# with lilac).
"-namelist",
"&clm_inparm start_ymd=20000101 {} /".format(extra_namelist_opts),
"-use_case",
use_case,
# For now, we assume ignore_ic_year, not ignore_ic_date
"-ignore_ic_year",
# -clm_start_type seems unimportant (see discussion in
# https://github.com/ESCOMP/CTSM/issues/876)
"-clm_start_type",
"default",
"-configuration",
configuration,
"-structure",
structure,
"-lilac",
"-lnd_frac",
lnd_domain_file,
"-glc_nec",
str(10),
"-co2_ppmv",
co2_ppmv,
"-co2_type",
"constant",
"-clm_accelerated_spinup",
spinup,
"-lnd_tuning_mode",
lnd_tuning_mode,
# Eventually make -no-megan dynamic (see
# https://github.com/ESCOMP/CTSM/issues/926)
"-no-megan",
"-config",
os.path.join(rundir, "config_cache.xml"),
"-envxml_dir",
rundir,
]
# NOTE(wjs, 2020-06-16) Note that we do NOT use the -mask argument; it's possible that
# we should be using it in some circumstances (I haven't looked into how it's used).
command.extend(["-res", "lilac", "-clm_usr_name", "lilac"])
command.extend(bldnml_opts.split())
subprocess.check_call(command, universal_newlines=True)
# remove temporary files in rundir
os.remove(os.path.join(rundir, "config_cache.xml"))
os.remove(os.path.join(rundir, "env_lilac.xml"))
os.remove(os.path.join(rundir, "drv_flds_in"))
os.remove(infile)
###############################################################################
def main(cime_path):
"""Main function
Args:
cime_path (str): path to the cime that we're using (this is passed in explicitly
rather than relying on calling path_to_cime so that we can be absolutely sure that
the scripts called here are coming from the same cime as the cime library we're
using).
"""
setup_logging_pre_config()
args = parse_command_line()
process_logging_args(args)
buildnml(cime_path=cime_path, rundir=args.rundir)
###############################################################################