diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 58a1869f9d..4798c37853 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -31,3 +31,4 @@ d866510188d26d51bcd6d37239283db690af7e82 183fc26a6691bbdf87f515dc47924a64be3ced9b 6fccf682eaf718615407d9bacdd3903b8786a03d 2500534eb0a83cc3aff94b30fb62e915054030bf +78d05967c2b027dc9776a884716597db6ef7f57c diff --git a/.gitignore b/.gitignore index ca701132a7..ec2a3b17f6 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,7 @@ unit_test_build /tools/site_and_regional/????.ad/ /tools/site_and_regional/????.postad/ /tools/site_and_regional/????.transient/ +/tools/site_and_regional/archive/ # build output *.o diff --git a/cime_config/buildnml b/cime_config/buildnml index 84e1581406..0521830616 100755 --- a/cime_config/buildnml +++ b/cime_config/buildnml @@ -144,8 +144,8 @@ def buildnml(case, caseroot, compname): or clm_usrdat_name is "NEON.PRISM" ): logger.warning( - "WARNING: Do you have approriprate initial conditions for this simulation?" - + " Check that the finidat file used in the lnd_in namelist is apprporiately spunup for your case" + "WARNING: Do you have appropriate initial conditions for this simulation?" + + " Check that the finidat file used in the lnd_in namelist is appropriately spunup for your case" ) if comp_atm != "datm": diff --git a/cime_config/config_pes.xml b/cime_config/config_pes.xml index 34fd593425..d39ba06e49 100644 --- a/cime_config/config_pes.xml +++ b/cime_config/config_pes.xml @@ -119,13 +119,13 @@ none -1 - -1 - -1 - -1 - -1 - -1 - -1 - -1 + -4 + -4 + -4 + -4 + -4 + -4 + -4 1 @@ -631,6 +631,43 @@ + + + + none + + -1 + -21 + -21 + -21 + -21 + -21 + -21 + -21 + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + 0 + -1 + -1 + -1 + -1 + -1 + -1 + -1 + + + + @@ -742,19 +779,56 @@ + + + + none + + -1 + -8 + -8 + -8 + -8 + -8 + -8 + -8 + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + 0 + -1 + -1 + -1 + -1 + -1 + -1 + -1 + + + + none -1 - -5 - -5 - -5 - -5 - -5 - -5 - -5 + -12 + -12 + -12 + -12 + -12 + -12 + -12 1 @@ -1560,6 +1634,158 @@ + + + + none + + -1 + -4 + -4 + -4 + -4 + -4 + -4 + -4 + -4 + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + 0 + -1 + -1 + -1 + -1 + -1 + -1 + -1 + + + + + + + + none + + -1 + -8 + -8 + -8 + -8 + -8 + -8 + -8 + -8 + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + 0 + -1 + -1 + -1 + -1 + -1 + -1 + -1 + + + + + + + + none + + -1 + -12 + -12 + -12 + -12 + -12 + -12 + -12 + -12 + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + 0 + -1 + -1 + -1 + -1 + -1 + -1 + -1 + + + + + + + + none + + -1 + -36 + -36 + -36 + -36 + -36 + -36 + -36 + -36 + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + 0 + -1 + -1 + -1 + -1 + -1 + -1 + -1 + + + + diff --git a/doc/ChangeLog b/doc/ChangeLog index fbaa87f2dd..f822c5a72e 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,4 +1,65 @@ =============================================================== +Tag name: ctsm5.1.dev175 +Originator(s): slevis (Samuel Levis,UCAR/TSS,303-665-1310) +Date: Thu 21 Mar 2024 05:49:04 PM MDT +One-line Summary: merge-b4bdev-20240321 + +Purpose and description of changes +---------------------------------- + +Merge master 20240313 #2421 (Update of externals to what's expected in cesm2_3_beta17) +Fix for cray compiler format issue #2391 +Remove LILAC references to mct #2374 +Refactoring of neon_site into tower_site and neon_site #2363 +Fix misplaced stopf in CNDriverMod.F90 #2358 +Update some PE layouts on Derecho #2429 + +Significant changes to scientifically-supported configurations +-------------------------------------------------------------- + +Does this tag change answers significantly for any of the following physics configurations? +(Details of any changes will be given in the "Answer changes" section below.) + + [Put an [X] in the box for any configuration with significant answer changes.] + +[ ] clm5_1 + +[ ] clm5_0 + +[ ] ctsm5_0-nwp + +[ ] clm4_5 + + +Bugs fixed +---------- +CTSM issues fixed (include CTSM Issue #): + Listed in Purpose and Description above + +Testing summary: +---------------- + + [PASS means all tests PASS; OK means tests PASS other than expected fails.] + + regular tests (aux_clm: https://github.com/ESCOMP/CTSM/wiki/System-Testing-Guide#pre-merge-system-testing): + + derecho ----- OK + izumi ------- OK + +Answer changes +-------------- +Changes answers relative to baseline: NO + +Other details +------------- +List any externals directories updated (cime, rtm, mosart, cism, fates, etc.): + See #2421 for update of externals to what's expected in cesm2_3_beta17 + +Pull Requests that document the changes (include PR ids): + https://github.com/ESCOMP/ctsm/pull/2431 + +=============================================================== +=============================================================== Tag name: ctsm5.1.dev174 Originator(s): olyson (Keith Oleson,UCAR/TSS) Date: Thu 14 Mar 2024 04:56:37 PM MDT diff --git a/doc/ChangeSum b/doc/ChangeSum index 49afdeb03f..e8c32d90a8 100644 --- a/doc/ChangeSum +++ b/doc/ChangeSum @@ -1,5 +1,6 @@ Tag Who Date Summary ============================================================================================================================ + ctsm5.1.dev175 slevis 03/21/2024 merge-b4bdev-20240321 ctsm5.1.dev174 olyson 03/14/2024 Improve vegetation health at high latitudes ctsm5.1.dev173 rgknox 03/13/2024 New FATES namelist variable: fates_history_dimlevel ctsm5.1.dev172 erik 03/12/2024 Merge b4b-dev diff --git a/lilac/src/lilac_mod.F90 b/lilac/src/lilac_mod.F90 index 12dd4f74a6..af5e2edd22 100644 --- a/lilac/src/lilac_mod.F90 +++ b/lilac/src/lilac_mod.F90 @@ -8,7 +8,6 @@ module lilac_mod ! External libraries use ESMF - use mct_mod , only : mct_world_init ! shr code routines use shr_sys_mod , only : shr_sys_abort @@ -146,10 +145,7 @@ subroutine lilac_init2(mpicom, atm_global_index, atm_lons, atm_lats, & integer, parameter :: debug = 1 !-- internal debug level character(len=*), parameter :: subname=trim(modname)//': [lilac_init] ' - ! initialization of mct and pio - integer :: ncomps = 1 ! for mct - integer, pointer :: mycomms(:) ! for mct - integer, pointer :: myids(:) ! for mct + ! initialization of pio integer :: compids(1) = (/1/) ! for pio_init2 - array with component ids character(len=32) :: compLabels(1) = (/'LND'/) ! for pio_init2 character(len=64) :: comp_name(1) = (/'LND'/) ! for pio_init2 @@ -220,14 +216,6 @@ subroutine lilac_init2(mpicom, atm_global_index, atm_lons, atm_lats, & call ESMF_VMGet(vm, localPet=mytask, rc=rc) if (chkerr(rc,__LINE__,u_FILE_u)) return - !------------------------------------------------------------------------- - ! Initialize MCT (this is needed for data model functionality) - !------------------------------------------- - allocate(mycomms(1), myids(1)) - mycomms = (/mpicom/) ; myids = (/1/) - call mct_world_init(ncomps, mpicom, mycomms, myids) - call ESMF_LogWrite(subname//"initialized mct ... ", ESMF_LOGMSG_INFO) - !------------------------------------------------------------------------- ! Initialize PIO with second initialization !------------------------------------------------------------------------- diff --git a/python/Makefile b/python/Makefile index b43e1c5e53..9645242111 100644 --- a/python/Makefile +++ b/python/Makefile @@ -28,6 +28,10 @@ PYLINT_SRC = \ # ../cime_config/buildnml all: test black lint +# ---------------------------------------------------------------- +# See the stest target about this issue + @echo "Run './run_ctsm_py_tests --sys' by hand afterwards" +# ---------------------------------------------------------------- @echo @echo @echo "Successfully ran all standard tests" @@ -40,7 +44,14 @@ utest: FORCE .PHONY: stest stest: FORCE - $(PYTHON) ./run_ctsm_py_tests $(TEST_ARGS) --sys +# ---------------------------------------------------------------- +# EBK 2024-03-19: Comment out running here because of this issue: +# https://github.com/ESCOMP/CTSM/pull/2363#issuecomment-1967884908 +#$(PYTHON) ./run_ctsm_py_tests $(TEST_ARGS) --sys +# Instead run by hand which seems to be working for now... +# ---------------------------------------------------------------- + @echo "System tests currently don't run under Make so..." + @echo "Run './run_ctsm_py_tests --sys' by hand afterwards" .PHONY: lint lint: FORCE diff --git a/python/ctsm/site_and_regional/neon_site.py b/python/ctsm/site_and_regional/neon_site.py index 9ad690becb..4af8e66fdd 100755 --- a/python/ctsm/site_and_regional/neon_site.py +++ b/python/ctsm/site_and_regional/neon_site.py @@ -3,18 +3,18 @@ """ # Import libraries -import glob import logging import os -import re -import shutil import sys -import time # Get the ctsm util tools and then the cime tools. _CTSM_PYTHON = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "python")) sys.path.insert(1, _CTSM_PYTHON) +# -- import local classes for this script +# pylint: disable=wrong-import-position +from ctsm.site_and_regional.tower_site import TowerSite + # pylint: disable=wrong-import-position, import-error, unused-import, wrong-import-order from ctsm import add_cime_to_path from ctsm.path_utils import path_to_ctsm_root @@ -28,130 +28,31 @@ # pylint: disable=too-many-instance-attributes -class NeonSite: +class NeonSite(TowerSite): """ A class for encapsulating neon sites. """ - def __init__(self, name, start_year, end_year, start_month, end_month, finidat): - self.name = name - self.start_year = int(start_year) - self.end_year = int(end_year) - self.start_month = int(start_month) - self.end_month = int(end_month) - self.cesmroot = path_to_ctsm_root() - self.finidat = finidat - def build_base_case( - self, cesmroot, output_root, res, compset, overwrite=False, setup_only=False + self, + cesmroot, + output_root, + res, + compset, + user_mods_dirs=None, + overwrite=False, + setup_only=False, ): - """ - Function for building a base_case to clone. - To spend less time on building ctsm for the neon cases, - all the other cases are cloned from this case - - Args: - self: - The NeonSite object - base_root (str): - root of the base_case CIME - res (str): - base_case resolution or gridname - compset (str): - base case compset - overwrite (bool) : - Flag to overwrite the case if exists - """ - print("---- building a base case -------") - # pylint: disable=attribute-defined-outside-init - self.base_case_root = output_root - # pylint: enable=attribute-defined-outside-init - user_mods_dirs = [os.path.join(cesmroot, "cime_config", "usermods_dirs", "NEON", self.name)] - if not output_root: - output_root = os.getcwd() - case_path = os.path.join(output_root, self.name) - - logger.info("base_case_name : %s", self.name) - logger.info("user_mods_dir : %s", user_mods_dirs[0]) - - if overwrite and os.path.isdir(case_path): - print("Removing the existing case at: {}".format(case_path)) - if os.getcwd() == case_path: - abort("Trying to remove the directory tree that we are in") - - shutil.rmtree(case_path) - - with Case(case_path, read_only=False) as case: - if not os.path.isdir(case_path): - print("---- creating a base case -------") - - case.create( - case_path, - cesmroot, - compset, - res, - run_unsupported=True, - answer="r", - output_root=output_root, - user_mods_dirs=user_mods_dirs, - driver="nuopc", - ) - - print("---- base case created ------") - - # --change any config for base_case: - # case.set_value("RUN_TYPE","startup") - print("---- base case setup ------") - case.case_setup() - else: - # For existing case check that the compset name is correct - existingcompname = case.get_value("COMPSET") - match = re.search("^HIST", existingcompname, flags=re.IGNORECASE) - if re.search("^HIST", compset, flags=re.IGNORECASE) is None: - expect( - match is None, - """Existing base case is a historical type and should not be - --rerun with the --overwrite option""", - ) - else: - expect( - match is not None, - """Existing base case should be a historical type and is not - --rerun with the --overwrite option""", - ) - # reset the case - case.case_setup(reset=True) - case_path = case.get_value("CASEROOT") - - if setup_only: - return case_path + if user_mods_dirs is None: + user_mods_dirs = [ + os.path.join(self.cesmroot, "cime_config", "usermods_dirs", "NEON", self.name) + ] + print("in neonsite adding usermodsdirs") + print("usermodsdirs: {}".format(user_mods_dirs)) + case_path = super().build_base_case(cesmroot, output_root, res, compset, user_mods_dirs) - print("---- base case build ------") - print("--- This may take a while and you may see WARNING messages ---") - # always walk through the build process to make sure it's up to date. - initial_time = time.time() - build.case_build(case_path, case=case) - end_time = time.time() - total = end_time - initial_time - print("Time required to building the base case: {} s.".format(total)) - # update case_path to be the full path to the base case return case_path - # pylint: disable=no-self-use - def get_batch_query(self, case): - """ - Function for querying the batch queue query command for a case, depending on the - user's batch system. - - Args: - case: - case object - """ - - if case.get_value("BATCH_SYSTEM") == "none": - return "none" - return case.get_value("batch_query") - # pylint: disable=too-many-statements def run_case( self, @@ -160,6 +61,8 @@ def run_case( prism, run_length, user_version, + tower_type=None, + user_mods_dirs=None, overwrite=False, setup_only=False, no_batch=False, @@ -195,206 +98,16 @@ def run_case( user_mods_dirs = [ os.path.join(self.cesmroot, "cime_config", "usermods_dirs", "NEON", self.name) ] - expect( - os.path.isdir(base_case_root), - "Error base case does not exist in {}".format(base_case_root), + tower_type = "NEON" + super().run_case( + base_case_root, run_type, prism, run_length, user_version, tower_type, user_mods_dirs ) - # -- if user gives a version: - if user_version: - version = user_version - else: - version = "latest" - - print("using this version:", version) - - if experiment is not None: - self.name = self.name + "." + experiment - case_root = os.path.abspath(os.path.join(base_case_root, "..", self.name + "." + run_type)) - - rundir = None - if os.path.isdir(case_root): - if overwrite: - print("---- removing the existing case -------") - if os.getcwd() == case_root: - abort("Trying to remove the directory tree that we are in") - shutil.rmtree(case_root) - elif rerun: - with Case(case_root, read_only=False) as case: - rundir = case.get_value("RUNDIR") - # For existing case check that the compset name is correct - existingcompname = case.get_value("COMPSET") - match = re.search("^HIST", existingcompname, flags=re.IGNORECASE) - # pylint: disable=undefined-variable - if re.search("^HIST", compset, flags=re.IGNORECASE) is None: - expect( - match is None, - """Existing base case is a historical type and should not be - --rerun with the --overwrite option""", - ) - # pylint: enable=undefined-variable - else: - expect( - match is not None, - """Existing base case should be a historical type and is not - --rerun with the --overwrite option""", - ) - if os.path.isfile(os.path.join(rundir, "ESMF_Profile.summary")): - print("Case {} appears to be complete, not rerunning.".format(case_root)) - elif not setup_only: - print("Resubmitting case {}".format(case_root)) - case.submit(no_batch=no_batch) - print("-----------------------------------") - print("Successfully submitted case!") - batch_query = self.get_batch_query(case) - if batch_query != "none": - print(f"Use {batch_query} to check its run status") - return - else: - logger.warning("Case already exists in %s, not overwritting", case_root) - return - - if run_type == "postad": - adcase_root = case_root.replace(".postad", ".ad") - if not os.path.isdir(adcase_root): - logger.warning("postad requested but no ad case found in %s", adcase_root) - return - - if not os.path.isdir(case_root): - # read_only = False should not be required here - with Case(base_case_root, read_only=False) as basecase: - print("---- cloning the base case in {}".format(case_root)) - # - # EBK: 11/05/2022 -- Note keeping the user_mods_dirs argument is important. Although - # it causes some of the user_nl_* files to have duplicated inputs. It also ensures - # that the shell_commands file is copied, as well as taking care of the DATM inputs. - # See https://github.com/ESCOMP/CTSM/pull/1872#pullrequestreview-1169407493 - # - basecase.create_clone(case_root, keepexe=True, user_mods_dirs=user_mods_dirs) - - with Case(case_root, read_only=False) as case: - if run_type != "transient": - # in order to avoid the complication of leap years, - # we always set the run_length in units of days. - case.set_value("STOP_OPTION", "ndays") - case.set_value("REST_OPTION", "end") - case.set_value("CONTINUE_RUN", False) - case.set_value("NEONVERSION", version) - if prism: - case.set_value("CLM_USRDAT_NAME", "NEON.PRISM") - - if run_type == "ad": - case.set_value("CLM_FORCE_COLDSTART", "on") - case.set_value("CLM_ACCELERATED_SPINUP", "on") - case.set_value("RUN_REFDATE", "0018-01-01") - case.set_value("RUN_STARTDATE", "0018-01-01") - case.set_value("RESUBMIT", 1) - case.set_value("STOP_N", run_length) - - else: - case.set_value("CLM_FORCE_COLDSTART", "off") - case.set_value("CLM_ACCELERATED_SPINUP", "off") - case.set_value("RUN_TYPE", "hybrid") - - if run_type == "postad": - self.set_ref_case(case) - case.set_value("STOP_N", run_length) - - # For transient cases STOP will be set in the user_mod_directory - if run_type == "transient": - if self.finidat: - case.set_value("RUN_TYPE", "startup") - else: - if not self.set_ref_case(case): - return - case.set_value("CALENDAR", "GREGORIAN") - case.set_value("RESUBMIT", 0) - case.set_value("STOP_OPTION", "nmonths") - - if not rundir: - rundir = case.get_value("RUNDIR") - self.modify_user_nl(case_root, run_type, rundir) - - case.create_namelists() - # explicitly run check_input_data - case.check_all_input_data() - if not setup_only: - case.submit(no_batch=no_batch) - print("-----------------------------------") - print("Successfully submitted case!") - batch_query = self.get_batch_query(case) - if batch_query != "none": - print(f"Use {batch_query} to check its run status") - - def set_ref_case(self, case): - """ - Set an existing case as the reference case, eg for use with spinup. - """ - rundir = case.get_value("RUNDIR") - case_root = case.get_value("CASEROOT") - if case_root.endswith(".postad"): - ref_case_root = case_root.replace(".postad", ".ad") - root = ".ad" - else: - ref_case_root = case_root.replace(".transient", ".postad") - root = ".postad" - if not os.path.isdir(ref_case_root): - logger.warning( - "ERROR: spinup must be completed first, could not find directory %s", ref_case_root - ) - return False - - with Case(ref_case_root) as refcase: - refrundir = refcase.get_value("RUNDIR") - case.set_value("RUN_REFDIR", refrundir) - case.set_value("RUN_REFCASE", os.path.basename(ref_case_root)) - refdate = None - for reffile in glob.iglob(refrundir + "/{}{}.clm2.r.*.nc".format(self.name, root)): - m_searched = re.search(r"(\d\d\d\d-\d\d-\d\d)-\d\d\d\d\d.nc", reffile) - if m_searched: - refdate = m_searched.group(1) - symlink_force(reffile, os.path.join(rundir, os.path.basename(reffile))) - logger.info("Found refdate of %s", refdate) - if not refdate: - logger.warning("Could not find refcase for %s", case_root) - return False - - for rpfile in glob.iglob(refrundir + "/rpointer*"): - safe_copy(rpfile, rundir) - if not os.path.isdir(os.path.join(rundir, "inputdata")) and os.path.isdir( - os.path.join(refrundir, "inputdata") - ): - symlink_force(os.path.join(refrundir, "inputdata"), os.path.join(rundir, "inputdata")) - - case.set_value("RUN_REFDATE", refdate) - if case_root.endswith(".postad"): - case.set_value("RUN_STARTDATE", refdate) - # NOTE: if start options are set, RUN_STARTDATE should be modified here - return True - - def modify_user_nl(self, case_root, run_type, rundir): - """ - Modify user namelist. If transient, include finidat in user_nl; - Otherwise, adjust user_nl to include different mfilt, nhtfrq, and variables in hist_fincl1. - """ - user_nl_fname = os.path.join(case_root, "user_nl_clm") - user_nl_lines = None - if run_type == "transient": - if self.finidat: - user_nl_lines = [ - "finidat = '{}/inputdata/lnd/ctsm/initdata/{}'".format(rundir, self.finidat) - ] - else: - user_nl_lines = [ - "hist_fincl2 = ''", - "hist_mfilt = 20", - "hist_nhtfrq = -8760", - "hist_empty_htapes = .true.", + def modify_user_nl(self, case_root, run_type, rundir, site_lines=None): + # TODO: include neon-specific user namelist lines, using this as just an example currently + if site_lines is None: + site_lines = [ """hist_fincl1 = 'TOTECOSYSC', 'TOTECOSYSN', 'TOTSOMC', 'TOTSOMN', 'TOTVEGC', - 'TOTVEGN', 'TLAI', 'GPP', 'CPOOL', 'NPP', 'TWS', 'H2OSNO'""", + 'TOTVEGN', 'TLAI', 'GPP', 'CPOOL', 'NPP', 'TWS', 'H2OSNO',""" ] - - if user_nl_lines: - with open(user_nl_fname, "a") as nl_file: - for line in user_nl_lines: - nl_file.write("{}\n".format(line)) + super().modify_user_nl(case_root, run_type, rundir, site_lines) diff --git a/python/ctsm/site_and_regional/run_neon.py b/python/ctsm/site_and_regional/run_neon.py index 72bf3fdfb4..d831f8dba2 100755 --- a/python/ctsm/site_and_regional/run_neon.py +++ b/python/ctsm/site_and_regional/run_neon.py @@ -174,8 +174,10 @@ def main(description): """ cesmroot = path_to_ctsm_root() # Get the list of supported neon sites from usermods + # The [!Fd]* portion means that we won't retrieve cases that start with: + # F (FATES) or d (default). We should be aware of adding cases that start with these. valid_neon_sites = glob.glob( - os.path.join(cesmroot, "cime_config", "usermods_dirs", "NEON", "[!d]*") + os.path.join(cesmroot, "cime_config", "usermods_dirs", "NEON", "[!Fd]*") ) valid_neon_sites = sorted([v.split("/")[-1] for v in valid_neon_sites]) @@ -220,8 +222,9 @@ def main(description): if run_from_postad: neon_site.finidat = None if not base_case_root: + user_mods_dirs = None base_case_root = neon_site.build_base_case( - cesmroot, output_root, res, compset, overwrite, setup_only + cesmroot, output_root, res, compset, user_mods_dirs, overwrite, setup_only ) logger.info("-----------------------------------") logger.info("Running CTSM for neon site : %s", neon_site.name) diff --git a/python/ctsm/site_and_regional/tower_site.py b/python/ctsm/site_and_regional/tower_site.py new file mode 100644 index 0000000000..1679df83e9 --- /dev/null +++ b/python/ctsm/site_and_regional/tower_site.py @@ -0,0 +1,421 @@ +""" +This module includes the definition for the TowerSite class, +which has NeonSite and Plumber2Site child classes. This class defines common +functionalities that are in both NeonSite and Plumber2Site classes. +""" +# -- Import libraries + +# -- standard libraries +import os.path +import glob +import logging +import re +import shutil +import sys +import time + +# Get the ctsm util tools and then the cime tools. +_CTSM_PYTHON = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "python")) +sys.path.insert(1, _CTSM_PYTHON) + +# pylint: disable=wrong-import-position, import-error, unused-import, wrong-import-order +from ctsm import add_cime_to_path +from ctsm.path_utils import path_to_ctsm_root +from ctsm.utils import abort + +from CIME import build +from CIME.case import Case +from CIME.utils import safe_copy, expect, symlink_force + +logger = logging.getLogger(__name__) + + +# pylint: disable=too-many-instance-attributes +class TowerSite: + """ + Parent class to NeonSite and Plumber2Site classes. + ... + Attributes + ---------- + Methods + ------- + """ + + def __init__(self, name, start_year, end_year, start_month, end_month, finidat): + """ + Initializes TowerSite with the given arguments. + Parameters + ---------- + """ + self.name = name + self.start_year = int(start_year) + self.end_year = int(end_year) + self.start_month = int(start_month) + self.end_month = int(end_month) + self.cesmroot = path_to_ctsm_root() + self.finidat = finidat + + def __str__(self): + """ + Converts ingredients of the TowerSite to string for printing. + """ + return "{}\n{}".format( + str(self.__class__), + "\n".join( + ( + "{} = {}".format(str(key), str(self.__dict__[key])) + for key in sorted(self.__dict__) + ) + ), + ) + + def build_base_case( + self, cesmroot, output_root, res, compset, user_mods_dirs, overwrite=False, setup_only=False + ): + """ + Function for building a base_case to clone. + To spend less time on building ctsm for the neon cases, + all the other cases are cloned from this case + Args: + self: + The NeonSite object + base_root (str): + root of the base_case CIME + res (str): + base_case resolution or gridname + compset (str): + base case compset + overwrite (bool) : + Flag to overwrite the case if exists + """ + print("---- building a base case -------") + # pylint: disable=attribute-defined-outside-init + self.base_case_root = output_root + # pylint: enable=attribute-defined-outside-init + if not output_root: + output_root = os.getcwd() + case_path = os.path.join(output_root, self.name) + + logger.info("base_case_name : %s", self.name) + logger.info("user_mods_dir : %s", user_mods_dirs[0]) + + if overwrite and os.path.isdir(case_path): + print("Removing the existing case at: {}".format(case_path)) + if os.getcwd() == case_path: + abort("Trying to remove the directory tree that we are in") + + shutil.rmtree(case_path) + + with Case(case_path, read_only=False) as case: + if not os.path.isdir(case_path): + print("---- creating a base case -------") + case.create( + case_path, + cesmroot, + compset, + res, + run_unsupported=True, + answer="r", + output_root=output_root, + user_mods_dirs=user_mods_dirs, + driver="nuopc", + ) + + print("---- base case created ------") + + # --change any config for base_case: + # case.set_value("RUN_TYPE","startup") + print("---- base case setup ------") + case.case_setup() + else: + # For existing case check that the compset name is correct + existingcompname = case.get_value("COMPSET") + match = re.search("^HIST", existingcompname, flags=re.IGNORECASE) + if re.search("^HIST", compset, flags=re.IGNORECASE) is None: + expect( + match is None, + """Existing base case is a historical type and should not be + --rerun with the --overwrite option""", + ) + else: + expect( + match is not None, + """Existing base case should be a historical type and is not + --rerun with the --overwrite option""", + ) + # reset the case + case.case_setup(reset=True) + case_path = case.get_value("CASEROOT") + + if setup_only: + return case_path + + print("---- base case build ------") + print("--- This may take a while and you may see WARNING messages ---") + # always walk through the build process to make sure it's up to date. + initial_time = time.time() + build.case_build(case_path, case=case) + end_time = time.time() + total = end_time - initial_time + print("Time required to building the base case: {} s.".format(total)) + # update case_path to be the full path to the base case + return case_path + + # pylint: disable=no-self-use + def get_batch_query(self, case): + """ + Function for querying the batch queue query command for a case, depending on the + user's batch system. + Args: + case: + case object + """ + + if case.get_value("BATCH_SYSTEM") == "none": + return "none" + return case.get_value("batch_query") + + def modify_user_nl(self, case_root, run_type, rundir, site_lines=None): + """ + Modify user namelist. If transient, include finidat in user_nl; + Otherwise, adjust user_nl to include different mfilt, nhtfrq, and variables in hist_fincl1. + """ + user_nl_fname = os.path.join(case_root, "user_nl_clm") + user_nl_lines = None + if run_type == "transient": + if self.finidat: + user_nl_lines = [ + "finidat = '{}/inputdata/lnd/ctsm/initdata/{}'".format(rundir, self.finidat) + ] + else: + user_nl_lines = [ + "hist_fincl2 = ''", + "hist_mfilt = 20", + "hist_nhtfrq = -8760", + "hist_empty_htapes = .true.", + ] + site_lines + + if user_nl_lines: + with open(user_nl_fname, "a") as nl_file: + for line in user_nl_lines: + nl_file.write("{}\n".format(line)) + + def set_ref_case(self, case): + """ + Set an existing case as the reference case, eg for use with spinup. + """ + rundir = case.get_value("RUNDIR") + case_root = case.get_value("CASEROOT") + if case_root.endswith(".postad"): + ref_case_root = case_root.replace(".postad", ".ad") + root = ".ad" + else: + ref_case_root = case_root.replace(".transient", ".postad") + root = ".postad" + if not os.path.isdir(ref_case_root): + logger.warning( + "ERROR: spinup must be completed first, could not find directory %s", ref_case_root + ) + return False + + with Case(ref_case_root) as refcase: + refrundir = refcase.get_value("RUNDIR") + case.set_value("RUN_REFDIR", refrundir) + case.set_value("RUN_REFCASE", os.path.basename(ref_case_root)) + refdate = None + for reffile in glob.iglob(refrundir + "/{}{}.clm2.r.*.nc".format(self.name, root)): + m_searched = re.search(r"(\d\d\d\d-\d\d-\d\d)-\d\d\d\d\d.nc", reffile) + if m_searched: + refdate = m_searched.group(1) + symlink_force(reffile, os.path.join(rundir, os.path.basename(reffile))) + logger.info("Found refdate of %s", refdate) + if not refdate: + logger.warning("Could not find refcase for %s", case_root) + return False + + for rpfile in glob.iglob(refrundir + "/rpointer*"): + safe_copy(rpfile, rundir) + if not os.path.isdir(os.path.join(rundir, "inputdata")) and os.path.isdir( + os.path.join(refrundir, "inputdata") + ): + symlink_force(os.path.join(refrundir, "inputdata"), os.path.join(rundir, "inputdata")) + + case.set_value("RUN_REFDATE", refdate) + if case_root.endswith(".postad"): + case.set_value("RUN_STARTDATE", refdate) + # NOTE: if start options are set, RUN_STARTDATE should be modified here + return True + + # pylint: disable=too-many-statements + # TODO: This code should be broken up into smaller pieces + def run_case( + self, + base_case_root, + run_type, + prism, + run_length, + user_version, + tower_type, + user_mods_dirs, + overwrite=False, + setup_only=False, + no_batch=False, + rerun=False, + experiment=False, + ): + """ + Run case. + + Args: + self + base_case_root: str, opt + file path of base case + run_type: str, opt + transient, post_ad, or ad case, default transient + prism: bool, opt + if True, use PRISM precipitation, default False + run_length: str, opt + length of run, default '4Y' + user_version: str, opt + default 'latest' + overwrite: bool, opt + default False + setup_only: bool, opt + default False; if True, set up but do not run case + no_batch: bool, opt + default False + rerun: bool, opt + default False + experiment: str, opt + name of experiment, default False + """ + expect( + os.path.isdir(base_case_root), + "Error base case does not exist in {}".format(base_case_root), + ) + # -- if user gives a version: + if user_version: + version = user_version + else: + version = "latest" + + print("using this version:", version) + + if experiment is not False: + self.name = self.name + "." + experiment + case_root = os.path.abspath(os.path.join(base_case_root, "..", self.name + "." + run_type)) + + rundir = None + if os.path.isdir(case_root): + if overwrite: + print("---- removing the existing case -------") + if os.getcwd() == case_root: + abort("Trying to remove the directory tree that we are in") + + shutil.rmtree(case_root) + elif rerun: + with Case(case_root, read_only=False) as case: + rundir = case.get_value("RUNDIR") + # For existing case check that the compset name is correct + existingcompname = case.get_value("COMPSET") + match = re.search("^HIST", existingcompname, flags=re.IGNORECASE) + # pylint: disable=undefined-variable + if re.search("^HIST", compset, flags=re.IGNORECASE) is None: + expect( + match is None, + """Existing base case is a historical type and should not be + --rerun with the --overwrite option""", + ) + # pylint: enable=undefined-variable + else: + expect( + match is not None, + """Existing base case should be a historical type and is not + --rerun with the --overwrite option""", + ) + if os.path.isfile(os.path.join(rundir, "ESMF_Profile.summary")): + print("Case {} appears to be complete, not rerunning.".format(case_root)) + elif not setup_only: + print("Resubmitting case {}".format(case_root)) + case.submit(no_batch=no_batch) + print("-----------------------------------") + print("Successfully submitted case!") + batch_query = self.get_batch_query(case) + if batch_query != "none": + print(f"Use {batch_query} to check its run status") + return + else: + logger.warning("Case already exists in %s, not overwritting", case_root) + return + if run_type == "postad": + adcase_root = case_root.replace(".postad", ".ad") + if not os.path.isdir(adcase_root): + logger.warning("postad requested but no ad case found in %s", adcase_root) + return + + if not os.path.isdir(case_root): + # read_only = False should not be required here + with Case(base_case_root, read_only=False) as basecase: + print("---- cloning the base case in {}".format(case_root)) + # + # EBK: 11/05/2022 -- Note keeping the user_mods_dirs argument is important. Although + # it causes some of the user_nl_* files to have duplicated inputs. It also ensures + # that the shell_commands file is copied, as well as taking care of the DATM inputs. + # See https://github.com/ESCOMP/CTSM/pull/1872#pullrequestreview-1169407493 + # + basecase.create_clone(case_root, keepexe=True, user_mods_dirs=user_mods_dirs) + + with Case(case_root, read_only=False) as case: + if run_type != "transient": + # in order to avoid the complication of leap years, + # we always set the run_length in units of days. + case.set_value("STOP_OPTION", "ndays") + case.set_value("REST_OPTION", "end") + case.set_value("CONTINUE_RUN", False) + if tower_type == "NEON": + case.set_value("NEONVERSION", version) + if prism: + case.set_value("CLM_USRDAT_NAME", "NEON.PRISM") + + if run_type == "ad": + case.set_value("CLM_FORCE_COLDSTART", "on") + case.set_value("CLM_ACCELERATED_SPINUP", "on") + case.set_value("RUN_REFDATE", "0018-01-01") + case.set_value("RUN_STARTDATE", "0018-01-01") + case.set_value("RESUBMIT", 1) + case.set_value("STOP_N", run_length) + + else: + case.set_value("CLM_FORCE_COLDSTART", "off") + case.set_value("CLM_ACCELERATED_SPINUP", "off") + case.set_value("RUN_TYPE", "hybrid") + + if run_type == "postad": + self.set_ref_case(case) + case.set_value("STOP_N", run_length) + + # For transient cases STOP will be set in the user_mod_directory + if run_type == "transient": + if self.finidat: + case.set_value("RUN_TYPE", "startup") + else: + if not self.set_ref_case(case): + return + case.set_value("CALENDAR", "GREGORIAN") + case.set_value("RESUBMIT", 0) + case.set_value("STOP_OPTION", "nmonths") + if not rundir: + rundir = case.get_value("RUNDIR") + + self.modify_user_nl(case_root, run_type, rundir) + + case.create_namelists() + # explicitly run check_input_data + case.check_all_input_data() + if not setup_only: + case.submit(no_batch=no_batch) + print("-----------------------------------") + print("Successfully submitted case!") + batch_query = self.get_batch_query(case) + if batch_query != "none": + print(f"Use {batch_query} to check its run status") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 27d85d464f..568b53cd15 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,13 +19,10 @@ add_definitions(-DHIDE_MPI) add_subdirectory(${CLM_ROOT}/share/src csm_share) add_subdirectory(${CLM_ROOT}/share/unit_test_stubs/util csm_share_stubs) add_subdirectory(${CLM_ROOT}/share/src/esmf_wrf_timemgr esmf_wrf_timemgr) -add_subdirectory(${CLM_ROOT}/components/cpl7/driver/shr drv_share) -# Extract just the files we need from drv_share -set (drv_sources_needed_base - glc_elevclass_mod.F90 - ) -extract_sources("${drv_sources_needed_base}" "${drv_sources}" drv_sources_needed) +# Add the single file we need from CMEPS +set (drv_sources_needed + ${CLM_ROOT}/components/cmeps/cesm/nuopc_cap_share/glc_elevclass_mod.F90) # Add CLM source directories add_subdirectory(${CLM_ROOT}/src/utils clm_utils) diff --git a/src/biogeochem/CNDriverMod.F90 b/src/biogeochem/CNDriverMod.F90 index bee506c8cb..b23019eb23 100644 --- a/src/biogeochem/CNDriverMod.F90 +++ b/src/biogeochem/CNDriverMod.F90 @@ -285,9 +285,9 @@ subroutine CNDriverNoLeaching(bounds, nvegnpool, & num_bgc_vegp, filter_bgc_vegp, 0._r8, & num_bgc_soilc, filter_bgc_soilc, 0._r8) + call t_stopf('CNZero-vegbgc-nflux') end if - call t_stopf('CNZero-vegbgc-nflux') call t_startf('CNZero-soilbgc-nflux') call soilbiogeochem_nitrogenflux_inst%SetValues( & num_bgc_soilc, filter_bgc_soilc, 0._r8) diff --git a/src/biogeochem/CropType.F90 b/src/biogeochem/CropType.F90 index a96e9e939f..713e9fe5d9 100644 --- a/src/biogeochem/CropType.F90 +++ b/src/biogeochem/CropType.F90 @@ -352,7 +352,9 @@ subroutine InitHistory(this, bounds) ptr_patch=this%sowing_reason_perharv_patch, default='inactive') this%harvest_reason_thisyr_patch(begp:endp,:) = spval - call hist_addfld2d (fname='HARVEST_REASON_PERHARV', units='1 = mature; 2 = max season length; 3 = incorrect Dec. 31 sowing; 4 = sowing today; 5 = sowing tomorrow; 6 = tomorrow == idop; 7 = killed by cold temperature during vernalization', type2d='mxharvests', & + call hist_addfld2d (fname='HARVEST_REASON_PERHARV', units='1 = mature; 2 = max season length; 3 = incorrect Dec. 31 '// & + 'sowing; 4 = sowing today; 5 = sowing tomorrow; 6 = tomorrow == idop; 7 = killed by cold temperature during vernalization', & + type2d='mxharvests', & avgflag='I', long_name='Reason for each crop harvest; should only be output annually', & ptr_patch=this%harvest_reason_thisyr_patch, default='inactive') diff --git a/src/unit_test_stubs/csm_share/CMakeLists.txt b/src/unit_test_stubs/csm_share/CMakeLists.txt index f1c6f12ded..33ddbfb342 100644 --- a/src/unit_test_stubs/csm_share/CMakeLists.txt +++ b/src/unit_test_stubs/csm_share/CMakeLists.txt @@ -1,6 +1,4 @@ list(APPEND share_sources - mct_mod_stub.F90 - seq_comm_mct.F90 shr_mpi_mod_stub.F90 ) diff --git a/src/unit_test_stubs/csm_share/mct_mod_stub.F90 b/src/unit_test_stubs/csm_share/mct_mod_stub.F90 deleted file mode 100644 index 832b8847d7..0000000000 --- a/src/unit_test_stubs/csm_share/mct_mod_stub.F90 +++ /dev/null @@ -1,30 +0,0 @@ -module mct_mod - - ! This is a stub of mct_mod, which only includes the bare minimum needed to build CLM - ! unit tests - - implicit none - - public :: mct_gsMap - public :: mct_gsMap_orderedPoints - - type mct_gsMap - ! Empty, dummy type - end type mct_gsMap - -contains - - subroutine mct_gsMap_orderedPoints(GSMap, PEno, Points) - ! Stub routine that simply matches the signature of mct_gsMap_orderedPoints - ! this routine allocates the Points array, to match the documented behavior of the - ! real routine. This is needed so that a later deallocate will succeed. But note that - ! it is just allocated to be of size 1, so it cannot be used for any real - ! calculations. - type(mct_gsMap), intent(in) :: GSMap - integer, intent(in) :: PEno - integer,dimension(:),pointer :: Points - - allocate(Points(1)) - end subroutine mct_gsMap_orderedPoints - -end module mct_mod diff --git a/src/unit_test_stubs/csm_share/seq_comm_mct.F90 b/src/unit_test_stubs/csm_share/seq_comm_mct.F90 deleted file mode 100644 index f8201284ba..0000000000 --- a/src/unit_test_stubs/csm_share/seq_comm_mct.F90 +++ /dev/null @@ -1,10 +0,0 @@ -module seq_comm_mct - ! Stub of seq_comm_mct, containing just what's needed for CLM modules. - ! - ! Note that the true seq_comm_mct is in cime/scr/drivers/mct/shr - - implicit none - save - - integer, public :: logunit = 6 -end module seq_comm_mct