diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 887d27c308..5f19a43f3c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -22,6 +22,7 @@ e4d38681df23ccca0ae29581a45f8362574e0630 a9d96219902cf609636886c7073a84407f450d9a d866510188d26d51bcd6d37239283db690af7e82 0dcd0a3c1abcaffe5529f8d79a6bc34734b195c7 +e096358c832ab292ddfd22dd5878826c7c788968 # Ran SystemTests and python/ctsm through black python formatter 5364ad66eaceb55dde2d3d598fe4ce37ac83a93c 8056ae649c1b37f5e10aaaac79005d6e3a8b2380 diff --git a/python/ctsm/test/test_sys_gen_mksurfdata_jobscript_multi.py b/python/ctsm/test/test_sys_gen_mksurfdata_jobscript_multi.py index c7f2df5d0a..6f6bd88553 100755 --- a/python/ctsm/test/test_sys_gen_mksurfdata_jobscript_multi.py +++ b/python/ctsm/test/test_sys_gen_mksurfdata_jobscript_multi.py @@ -47,10 +47,12 @@ def tearDown(self): os.chdir(self._original_wd) shutil.rmtree(self._tempdir, ignore_errors=True) - def createJS(self, nodes, tasks_per_node, scenario, option_list=[]): + def createJS(self, nodes, tasks_per_node, scenario, option_list=None): """ Create a JobScript by sending a list of options in """ + if option_list is None: + option_list = [] if len(option_list) > 1: sys.argv.extend(option_list) sys.argv.extend( diff --git a/python/ctsm/test/test_sys_gen_mksurfdata_jobscript_single.py b/python/ctsm/test/test_sys_gen_mksurfdata_jobscript_single.py index 5913142a68..fdcc97ffd1 100755 --- a/python/ctsm/test/test_sys_gen_mksurfdata_jobscript_single.py +++ b/python/ctsm/test/test_sys_gen_mksurfdata_jobscript_single.py @@ -50,10 +50,12 @@ def tearDown(self): os.chdir(self._original_wd) shutil.rmtree(self._tempdir, ignore_errors=True) - def createJS(self, nodes, tasks_per_node, option_list=[]): + def createJS(self, nodes, tasks_per_node, option_list=None): """ Create a JobScript by sending a list of options in """ + if option_list is None: + option_list = [] if len(option_list) > 1: sys.argv.extend(option_list) sys.argv.extend( diff --git a/python/ctsm/test/test_sys_mesh_modifier.py b/python/ctsm/test/test_sys_mesh_modifier.py index 6b282fa0b0..f94c86e547 100755 --- a/python/ctsm/test/test_sys_mesh_modifier.py +++ b/python/ctsm/test/test_sys_mesh_modifier.py @@ -44,10 +44,6 @@ def setUp(self): path_to_ctsm_root(), "tools/modify_input_files/modify_mesh_template.cfg" ) self.testinputs_path = os.path.join(path_to_ctsm_root(), "python/ctsm/test/testinputs") - fsurdat_in = os.path.join( - self.testinputs_path, - "surfdata_5x5_amazon_hist_16pfts_CMIP6_2000_c231031.nc", - ) self._tempdir = tempfile.mkdtemp() self._cfg_file_path = os.path.join(self._tempdir, "modify_mesh_mask.cfg") self._mesh_mask_in = os.path.join(self._tempdir, "mesh_mask_in.nc") @@ -72,8 +68,8 @@ def setUp(self): except subprocess.CalledProcessError as e: sys.exit(f"{e} ERROR using {configure_cmd}") - def createScripGridAndMask(self): - """Create the SCRIP gird and mask file""" + def createScripGridAndMask(self, fsurdat_in): + """Create the SCRIP grid and mask file""" # Generate scrip file from fsurdat_in using nco # In the ctsm_py environment this requires running 'module load nco' # interactively @@ -86,14 +82,14 @@ def createScripGridAndMask(self): # This could also alturnatively be done, by using the stored SCRIP grid file for the resolution under CESM inputdata ncks_cmd = ( - f"ncks --rgr infer --rgr scrip={self.scrip_file} {self.fsurdat_in} {self.metadata_file}" + f"ncks --rgr infer --rgr scrip={self.scrip_file} {fsurdat_in} {self.metadata_file}" ) try: subprocess.check_call(ncks_cmd, shell=True) except subprocess.CalledProcessError as e: err_msg = ( f"{e} ERROR using ncks to generate {self.scrip_file} from " - + f"{self.fsurdat_in}; MOST LIKELY SHOULD INVOKE module load nco" + + f"{fsurdat_in}; MOST LIKELY SHOULD INVOKE module load nco" ) sys.exit(err_msg) # Run .env_mach_specific.sh to load esmf and generate mesh_mask_in @@ -109,13 +105,13 @@ def createScripGridAndMask(self): # Generate landmask_file from fsurdat_in self._lat_varname = "LATIXY" # same as in fsurdat_in self._lon_varname = "LONGXY" # same as in fsurdat_in - fsurdat_in_data = xr.open_dataset(self.fsurdat_in) + fsurdat_in_data = xr.open_dataset(fsurdat_in) assert self._lat_varname in fsurdat_in_data.variables assert self._lon_varname in fsurdat_in_data.variables self._lat_dimname = fsurdat_in_data[self._lat_varname].dims[0] self._lon_dimname = fsurdat_in_data[self._lat_varname].dims[1] - def createLandMaskFile(self): + def createLandMaskFile(self, fsurdat_in): """Create the LandMask file from the input fsurdat_in file""" if os.path.exists(self._landmask_file): os.remove(self._landmask_file) @@ -124,14 +120,12 @@ def createLandMaskFile(self): + "-A -v -s 'landmask=LANDFRAC_MKSURFDATA.convert(NC_INT)' " + f"-A -v -s {self._lat_varname}={self._lat_varname} " + f"-A -v -s {self._lon_varname}={self._lon_varname} " - + f"{self.fsurdat_in} {self._landmask_file}" + + f"{fsurdat_in} {self._landmask_file}" ) try: subprocess.check_call(ncap2_cmd, shell=True) except subprocess.CalledProcessError as e: - sys.exit( - f"{e} ERROR using ncap2 to generate {self._landmask_file} from {self.fsurdat_in}" - ) + sys.exit(f"{e} ERROR using ncap2 to generate {self._landmask_file} from {fsurdat_in}") def tearDown(self): """ @@ -147,13 +141,13 @@ def test_allInfo(self): For a case where the mesh remains unchanged, it's just output as ocean so the mesh is output as all zero's rather than the all 1's that came in. """ - self.fsurdat_in = os.path.join( + fsurdat_in = os.path.join( self.testinputs_path, "surfdata_5x5_amazon_hist_78pfts_CMIP6_2000_c230517.nc", ) - self.createScripGridAndMask() - self.createLandMaskFile() + self.createScripGridAndMask(fsurdat_in) + self.createLandMaskFile(fsurdat_in) self._create_config_file() # run the mesh_mask_modifier tool @@ -185,12 +179,12 @@ def test_modifyMesh(self): For a case where the mesh is changed. """ - self.fsurdat_in = os.path.join( + fsurdat_in = os.path.join( self.testinputs_path, "surfdata_5x5_amazon_hist_78pfts_CMIP6_2000_c230517_modify_mask.nc", ) - self.createScripGridAndMask() - self.createLandMaskFile() + self.createScripGridAndMask(fsurdat_in) + self.createLandMaskFile(fsurdat_in) self._create_config_file() if os.path.exists(self._mesh_mask_out): diff --git a/python/ctsm/test/test_unit_modify_fsurdat.py b/python/ctsm/test/test_unit_modify_fsurdat.py index 3b43e29a04..b796cd940d 100755 --- a/python/ctsm/test/test_unit_modify_fsurdat.py +++ b/python/ctsm/test/test_unit_modify_fsurdat.py @@ -390,7 +390,8 @@ def test_check_varlist_lists_wrongsizes(self): settings = {"var_lev1": lev1list} with self.assertRaisesRegex( SystemExit, - " Variable var_lev1 is 8 is of the wrong size. It should be = 9 in input settings dictionary", + " Variable var_lev1 is 8 is of the wrong size." + + " It should be = 9 in input settings dictionary", ): self.modify_fsurdat.check_varlist(settings) diff --git a/python/ctsm/toolchain/gen_mksurfdata_jobscript_multi.py b/python/ctsm/toolchain/gen_mksurfdata_jobscript_multi.py index 9b39f0e829..6e8d6ee3a7 100755 --- a/python/ctsm/toolchain/gen_mksurfdata_jobscript_multi.py +++ b/python/ctsm/toolchain/gen_mksurfdata_jobscript_multi.py @@ -137,6 +137,74 @@ def get_parser(): return parser +def write_runscript( + scenario, + jobscript_file, + number_of_nodes, + tasks_per_node, + account, + walltime, + queue, + target_list, + resolution_dict, + dataset_dict, + env_specific_script, + mksurfdata, + runfile, +): + """ + Write run script + """ + runfile.write("#!/bin/bash \n") + runfile.write(f"#PBS -A {account} \n") + runfile.write(f"#PBS -N mksrf_{scenario} \n") + runfile.write("#PBS -j oe \n") + runfile.write("#PBS -k eod \n") + runfile.write("#PBS -S /bin/bash \n") + runfile.write(f"#PBS -q {queue} \n") + runfile.write(f"#PBS -l walltime={walltime} \n") + runfile.write( + "#PBS -l select=" + + f"{number_of_nodes}:ncpus={tasks_per_node}:mpiprocs={tasks_per_node}:mem=218GB \n" + ) + runfile.write( + f"# This is a batch script to run a set of resolutions for mksurfdata_esmf {scenario}\n" + ) + runfile.write( + "# NOTE: THIS SCRIPT IS AUTOMATICALLY GENERATED " + + "SO IN GENERAL YOU SHOULD NOT EDIT it!!\n\n" + ) + runfile.write("\n") + + # Run env_mach_specific.sh to control the machine dependent + # environment including the paths to compilers and libraries + # external to cime such as netcdf + runfile.write(". " + env_specific_script + "\n") + check = "if [ $? != 0 ]; then echo 'Error running env_specific_script'; exit -4; fi" + runfile.write(f"{check} \n") + for target in target_list: + res_set = dataset_dict[target][1] + if res_set not in resolution_dict: + abort(f"Resolution is not in the resolution_dict: {res_set}") + for res in resolution_dict[res_set]: + namelist = f"{scenario}_{res}.namelist" + command = os.path.join(os.getcwd(), "gen_mksurfdata_namelist") + command = command + " " + dataset_dict[target][0] + " " + res + command = command + " --silent" + command = command + f" --namelist {namelist}" + print(f"command is {command}") + sys.argv = [x for x in command.split(" ") if x] + main_nml() + print(f"generated namelist {namelist}") + output = f"time mpiexec {mksurfdata} < {namelist}" + runfile.write(f"{output} \n") + check = f"if [ $? != 0 ]; then echo 'Error running resolution {res}'; exit -4; fi" + runfile.write(f"{check} \n") + runfile.write(f"echo Successfully ran resolution {res}\n") + + runfile.write(f"echo Successfully ran {jobscript_file}\n") + + def main(): """ See docstring at the top. @@ -168,7 +236,13 @@ def main(): # Determine resolution sets that are referenced in commands # TODO slevis: When new resolutions become supported in ccs_config, the # first entry will change to - # "standard_res_no_crop": ["0.9x1.25", "1.9x2.5", "mpasa60", "mpasa60-3conus", "mpasa60-3centralUS"], + # "standard_res_no_crop": [ + # "0.9x1.25", + # "1.9x2.5", + # "mpasa60", + # "mpasa60-3conus", + # "mpasa60-3centralUS", + # ], # -------------------------- resolution_dict = { "standard_res_no_crop": ["0.9x1.25", "1.9x2.5", "mpasa60"], @@ -246,7 +320,8 @@ def main(): "mpasa480", ), "crop-global-present-nldas": ( - "--start-year 2000 --end-year 2000 --res", # TODO slevis: --hirespft uses old data for now, so keep out + # TODO slevis: --hirespft uses old data for now, so keep out + "--start-year 2000 --end-year 2000 --res", "nldas_res", ), "crop-global-1850": ( @@ -370,7 +445,8 @@ def main(): if not os.path.exists(args.bld_path): print( args.bld_path - + " directory does NOT exist -- build mksurdata_esmf before running this script -- using ./gen_mksurfdata_build.sh" + + " directory does NOT exist -- build mksurdata_esmf before running this script --" + + " using ./gen_mksurfdata_build.sh" ) sys.exit(1) @@ -387,53 +463,20 @@ def main(): # -------------------------- with open(jobscript_file, "w", encoding="utf-8") as runfile: - runfile.write("#!/bin/bash \n") - runfile.write(f"#PBS -A {account} \n") - runfile.write(f"#PBS -N mksrf_{scenario} \n") - runfile.write("#PBS -j oe \n") - runfile.write("#PBS -k eod \n") - runfile.write("#PBS -S /bin/bash \n") - runfile.write(f"#PBS -q {queue} \n") - runfile.write(f"#PBS -l walltime={walltime} \n") - runfile.write( - f"#PBS -l select={number_of_nodes}:ncpus={tasks_per_node}:mpiprocs={tasks_per_node}:mem=218GB \n" - ) - runfile.write( - f"# This is a batch script to run a set of resolutions for mksurfdata_esmf {scenario} \n" - ) - runfile.write( - "# NOTE: THIS SCRIPT IS AUTOMATICALLY GENERATED SO IN GENERAL YOU SHOULD NOT EDIT it!!\n\n" + write_runscript( + scenario, + jobscript_file, + number_of_nodes, + tasks_per_node, + account, + walltime, + queue, + target_list, + resolution_dict, + dataset_dict, + env_specific_script, + mksurfdata, + runfile, ) - runfile.write("\n") - - n_p = int(tasks_per_node) * int(number_of_nodes) - - # Run env_mach_specific.sh to control the machine dependent - # environment including the paths to compilers and libraries - # external to cime such as netcdf - runfile.write(". " + env_specific_script + "\n") - check = "if [ $? != 0 ]; then echo 'Error running env_specific_script'; exit -4; fi" - runfile.write(f"{check} \n") - for target in target_list: - res_set = dataset_dict[target][1] - if res_set not in resolution_dict: - abort(f"Resolution is not in the resolution_dict: {res_set}") - for res in resolution_dict[res_set]: - namelist = f"{scenario}_{res}.namelist" - command = os.path.join(os.getcwd(), "gen_mksurfdata_namelist") - command = command + " " + dataset_dict[target][0] + " " + res - command = command + " --silent" - command = command + f" --namelist {namelist}" - print(f"command is {command}") - sys.argv = [x for x in command.split(" ") if x] - main_nml() - print(f"generated namelist {namelist}") - output = f"time mpiexec {mksurfdata} < {namelist}" - runfile.write(f"{output} \n") - check = f"if [ $? != 0 ]; then echo 'Error running resolution {res}'; exit -4; fi" - runfile.write(f"{check} \n") - runfile.write(f"echo Successfully ran resolution {res}\n") - - runfile.write(f"echo Successfully ran {jobscript_file}\n") print(f"echo Successfully created jobscript {jobscript_file}\n") diff --git a/python/ctsm/toolchain/gen_mksurfdata_jobscript_single.py b/python/ctsm/toolchain/gen_mksurfdata_jobscript_single.py index e0d6c32fee..f958d41e89 100755 --- a/python/ctsm/toolchain/gen_mksurfdata_jobscript_single.py +++ b/python/ctsm/toolchain/gen_mksurfdata_jobscript_single.py @@ -4,7 +4,6 @@ instructions, see README. """ import os -import sys import argparse import logging @@ -12,8 +11,10 @@ from ctsm.ctsm_logging import setup_logging_pre_config, add_logging_args, process_logging_args from ctsm.utils import abort from ctsm.path_utils import path_to_ctsm_root -from CIME.XML.env_mach_specific import EnvMachSpecific # pylint: disable=import-error -from CIME.BuildTools.configure import FakeCase # pylint: disable=import-error +from CIME.XML.env_mach_specific import ( # pylint: disable=import-error,wrong-import-order + EnvMachSpecific, +) +from CIME.BuildTools.configure import FakeCase # pylint: disable=import-error,wrong-import-order logger = logging.getLogger(__name__) @@ -93,6 +94,73 @@ def get_parser(): return parser +def write_runscript_part1(number_of_nodes, tasks_per_node, machine, account, runfile): + """ + Write run script (part 1) + """ + runfile.write("#!/bin/bash \n") + runfile.write("# Edit the batch directives for your batch system \n") + runfile.write(f"# Below are default batch directives for {machine} \n") + runfile.write("#PBS -N mksurfdata \n") + runfile.write("#PBS -j oe \n") + runfile.write("#PBS -k eod \n") + runfile.write("#PBS -S /bin/bash \n") + if machine == "derecho": + attribs = {"mpilib": "default"} + runfile.write("#PBS -l walltime=59:00 \n") + runfile.write(f"#PBS -A {account} \n") + runfile.write("#PBS -q main \n") + runfile.write( + "#PBS -l select=" + + f"{number_of_nodes}:ncpus={tasks_per_node}:mpiprocs={tasks_per_node} \n" + ) + elif machine == "casper": + attribs = {"mpilib": "default"} + runfile.write("#PBS -l walltime=1:00:00 \n") + runfile.write(f"#PBS -A {account} \n") + runfile.write("#PBS -q casper \n") + runfile.write( + f"#PBS -l select={number_of_nodes}:ncpus={tasks_per_node}:" + f"mpiprocs={tasks_per_node}:mem=80GB \n" + ) + elif machine == "izumi": + attribs = {"mpilib": "mvapich2"} + runfile.write("#PBS -l walltime=2:00:00 \n") + runfile.write("#PBS -q medium \n") + runfile.write(f"#PBS -l nodes={number_of_nodes}:ppn={tasks_per_node},mem=555GB -r n \n") + tool_path = os.path.dirname(os.path.abspath(__file__)) + runfile.write("\n") + runfile.write(f"cd {tool_path} \n") + + runfile.write("\n") + return attribs + + +def write_runscript_part2(namelist_file, runfile, executable, mksurfdata_path, env_mach_path): + """ + Write run script (part 2) + """ + runfile.write( + "# Run env_mach_specific.sh to control the machine " + "dependent environment including the paths to " + "compilers and libraries external to cime such as netcdf" + ) + runfile.write(f"\n. {env_mach_path}\n") + check = 'if [ $? != 0 ]; then echo "Error running env_mach_specific"; exit -4; fi' + runfile.write(f"{check} \n") + runfile.write( + "# Edit the mpirun command to use the MPI executable " + "on your system and the arguments it requires \n" + ) + output = f"{executable} {mksurfdata_path} < {namelist_file}" + runfile.write(f"{output} \n") + logger.info("run command is %s", output) + + check = f'if [ $? != 0 ]; then echo "Error running for namelist {namelist_file}"; exit -4; fi' + runfile.write(f"{check} \n") + runfile.write("echo Successfully ran resolution\n") + + def main(): """ See docstring at the top. @@ -117,109 +185,55 @@ def main(): # Write run script (part 1) # -------------------------- with open(jobscript_file, "w", encoding="utf-8") as runfile: - runfile.write("#!/bin/bash \n") - runfile.write("# Edit the batch directives for your batch system \n") - runfile.write(f"# Below are default batch directives for {machine} \n") - runfile.write("#PBS -N mksurfdata \n") - runfile.write("#PBS -j oe \n") - runfile.write("#PBS -k eod \n") - runfile.write("#PBS -S /bin/bash \n") - if machine == "derecho": - attribs = {"mpilib": "default"} - runfile.write("#PBS -l walltime=59:00 \n") - runfile.write(f"#PBS -A {account} \n") - runfile.write("#PBS -q main \n") - runfile.write( - f"#PBS -l select={number_of_nodes}:ncpus={tasks_per_node}:mpiprocs={tasks_per_node} \n" - ) - elif machine == "casper": - attribs = {"mpilib": "default"} - runfile.write("#PBS -l walltime=1:00:00 \n") - runfile.write(f"#PBS -A {account} \n") - runfile.write("#PBS -q casper \n") - runfile.write( - f"#PBS -l select={number_of_nodes}:ncpus={tasks_per_node}:" - f"mpiprocs={tasks_per_node}:mem=80GB \n" - ) - elif machine == "izumi": - attribs = {"mpilib": "mvapich2"} - runfile.write("#PBS -l walltime=2:00:00 \n") - runfile.write("#PBS -q medium \n") - runfile.write(f"#PBS -l nodes={number_of_nodes}:ppn={tasks_per_node},mem=555GB -r n \n") - tool_path = os.path.dirname(os.path.abspath(__file__)) - runfile.write("\n") - runfile.write(f"cd {tool_path} \n") - - runfile.write("\n") + attribs = write_runscript_part1(number_of_nodes, tasks_per_node, machine, account, runfile) - # -------------------------- - # Obtain mpirun command from env_mach_specific.xml - # -------------------------- - bld_path = args.bld_path - if not os.path.exists(bld_path): - abort("Input Build path (" + bld_path + ") does NOT exist, aborting") - # Get the ems_file object with standalone_configure=True - # and the fake_case object with mpilib=attribs['mpilib'] - # so as to use the get_mpirun function pointing to fake_case - ems_file = EnvMachSpecific(bld_path, standalone_configure=True) - fake_case = FakeCase( - compiler=None, mpilib=attribs["mpilib"], debug=False, comp_interface=None - ) - total_tasks = int(tasks_per_node) * int(number_of_nodes) - cmd = ems_file.get_mpirun( - fake_case, - attribs, - job="name", - exe_only=True, - overrides={ - "total_tasks": total_tasks, - }, - ) - # cmd is a tuple: - # cmd[0] contains the mpirun command (eg mpirun, mpiexe, etc) as string - # cmd[1] contains a list of strings that we append as options to cmd[0] - # The replace function removes unnecessary characters that appear in - # some such options - executable = f'{cmd[0]} {" ".join(cmd[1])}'.replace("ENV{", "").replace("}", "") - - mksurfdata_path = os.path.join(bld_path, "mksurfdata") - if not os.path.exists(mksurfdata_path): - abort( - "mksurfdata_esmf executable (" - + mksurfdata_path - + ") does NOT exist in the bld-path, aborting" - ) - env_mach_path = os.path.join(bld_path, ".env_mach_specific.sh") - if not os.path.exists(env_mach_path): - abort( - "Environment machine specific file (" - + env_mach_path - + ") does NOT exist in the bld-path, aborting" - ) - - # -------------------------- - # Write run script (part 2) - # -------------------------- - runfile.write( - "# Run env_mach_specific.sh to control the machine " - "dependent environment including the paths to " - "compilers and libraries external to cime such as netcdf" + # -------------------------- + # Obtain mpirun command from env_mach_specific.xml + # -------------------------- + bld_path = args.bld_path + if not os.path.exists(bld_path): + abort("Input Build path (" + bld_path + ") does NOT exist, aborting") + # Get the ems_file object with standalone_configure=True + # and the fake_case object with mpilib=attribs['mpilib'] + # so as to use the get_mpirun function pointing to fake_case + ems_file = EnvMachSpecific(bld_path, standalone_configure=True) + fake_case = FakeCase(compiler=None, mpilib=attribs["mpilib"], debug=False, comp_interface=None) + total_tasks = int(tasks_per_node) * int(number_of_nodes) + cmd = ems_file.get_mpirun( + fake_case, + attribs, + job="name", + exe_only=True, + overrides={ + "total_tasks": total_tasks, + }, + ) + # cmd is a tuple: + # cmd[0] contains the mpirun command (eg mpirun, mpiexe, etc) as string + # cmd[1] contains a list of strings that we append as options to cmd[0] + # The replace function removes unnecessary characters that appear in + # some such options + executable = f'{cmd[0]} {" ".join(cmd[1])}'.replace("ENV{", "").replace("}", "") + + mksurfdata_path = os.path.join(bld_path, "mksurfdata") + if not os.path.exists(mksurfdata_path): + abort( + "mksurfdata_esmf executable (" + + mksurfdata_path + + ") does NOT exist in the bld-path, aborting" ) - runfile.write(f"\n. {env_mach_path}\n") - check = 'if [ $? != 0 ]; then echo "Error running env_mach_specific"; exit -4; fi' - runfile.write(f"{check} \n") - runfile.write( - "# Edit the mpirun command to use the MPI executable " - "on your system and the arguments it requires \n" + env_mach_path = os.path.join(bld_path, ".env_mach_specific.sh") + if not os.path.exists(env_mach_path): + abort( + "Environment machine specific file (" + + env_mach_path + + ") does NOT exist in the bld-path, aborting" ) - output = f"{executable} {mksurfdata_path} < {namelist_file}" - runfile.write(f"{output} \n") - logger.info("run command is %s", output) - check = ( - f'if [ $? != 0 ]; then echo "Error running for namelist {namelist_file}"; exit -4; fi' - ) - runfile.write(f"{check} \n") - runfile.write("echo Successfully ran resolution\n") + # -------------------------- + # Write run script (part 2) + # -------------------------- + with open(jobscript_file, "a", encoding="utf-8") as runfile: + write_runscript_part2(namelist_file, runfile, executable, mksurfdata_path, env_mach_path) print(f"echo Successfully created jobscript {jobscript_file}\n") diff --git a/python/ctsm/toolchain/gen_mksurfdata_namelist.py b/python/ctsm/toolchain/gen_mksurfdata_namelist.py index 9d6eecbc9d..8a953c39df 100755 --- a/python/ctsm/toolchain/gen_mksurfdata_namelist.py +++ b/python/ctsm/toolchain/gen_mksurfdata_namelist.py @@ -259,6 +259,7 @@ def main(): """ See docstring at the top. """ + # pylint: disable=too-many-statements args = get_parser().parse_args() process_logging_args(args) @@ -278,6 +279,167 @@ def main(): potveg = args.potveg_flag glc_nec = args.glc_nec + hires_pft, hires_soitex = process_hires_options(args, start_year, end_year) + + if force_model_mesh_file is not None: + open_mesh_file(force_model_mesh_file, force_model_mesh_nx, force_model_mesh_ny) + + hostname = os.getenv("HOSTNAME") + logname = os.getenv("LOGNAME") + + logger.info("hostname is %s", hostname) + logger.info("logname is %s", logname) + + if ssp_rcp == "none": + check_ssp_years(start_year, end_year) + + # determine pft_years - needed to parse xml file + pft_years_ssp, pft_years = determine_pft_years(start_year, end_year, potveg) + + # Create land-use txt file for a transient case. + # Determine the run type and if a transient run create output landuse txt file + if end_year > start_year: + run_type = "transient" + else: + run_type = "timeslice" + logger.info("run_type = %s", run_type) + + # error check on glc_nec + if (glc_nec <= 0) or (glc_nec >= 100): + raise argparse.ArgumentTypeError("ERROR: glc_nec must be between 1 and 99.") + + # create attribute list for parsing xml file + attribute_list = { + "hires_pft": hires_pft, + "hires_soitex": hires_soitex, + "pft_years": pft_years, + "pft_years_ssp": pft_years_ssp, + "ssp_rcp": ssp_rcp, + "res": res, + } + + # determine input rawdata + tool_path, must_run_download_input_data, rawdata_files = determine_input_rawdata( + start_year, + input_path, + attribute_list, + ) + + # determine output mesh + determine_output_mesh(res, force_model_mesh_file, input_path, rawdata_files, tool_path) + + # Determine num_pft + if nocrop_flag: + num_pft = "16" + else: + num_pft = "78" + logger.info("num_pft is %s", num_pft) + + # Write out if surface dataset will be created + if nosurfdata_flag: + logger.info("surface dataset will not be created") + else: + logger.info("surface dataset will be created") + + ( + landuse_fname, + fdyndat, + nlfname, + fsurdat, + fsurlog, + must_run_download_input_data, + ) = get_file_paths( + args, + start_year, + end_year, + ssp_rcp, + res, + pft_years, + run_type, + rawdata_files, + num_pft, + must_run_download_input_data, + ) + + git_desc_cmd = f"git -C {tool_path} describe" + try: + # The "git -C" option permits a system test to run this tool from + # elsewhere while running the git command from the tool_path + gitdescribe = subprocess.check_output(git_desc_cmd, shell=True).strip() + except subprocess.CalledProcessError as error: + # In case the "git -C" option is unavailable, as on casper (2022/5/24) + # Still, this does NOT allow the system test to work on machines + # without git -C + logger.info("git -C option unavailable on casper as of 2022/7/2 %s", error) + gitdescribe = subprocess.check_output("git describe", shell=True).strip() + gitdescribe = gitdescribe.decode("utf-8") + + # The below two overrides are only used for testing and validation + # it takes a long time to generate the mapping files + # from 1km to the following two resolutions since the output mesh has so few points + if res == "10x15": + mksrf_ftopostats_override = os.path.join( + input_path, "lnd", "clm2", "rawdata", "surfdata_topo_10x15_c220303.nc" + ) + logger.info("will override mksrf_ftopostats with = %s", mksrf_ftopostats_override) + else: + mksrf_ftopostats_override = "" + + # ---------------------------------------- + # Write output namelist file + # ---------------------------------------- + + with open(nlfname, "w", encoding="utf-8") as nlfile: + nlfile.write("&mksurfdata_input \n") + + # ------------------- + # raw input data + # ------------------- + must_run_download_input_data = write_nml_rawinput( + start_year, + force_model_mesh_file, + force_model_mesh_nx, + force_model_mesh_ny, + vic_flag, + rawdata_files, + landuse_fname, + mksrf_ftopostats_override, + nlfile, + must_run_download_input_data, + ) + + # ------------------- + # output data + # ------------------- + write_nml_outdata( + nosurfdata_flag, + vic_flag, + inlandwet, + glc_flag, + hostname, + logname, + num_pft, + fdyndat, + fsurdat, + fsurlog, + gitdescribe, + nlfile, + ) + + nlfile.write("/ \n") + + if must_run_download_input_data: + temp_nlfname = "surfdata.namelist" + os.rename(nlfname, temp_nlfname) + nlfname = temp_nlfname + + print(f"Successfully created input namelist file {nlfname}") + + +def process_hires_options(args, start_year, end_year): + """ + Process options related to hi-res + """ if args.hres_pft: if (start_year == 1850 and end_year == 1850) or (start_year == 2005 and end_year == 2005): hires_pft = "on" @@ -294,69 +456,85 @@ def main(): hires_soitex = "on" else: hires_soitex = "off" + return hires_pft, hires_soitex - if force_model_mesh_file is not None: - # open mesh_file to read element_count and, if available, orig_grid_dims - mesh_file = netCDF4.Dataset(force_model_mesh_file, "r") - element_count = mesh_file.dimensions["elementCount"].size - if "origGridDims" in mesh_file.variables: - orig_grid_dims = mesh_file.variables["origGridDims"] - if ( - int(force_model_mesh_nx) == orig_grid_dims[0] - and int(force_model_mesh_ny) == orig_grid_dims[1] - ): - mesh_file.close() - else: - error_msg = ( - "ERROR: Found variable origGridDims in " - f"{force_model_mesh_file} with values " - f"{orig_grid_dims[:]} that do not agree with the " - "user-entered mesh_nx and mesh_ny values of " - f"{[force_model_mesh_nx, force_model_mesh_ny]}." - ) - sys.exit(error_msg) - elif force_model_mesh_nx is None or force_model_mesh_ny is None: - error_msg = ( - "ERROR: You set --model-mesh so you MUST ALSO " - "SET --model-mesh-nx AND --model-mesh-ny" - ) - sys.exit(error_msg) - # using force_model_mesh_nx and force_model_mesh_ny either from the - # mesh file (see previous if statement) or the user-entered values - if element_count != int(force_model_mesh_nx) * int(force_model_mesh_ny): - error_msg = ( - "ERROR: The product of " - "--model-mesh-nx x --model-mesh-ny must equal " - "exactly elementCount in --model-mesh" - ) - sys.exit(error_msg) +def check_ssp_years(start_year, end_year): + """ + Check years associated with SSP period + """ + if int(start_year) > 2015: + error_msg = ( + "ERROR: if start-year > 2015 must add an --ssp_rcp " + "argument that is not none: valid opts for ssp-rcp " + f"are {valid_opts}" + ) + sys.exit(error_msg) + elif int(end_year) > 2015: + error_msg = ( + "ERROR: if end-year > 2015 must add an --ssp-rcp " + "argument that is not none: valid opts for ssp-rcp " + f"are {valid_opts}" + ) + sys.exit(error_msg) - hostname = os.getenv("HOSTNAME") - logname = os.getenv("LOGNAME") - logger.info("hostname is %s", hostname) - logger.info("logname is %s", logname) +def get_file_paths( + args, + start_year, + end_year, + ssp_rcp, + res, + pft_years, + run_type, + rawdata_files, + num_pft, + must_run_download_input_data, +): + """ + Get various file paths + """ + if run_type == "transient": + landuse_fname, must_run_download_input_data = handle_transient_run( + start_year, end_year, ssp_rcp, rawdata_files, num_pft, must_run_download_input_data + ) + print(f"Successfully created input landuse file {landuse_fname}") + else: + landuse_fname = "" + time_stamp = datetime.today().strftime("%y%m%d") if ssp_rcp == "none": - if int(start_year) > 2015: - error_msg = ( - "ERROR: if start-year > 2015 must add an --ssp_rcp " - "argument that is not none: valid opts for ssp-rcp " - f"are {valid_opts}" - ) - sys.exit(error_msg) - elif int(end_year) > 2015: - error_msg = ( - "ERROR: if end-year > 2015 must add an --ssp-rcp " - "argument that is not none: valid opts for ssp-rcp " - f"are {valid_opts}" - ) - sys.exit(error_msg) + if pft_years == "PtVg": + ssp_rcp_name = "PtVeg_nourb" + else: + ssp_rcp_name = "hist" + else: + ssp_rcp_name = ssp_rcp + if int(end_year) == int(start_year): + fdyndat = "" + else: + fdyndat = ( + f"landuse.timeseries_{res}_{ssp_rcp_name}" + f"_{start_year}-{end_year}_{num_pft}pfts_c{time_stamp}.nc" + ) - pft_years_ssp = "-999" + prefix = f"surfdata_{res}_{ssp_rcp_name}_{start_year}_{num_pft}pfts_c{time_stamp}." - # determine pft_years - needed to parse xml file + if args.namelist_fname is None: + nlfname = f"{prefix}namelist" + else: + nlfname = args.namelist_fname + + fsurdat = f"{prefix}nc" + fsurlog = f"{prefix}log" + return landuse_fname, fdyndat, nlfname, fsurdat, fsurlog, must_run_download_input_data + + +def determine_pft_years(start_year, end_year, potveg): + """ + determine pft_years - needed to parse xml file + """ + pft_years_ssp = "-999" if potveg: pft_years = "PtVg" elif int(start_year) == 1850 and int(end_year) == 1850: @@ -388,34 +566,247 @@ def main(): sys.exit(error_msg) logger.info("pft_years = %s", pft_years) - - # Create land-use txt file for a transient case. - # Determine the run type and if a transient run create output landuse txt file - if end_year > start_year: - run_type = "transient" + return pft_years_ssp, pft_years + + +def write_nml_outdata( + nosurfdata_flag, + vic_flag, + inlandwet, + glc_flag, + hostname, + logname, + num_pft, + fdyndat, + fsurdat, + fsurlog, + gitdescribe, + nlfile, +): + """ + Write output namelist file: output data + """ + # ------------------- + # output data files + # ------------------- + if nosurfdata_flag: + nlfile.write(" fsurdat = ' ' \n") else: - run_type = "timeslice" - logger.info("run_type = %s", run_type) + nlfile.write(f" fsurdat = '{fsurdat}'\n") + nlfile.write(f" fsurlog = '{fsurlog}' \n") + nlfile.write(f" fdyndat = '{fdyndat}' \n") + + # ------------------- + # output data logicals + # ------------------- + nlfile.write(f" numpft = {num_pft} \n") + nlfile.write(f" no_inlandwet = .{str(not inlandwet).lower()}. \n") + nlfile.write(f" outnc_3dglc = .{str(glc_flag).lower()}. \n") + nlfile.write(f" outnc_vic = .{str(vic_flag).lower()}. \n") + nlfile.write(" outnc_large_files = .false. \n") + nlfile.write(" outnc_double = .true. \n") + nlfile.write(f" logname = '{logname}' \n") + nlfile.write(f" hostname = '{hostname}' \n") + nlfile.write(f" gitdescribe = '{gitdescribe}' \n") + + +def write_nml_rawinput( + start_year, + force_model_mesh_file, + force_model_mesh_nx, + force_model_mesh_ny, + vic_flag, + rawdata_files, + landuse_fname, + mksrf_ftopostats_override, + nlfile, + must_run_download_input_data, +): + """ + Write output namelist file: raw input data + """ + # pylint: disable=too-many-statements + if force_model_mesh_file is None: + mksrf_fgrid_mesh_nx = rawdata_files["mksrf_fgrid_mesh_nx"] + mksrf_fgrid_mesh_ny = rawdata_files["mksrf_fgrid_mesh_ny"] + mksrf_fgrid_mesh = rawdata_files["mksrf_fgrid_mesh"] + else: + mksrf_fgrid_mesh_nx = force_model_mesh_nx + mksrf_fgrid_mesh_ny = force_model_mesh_ny + mksrf_fgrid_mesh = force_model_mesh_file + nlfile.write(f" mksrf_fgrid_mesh = '{mksrf_fgrid_mesh}' \n") + nlfile.write(f" mksrf_fgrid_mesh_nx = {mksrf_fgrid_mesh_nx} \n") + nlfile.write(f" mksrf_fgrid_mesh_ny = {mksrf_fgrid_mesh_ny} \n") + + for key, value in rawdata_files.items(): + if key == "mksrf_ftopostats" and mksrf_ftopostats_override != "": + nlfile.write(f" mksrf_ftopostats_override = '{mksrf_ftopostats_override}' \n") + elif "_fvic" not in key and "mksrf_fvegtyp" not in key and "mksrf_fgrid" not in key: + # write everything else + nlfile.write(f" {key} = '{value}' \n") + + if start_year <= 2015: + mksrf_fvegtyp = rawdata_files["mksrf_fvegtyp"] + mksrf_fvegtyp_mesh = rawdata_files["mksrf_fvegtyp_mesh"] + mksrf_fhrvtyp = rawdata_files["mksrf_fvegtyp"] + mksrf_fhrvtyp_mesh = rawdata_files["mksrf_fvegtyp_mesh"] + mksrf_fpctlak = rawdata_files["mksrf_fvegtyp_lake"] + mksrf_furban = rawdata_files["mksrf_fvegtyp_urban"] + else: + mksrf_fvegtyp = rawdata_files["mksrf_fvegtyp_ssp"] + mksrf_fvegtyp_mesh = rawdata_files["mksrf_fvegtyp_ssp_mesh"] + mksrf_fhrvtyp = rawdata_files["mksrf_fvegtyp_ssp"] + mksrf_fhrvtyp_mesh = rawdata_files["mksrf_fvegtyp_ssp_mesh"] + mksrf_fpctlak = rawdata_files["mksrf_fvegtyp_ssp_lake"] + mksrf_furban = rawdata_files["mksrf_fvegtyp_ssp_urban"] + if "%y" in mksrf_fvegtyp: + mksrf_fvegtyp = mksrf_fvegtyp.replace("%y", str(start_year)) + if "%y" in mksrf_fhrvtyp: + mksrf_fhrvtyp = mksrf_fhrvtyp.replace("%y", str(start_year)) + if "%y" in mksrf_fpctlak: + mksrf_fpctlak = mksrf_fpctlak.replace("%y", str(start_year)) + if "%y" in mksrf_furban: + mksrf_furban = mksrf_furban.replace("%y", str(start_year)) + if not os.path.isfile(mksrf_fvegtyp): + print("WARNING: input mksrf_fvegtyp file " f"{mksrf_fvegtyp} does not exist") + print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") + must_run_download_input_data = True + if not os.path.isfile(mksrf_fhrvtyp): + print("WARNING: input mksrf_fhrvtyp file " f"{mksrf_fhrvtyp} does not exist") + print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") + must_run_download_input_data = True + if not os.path.isfile(mksrf_fpctlak): + print("WARNING: input mksrf_fpctlak file " f"{mksrf_fpctlak} does not exist") + print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") + must_run_download_input_data = True + if not os.path.isfile(mksrf_furban): + print("WARNING: input mksrf_furban file " f"{mksrf_furban} does not exist") + print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") + must_run_download_input_data = True + nlfile.write(f" mksrf_fvegtyp = '{mksrf_fvegtyp}' \n") + nlfile.write(f" mksrf_fvegtyp_mesh = '{mksrf_fvegtyp_mesh}' \n") + nlfile.write(f" mksrf_fhrvtyp = '{mksrf_fhrvtyp}' \n") + nlfile.write(f" mksrf_fhrvtyp_mesh = '{mksrf_fhrvtyp_mesh}' \n") + nlfile.write(f" mksrf_fpctlak = '{mksrf_fpctlak}' \n") + nlfile.write(f" mksrf_furban = '{mksrf_furban}' \n") + + if vic_flag: + mksrf_fvic = rawdata_files["mksrf_fvic"] + nlfile.write(f" mksrf_fvic = '{mksrf_fvic}' \n") + mksrf_fvic_mesh = rawdata_files["mksrf_fvic_mesh"] + nlfile.write(f" mksrf_fvic_mesh = '{mksrf_fvic_mesh}' \n") + + nlfile.write(f" mksrf_fdynuse = '{landuse_fname} ' \n") + return must_run_download_input_data + + +def handle_transient_run( + start_year, end_year, ssp_rcp, rawdata_files, num_pft, must_run_download_input_data +): + """ + Settings and printout for when run_type is "transient" + """ + if ssp_rcp == "none": + landuse_fname = f"landuse_timeseries_hist_{start_year}-{end_year}_{num_pft}pfts.txt" + else: + landuse_fname = f"landuse_timeseries_{ssp_rcp}_{start_year}-{end_year}_{num_pft}pfts.txt" + + with open(landuse_fname, "w", encoding="utf-8") as landuse_file: + for year in range(start_year, end_year + 1): + year_str = str(year) + if year <= 2015: + file1 = rawdata_files["mksrf_fvegtyp"] + file2 = rawdata_files["mksrf_fvegtyp_urban"] + file3 = rawdata_files["mksrf_fvegtyp_lake"] + else: + file1 = rawdata_files["mksrf_fvegtyp_ssp"] + file2 = rawdata_files["mksrf_fvegtyp_ssp_urban"] + file3 = rawdata_files["mksrf_fvegtyp_ssp_lake"] + + landuse_input_fname = file1.replace("%y", year_str) + landuse_input_fnam2 = file2.replace("%y", year_str) + landuse_input_fnam3 = file3.replace("%y", year_str) + if not os.path.isfile(landuse_input_fname): + print("WARNING: landunit_input_fname: " f"{landuse_input_fname} does not exist") + print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") + must_run_download_input_data = True + if not os.path.isfile(landuse_input_fnam2): + print("WARNING: landunit_input_fnam2: " f"{landuse_input_fnam2} does not exist") + print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") + must_run_download_input_data = True + if not os.path.isfile(landuse_input_fnam3): + print("WARNING: landunit_input_fnam3: " f"{landuse_input_fnam3} does not exist") + print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") + must_run_download_input_data = True + + # -- Each line is written twice in the original perl code: + landuse_line = f"{landuse_input_fname:<196}{year_str}\n" + landuse_lin2 = f"{landuse_input_fnam2:<196}{year_str}\n" + landuse_lin3 = f"{landuse_input_fnam3:<196}{year_str}\n" + landuse_file.write(landuse_line) + landuse_file.write(landuse_line) + landuse_file.write(landuse_lin2) + landuse_file.write(landuse_lin3) + logger.debug("year : %s", year_str) + logger.debug(landuse_line) + return landuse_fname, must_run_download_input_data + + +def determine_output_mesh(res, force_model_mesh_file, input_path, rawdata_files, tool_path): + """ + determine output mesh + """ + xml_path = os.path.join(tool_path, "../../ccs_config/component_grids_nuopc.xml") + tree2 = ET.parse(xml_path) + root = tree2.getroot() + model_mesh = "" + for child1 in root: # this is domain tag + for _, value in child1.attrib.items(): + if value == res: + for child2 in child1: + if child2.tag == "mesh": + model_mesh = child2.text + rawdata_files["mksrf_fgrid_mesh"] = os.path.join( + input_path, model_mesh.strip("$DIN_LOC_ROOT/") + ) + if child2.tag == "nx": + rawdata_files["mksrf_fgrid_mesh_nx"] = child2.text + if child2.tag == "ny": + rawdata_files["mksrf_fgrid_mesh_ny"] = child2.text - # error check on glc_nec - if (glc_nec <= 0) or (glc_nec >= 100): - raise argparse.ArgumentTypeError("ERROR: glc_nec must be between 1 and 99.") + if not model_mesh and force_model_mesh_file is None: + valid_grids = [] + for child1 in root: # this is domain tag + for _, value in child1.attrib.items(): + valid_grids.append(value) + if res in valid_grids: + error_msg = ( + "ERROR: You have requested a valid grid for which " + "../../ccs_config/component_grids_nuopc.xml does not include a mesh " + "file. For a regular regional or 1x1 grid, you may generate the " + "fsurdat file using the subset_data tool instead. Alternatively " + "and definitely for curvilinear grids, you may generate " + "a mesh file using the workflow currently (2022/7) described in " + "https://github.com/ESCOMP/CTSM/issues/1773#issuecomment-1163432584" + "TODO Reminder to ultimately place these workflow instructions in " + "the User's Guide." + ) + sys.exit(error_msg) + else: + error_msg = f"ERROR: invalid input res {res}; " f"valid grid values are {valid_grids}" + sys.exit(error_msg) - # create attribute list for parsing xml file - attribute_list = { - "hires_pft": hires_pft, - "hires_soitex": hires_soitex, - "pft_years": pft_years, - "pft_years_ssp": pft_years_ssp, - "ssp_rcp": ssp_rcp, - "res": res, - } + +def determine_input_rawdata(start_year, input_path, attribute_list): + """ + determine input rawdata + """ + # pylint: disable=too-many-statements # create dictionary for raw data files names rawdata_files = {} - # determine input rawdata - _must_run_download_input_data = False + must_run_download_input_data = False tool_path = os.path.join(path_to_ctsm_root(), "tools", "mksurfdata_esmf") xml_path = os.path.join(tool_path, "gen_mksurfdata_namelist.xml") tree1 = ET.parse(xml_path) @@ -470,7 +861,7 @@ def main(): print( "WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES" ) - _must_run_download_input_data = True + must_run_download_input_data = True if item.tag == "mesh_filename": new_key = f"{child1.tag}_mesh" @@ -478,7 +869,7 @@ def main(): if not os.path.isfile(rawdata_files[new_key]): print("WARNING: input mesh file " f"{rawdata_files[new_key]} does not exist") print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") - _must_run_download_input_data = True + must_run_download_input_data = True if item.tag == "lake_filename": new_key = f"{child1.tag}_lake" @@ -491,271 +882,47 @@ def main(): if item.tag == "lookup_filename": new_key = f"{child1.tag}_lookup" rawdata_files[new_key] = os.path.join(input_path, item.text) + return tool_path, must_run_download_input_data, rawdata_files - # determine output mesh - xml_path = os.path.join(tool_path, "../../ccs_config/component_grids_nuopc.xml") - tree2 = ET.parse(xml_path) - root = tree2.getroot() - model_mesh = "" - for child1 in root: # this is domain tag - for _, value in child1.attrib.items(): - if value == res: - for child2 in child1: - if child2.tag == "mesh": - model_mesh = child2.text - rawdata_files["mksrf_fgrid_mesh"] = os.path.join( - input_path, model_mesh.strip("$DIN_LOC_ROOT/") - ) - if child2.tag == "nx": - rawdata_files["mksrf_fgrid_mesh_nx"] = child2.text - if child2.tag == "ny": - rawdata_files["mksrf_fgrid_mesh_ny"] = child2.text - if not model_mesh and force_model_mesh_file is None: - valid_grids = [] - for child1 in root: # this is domain tag - for _, value in child1.attrib.items(): - valid_grids.append(value) - if res in valid_grids: +def open_mesh_file(force_model_mesh_file, force_model_mesh_nx, force_model_mesh_ny): + """ + open mesh_file to read element_count and, if available, orig_grid_dims + """ + # pylint: disable=no-name-in-module,no-member + # The above "pylint: disable" is because pylint complains that netCDF4 + # has no member Dataset, even though it does. + mesh_file = netCDF4.Dataset(force_model_mesh_file, "r") + element_count = mesh_file.dimensions["elementCount"].size + if "origGridDims" in mesh_file.variables: + orig_grid_dims = mesh_file.variables["origGridDims"] + if ( + int(force_model_mesh_nx) == orig_grid_dims[0] + and int(force_model_mesh_ny) == orig_grid_dims[1] + ): + mesh_file.close() + else: error_msg = ( - "ERROR: You have requested a valid grid for which " - "../../ccs_config/component_grids_nuopc.xml does not include a mesh " - "file. For a regular regional or 1x1 grid, you may generate the " - "fsurdat file using the subset_data tool instead. Alternatively " - "and definitely for curvilinear grids, you may generate " - "a mesh file using the workflow currently (2022/7) described in " - "https://github.com/ESCOMP/CTSM/issues/1773#issuecomment-1163432584" - "TODO Reminder to ultimately place these workflow instructions in " - "the User's Guide." + "ERROR: Found variable origGridDims in " + f"{force_model_mesh_file} with values " + f"{orig_grid_dims[:]} that do not agree with the " + "user-entered mesh_nx and mesh_ny values of " + f"{[force_model_mesh_nx, force_model_mesh_ny]}." ) sys.exit(error_msg) - else: - error_msg = f"ERROR: invalid input res {res}; " f"valid grid values are {valid_grids}" - sys.exit(error_msg) - - # Determine num_pft - if nocrop_flag: - num_pft = "16" - else: - num_pft = "78" - logger.info("num_pft is %s", num_pft) - - # Write out if surface dataset will be created - if nosurfdata_flag: - logger.info("surface dataset will not be created") - else: - logger.info("surface dataset will be created") - - if run_type == "transient": - if ssp_rcp == "none": - landuse_fname = f"landuse_timeseries_hist_{start_year}-{end_year}_{num_pft}pfts.txt" - else: - landuse_fname = ( - f"landuse_timeseries_{ssp_rcp}_{start_year}-{end_year}_{num_pft}pfts.txt" - ) - - with open(landuse_fname, "w", encoding="utf-8") as landuse_file: - for year in range(start_year, end_year + 1): - year_str = str(year) - if year <= 2015: - file1 = rawdata_files["mksrf_fvegtyp"] - file2 = rawdata_files["mksrf_fvegtyp_urban"] - file3 = rawdata_files["mksrf_fvegtyp_lake"] - else: - file1 = rawdata_files["mksrf_fvegtyp_ssp"] - file2 = rawdata_files["mksrf_fvegtyp_ssp_urban"] - file3 = rawdata_files["mksrf_fvegtyp_ssp_lake"] - - landuse_input_fname = file1.replace("%y", year_str) - landuse_input_fnam2 = file2.replace("%y", year_str) - landuse_input_fnam3 = file3.replace("%y", year_str) - if not os.path.isfile(landuse_input_fname): - print("WARNING: landunit_input_fname: " f"{landuse_input_fname} does not exist") - print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") - _must_run_download_input_data = True - if not os.path.isfile(landuse_input_fnam2): - print("WARNING: landunit_input_fnam2: " f"{landuse_input_fnam2} does not exist") - print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") - _must_run_download_input_data = True - if not os.path.isfile(landuse_input_fnam3): - print("WARNING: landunit_input_fnam3: " f"{landuse_input_fnam3} does not exist") - print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") - _must_run_download_input_data = True - - # -- Each line is written twice in the original perl code: - landuse_line = f"{landuse_input_fname:<196}{year_str}\n" - landuse_lin2 = f"{landuse_input_fnam2:<196}{year_str}\n" - landuse_lin3 = f"{landuse_input_fnam3:<196}{year_str}\n" - landuse_file.write(landuse_line) - landuse_file.write(landuse_line) - landuse_file.write(landuse_lin2) - landuse_file.write(landuse_lin3) - logger.debug("year : %s", year_str) - logger.debug(landuse_line) - print(f"Successfully created input landuse file {landuse_fname}") - else: - landuse_fname = "" - - time_stamp = datetime.today().strftime("%y%m%d") - if ssp_rcp == "none": - if pft_years == "PtVg": - ssp_rcp_name = "PtVeg_nourb" - else: - ssp_rcp_name = "hist" - else: - ssp_rcp_name = ssp_rcp - if int(end_year) == int(start_year): - fdyndat = "" - else: - fdyndat = ( - f"landuse.timeseries_{res}_{ssp_rcp_name}" - f"_{start_year}-{end_year}_{num_pft}pfts_c{time_stamp}.nc" + elif force_model_mesh_nx is None or force_model_mesh_ny is None: + error_msg = ( + "ERROR: You set --model-mesh so you MUST ALSO " + "SET --model-mesh-nx AND --model-mesh-ny" ) + sys.exit(error_msg) - prefix = f"surfdata_{res}_{ssp_rcp_name}_{start_year}_{num_pft}pfts_c{time_stamp}." - - if args.namelist_fname is None: - nlfname = f"{prefix}namelist" - else: - nlfname = args.namelist_fname - - fsurdat = f"{prefix}nc" - fsurlog = f"{prefix}log" - - git_desc_cmd = f"git -C {tool_path} describe" - try: - # The "git -C" option permits a system test to run this tool from - # elsewhere while running the git command from the tool_path - gitdescribe = subprocess.check_output(git_desc_cmd, shell=True).strip() - except subprocess.CalledProcessError as error: - # In case the "git -C" option is unavailable, as on casper (2022/5/24) - # Still, this does NOT allow the system test to work on machines - # without git -C - logger.info("git -C option unavailable on casper as of 2022/7/2 %s", error) - gitdescribe = subprocess.check_output("git describe", shell=True).strip() - gitdescribe = gitdescribe.decode("utf-8") - - # The below two overrides are only used for testing and validation - # it takes a long time to generate the mapping files - # from 1km to the following two resolutions since the output mesh has so few points - if res == "10x15": - mksrf_ftopostats_override = os.path.join( - input_path, "lnd", "clm2", "rawdata", "surfdata_topo_10x15_c220303.nc" + # using force_model_mesh_nx and force_model_mesh_ny either from the + # mesh file (see previous if statement) or the user-entered values + if element_count != int(force_model_mesh_nx) * int(force_model_mesh_ny): + error_msg = ( + "ERROR: The product of " + "--model-mesh-nx x --model-mesh-ny must equal " + "exactly elementCount in --model-mesh" ) - logger.info("will override mksrf_ftopostats with = %s", mksrf_ftopostats_override) - else: - mksrf_ftopostats_override = "" - - # ---------------------------------------- - # Write output namelist file - # ---------------------------------------- - - with open(nlfname, "w", encoding="utf-8") as nlfile: - nlfile.write("&mksurfdata_input \n") - - # ------------------- - # raw input data - # ------------------- - if force_model_mesh_file is None: - mksrf_fgrid_mesh_nx = rawdata_files["mksrf_fgrid_mesh_nx"] - mksrf_fgrid_mesh_ny = rawdata_files["mksrf_fgrid_mesh_ny"] - mksrf_fgrid_mesh = rawdata_files["mksrf_fgrid_mesh"] - else: - mksrf_fgrid_mesh_nx = force_model_mesh_nx - mksrf_fgrid_mesh_ny = force_model_mesh_ny - mksrf_fgrid_mesh = force_model_mesh_file - nlfile.write(f" mksrf_fgrid_mesh = '{mksrf_fgrid_mesh}' \n") - nlfile.write(f" mksrf_fgrid_mesh_nx = {mksrf_fgrid_mesh_nx} \n") - nlfile.write(f" mksrf_fgrid_mesh_ny = {mksrf_fgrid_mesh_ny} \n") - - for key, value in rawdata_files.items(): - if key == "mksrf_ftopostats" and mksrf_ftopostats_override != "": - nlfile.write(f" mksrf_ftopostats_override = '{mksrf_ftopostats_override}' \n") - elif "_fvic" not in key and "mksrf_fvegtyp" not in key and "mksrf_fgrid" not in key: - # write everything else - nlfile.write(f" {key} = '{value}' \n") - - if start_year <= 2015: - mksrf_fvegtyp = rawdata_files["mksrf_fvegtyp"] - mksrf_fvegtyp_mesh = rawdata_files["mksrf_fvegtyp_mesh"] - mksrf_fhrvtyp = rawdata_files["mksrf_fvegtyp"] - mksrf_fhrvtyp_mesh = rawdata_files["mksrf_fvegtyp_mesh"] - mksrf_fpctlak = rawdata_files["mksrf_fvegtyp_lake"] - mksrf_furban = rawdata_files["mksrf_fvegtyp_urban"] - else: - mksrf_fvegtyp = rawdata_files["mksrf_fvegtyp_ssp"] - mksrf_fvegtyp_mesh = rawdata_files["mksrf_fvegtyp_ssp_mesh"] - mksrf_fhrvtyp = rawdata_files["mksrf_fvegtyp_ssp"] - mksrf_fhrvtyp_mesh = rawdata_files["mksrf_fvegtyp_ssp_mesh"] - mksrf_fpctlak = rawdata_files["mksrf_fvegtyp_ssp_lake"] - mksrf_furban = rawdata_files["mksrf_fvegtyp_ssp_urban"] - if "%y" in mksrf_fvegtyp: - mksrf_fvegtyp = mksrf_fvegtyp.replace("%y", str(start_year)) - if "%y" in mksrf_fhrvtyp: - mksrf_fhrvtyp = mksrf_fhrvtyp.replace("%y", str(start_year)) - if "%y" in mksrf_fpctlak: - mksrf_fpctlak = mksrf_fpctlak.replace("%y", str(start_year)) - if "%y" in mksrf_furban: - mksrf_furban = mksrf_furban.replace("%y", str(start_year)) - if not os.path.isfile(mksrf_fvegtyp): - print("WARNING: input mksrf_fvegtyp file " f"{mksrf_fvegtyp} does not exist") - print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") - _must_run_download_input_data = True - if not os.path.isfile(mksrf_fhrvtyp): - print("WARNING: input mksrf_fhrvtyp file " f"{mksrf_fhrvtyp} does not exist") - print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") - _must_run_download_input_data = True - if not os.path.isfile(mksrf_fpctlak): - print("WARNING: input mksrf_fpctlak file " f"{mksrf_fpctlak} does not exist") - print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") - _must_run_download_input_data = True - if not os.path.isfile(mksrf_furban): - print("WARNING: input mksrf_furban file " f"{mksrf_furban} does not exist") - print("WARNING: run ./download_input_data to try TO " "OBTAIN MISSING FILES") - _must_run_download_input_data = True - nlfile.write(f" mksrf_fvegtyp = '{mksrf_fvegtyp}' \n") - nlfile.write(f" mksrf_fvegtyp_mesh = '{mksrf_fvegtyp_mesh}' \n") - nlfile.write(f" mksrf_fhrvtyp = '{mksrf_fhrvtyp}' \n") - nlfile.write(f" mksrf_fhrvtyp_mesh = '{mksrf_fhrvtyp_mesh}' \n") - nlfile.write(f" mksrf_fpctlak = '{mksrf_fpctlak}' \n") - nlfile.write(f" mksrf_furban = '{mksrf_furban}' \n") - - if vic_flag: - mksrf_fvic = rawdata_files["mksrf_fvic"] - nlfile.write(f" mksrf_fvic = '{mksrf_fvic}' \n") - mksrf_fvic_mesh = rawdata_files["mksrf_fvic_mesh"] - nlfile.write(f" mksrf_fvic_mesh = '{mksrf_fvic_mesh}' \n") - - nlfile.write(f" mksrf_fdynuse = '{landuse_fname} ' \n") - - # ------------------- - # output data files - # ------------------- - if nosurfdata_flag: - nlfile.write(" fsurdat = ' ' \n") - else: - nlfile.write(f" fsurdat = '{fsurdat}'\n") - nlfile.write(f" fsurlog = '{fsurlog}' \n") - nlfile.write(f" fdyndat = '{fdyndat}' \n") - - # ------------------- - # output data logicals - # ------------------- - nlfile.write(f" numpft = {num_pft} \n") - nlfile.write(f" no_inlandwet = .{str(not inlandwet).lower()}. \n") - nlfile.write(f" outnc_3dglc = .{str(glc_flag).lower()}. \n") - nlfile.write(f" outnc_vic = .{str(vic_flag).lower()}. \n") - nlfile.write(" outnc_large_files = .false. \n") - nlfile.write(" outnc_double = .true. \n") - nlfile.write(f" logname = '{logname}' \n") - nlfile.write(f" hostname = '{hostname}' \n") - nlfile.write(f" gitdescribe = '{gitdescribe}' \n") - - nlfile.write("/ \n") - - if _must_run_download_input_data: - temp_nlfname = "surfdata.namelist" - os.rename(nlfname, temp_nlfname) - nlfname = temp_nlfname - - print(f"Successfully created input namelist file {nlfname}") + sys.exit(error_msg)