Ejemplo n.º 1
0
def delete_crontab_line(called_from_cron):
    """ Delete crontab line after job is complete i.e. either SUCCESS/FAILURE
    but not IN PROGRESS status"""

    print_input_args(locals())

    #import selected env vars
    IMPORTS = ["MACHINE", "USER", "CRONTAB_LINE"]
    import_vars(env_vars=IMPORTS)

    #
    # Get the full contents of the user's cron table.
    #
    (crontab_cmd, crontab_contents) = get_crontab_contents(called_from_cron)
    #
    # Remove the line in the contents of the cron table corresponding to the
    # current forecast experiment (if that line is part of the contents).
    # Then record the results back into the user's cron table.
    #
    if (CRONTAB_LINE + '\n') in crontab_contents:
        crontab_contents = crontab_contents.replace(CRONTAB_LINE + '\n', '')
    else:
        crontab_contents = crontab_contents.replace(CRONTAB_LINE, '')

    run_command(f'''echo '{crontab_contents}' | {crontab_cmd}''')
Ejemplo n.º 2
0
def create_diag_table_file(run_dir):
    """ Creates a diagnostic table file for each cycle to be run

    Args:
        run_dir: run directory
    Returns:
        Boolean
    """

    print_input_args(locals())

    #import all environment variables
    import_vars()

    #create a diagnostic table file within the specified run directory
    print_info_msg(f'''
        Creating a diagnostics table file (\"{DIAG_TABLE_FN}\") in the specified
        run directory...
        
          run_dir = \"{run_dir}\"''',
                   verbose=VERBOSE)

    diag_table_fp = os.path.join(run_dir, DIAG_TABLE_FN)

    print_info_msg(f'''
        
        Using the template diagnostics table file:
        
            diag_table_tmpl_fp = {DIAG_TABLE_TMPL_FP}
        
        to create:
        
            diag_table_fp = \"{diag_table_fp}\"''',
                   verbose=VERBOSE)

    settings = {'starttime': CDATE, 'cres': CRES}
    settings_str = cfg_to_yaml_str(settings)

    #call fill jinja
    try:
        fill_jinja_template([
            "-q", "-u", settings_str, "-t", DIAG_TABLE_TMPL_FP, "-o",
            diag_table_fp
        ])
    except:
        print_err_msg_exit(f'''
            !!!!!!!!!!!!!!!!!
            
            fill_jinja_template.py failed!
            
            !!!!!!!!!!!!!!!!!''')
        return False
    return True
Ejemplo n.º 3
0
def get_crontab_contents(called_from_cron):
    """
    #-----------------------------------------------------------------------
    #
    # This function returns the contents of the user's 
    # cron table as well as the command to use to manipulate the cron table
    # (i.e. the "crontab" command, but on some platforms the version or 
    # location of this may change depending on other circumstances, e.g. on
    # Cheyenne, this depends on whether a script that wants to call "crontab"
    # is itself being called from a cron job).  Arguments are as follows:
    #
    # called_from_cron:
    # Boolean flag that specifies whether this function (and the scripts or
    # functions that are calling it) are called as part of a cron job.  Must
    # be set to "TRUE" or "FALSE".
    #
    # outvarname_crontab_cmd:
    # Name of the output variable that will contain the command to issue for
    # the system "crontab" command.
    #
    # outvarname_crontab_contents:
    # Name of the output variable that will contain the contents of the 
    # user's cron table.
    # 
    #-----------------------------------------------------------------------
    """

    print_input_args(locals())

    #import selected env vars
    IMPORTS = ["MACHINE", "USER"]
    import_vars(env_vars=IMPORTS)

    __crontab_cmd__ = "crontab"
    #
    # On Cheyenne, simply typing "crontab" will launch the crontab command
    # at "/glade/u/apps/ch/opt/usr/bin/crontab".  This is a containerized
    # version of crontab that will work if called from scripts that are
    # themselves being called as cron jobs.  In that case, we must instead
    # call the system version of crontab at /usr/bin/crontab.
    #
    if MACHINE == "CHEYENNE":
        if called_from_cron:
            __crontab_cmd__ = "/usr/bin/crontab"
    (_, __crontab_contents__, _) = run_command(f'''{__crontab_cmd__} -l''')

    # replace single quotes (hopefully in comments) with double quotes
    __crontab_contents__ = __crontab_contents__.replace("'", '"')

    return __crontab_cmd__, __crontab_contents__
def check_ruc_lsm(ccpp_phys_suite_fp):
    """ This file defines a function that checks whether the RUC land surface
    model (LSM) parameterization is being called by the selected physics suite.

    Args:
        ccpp_phys_suite_fp: full path to CCPP physics suite xml file
    Returns:
        Boolean
    """

    print_input_args(locals())

    tree = load_xml_file(ccpp_phys_suite_fp)
    has_ruc = has_tag_with_value(tree, "scheme", "lsm_ruc")
    return has_ruc
def set_cycle_dates(date_start, date_end, cycle_hrs, incr_cycl_freq):
    """ This file defines a function that, given the starting date (date_start, 
    in the form YYYYMMDD), the ending date (date_end, in the form YYYYMMDD), 
    and an array containing the cycle hours for each day (whose elements 
    have the form HH), returns an array of cycle date-hours whose elements
    have the form YYYYMMDD.  Here, YYYY is a four-digit year, MM is a two-
    digit month, DD is a two-digit day of the month, and HH is a two-digit
    hour of the day.

    Args:
        date_start: start date
        date_end: end date
        cycle_hrs: [ HH0, HH1, ...]
        incr_cycl_freq: cycle frequency increment in hours
    Returns:
        A list of dates in a format YYYYMMDDHH
    """

    print_input_args(locals())

    #calculate date increment
    if incr_cycl_freq <= 24:
        incr_days = 1
    else:
        incr_days = incr_cycl_freq // 24
        if incr_cycl_freq % 24 != 0:
            print_err_msg_exit(f'''
                INCR_CYCL_FREQ is not divided by 24:
                  INCR_CYCL_FREQ = \"{incr_cycl_freq}\"''')

    #iterate over days and cycles
    all_cdates = []
    d = date_start
    while d <= date_end:
        for c in cycle_hrs:
            dc = d + timedelta(hours=c)
            v = datetime.strftime(dc, '%Y%m%d%H')
            all_cdates.append(v)
        d += timedelta(days=incr_days)

    return all_cdates
def create_model_configure_file(cdate, run_dir, sub_hourly_post,
                                dt_subhourly_post_mnts, dt_atmos):
    """ Creates a model configuration file in the specified
    run directory

    Args:
        cdate: cycle date
        run_dir: run directory
        sub_hourly_post
        dt_subhourly_post_mnts
        dt_atmos
    Returns:
        Boolean
    """

    print_input_args(locals())

    #import all environment variables
    import_vars()

    #
    #-----------------------------------------------------------------------
    #
    # Create a model configuration file in the specified run directory.
    #
    #-----------------------------------------------------------------------
    #
    print_info_msg(f'''
        Creating a model configuration file (\"{MODEL_CONFIG_FN}\") in the specified
        run directory (run_dir):
          run_dir = \"{run_dir}\"''',
                   verbose=VERBOSE)
    #
    # Extract from cdate the starting year, month, day, and hour of the forecast.
    #
    yyyy = cdate.year
    mm = cdate.month
    dd = cdate.day
    hh = cdate.hour
    #
    # Set parameters in the model configure file.
    #
    dot_quilting_dot = f".{lowercase(str(QUILTING))}."
    dot_print_esmf_dot = f".{lowercase(str(PRINT_ESMF))}."
    dot_cpl_dot = f".{lowercase(str(CPL))}."
    dot_write_dopost = f".{lowercase(str(WRITE_DOPOST))}."
    #
    #-----------------------------------------------------------------------
    #
    # Create a multiline variable that consists of a yaml-compliant string
    # specifying the values that the jinja variables in the template
    # model_configure file should be set to.
    #
    #-----------------------------------------------------------------------
    #
    settings = {
        'PE_MEMBER01': PE_MEMBER01,
        'print_esmf': dot_print_esmf_dot,
        'start_year': yyyy,
        'start_month': mm,
        'start_day': dd,
        'start_hour': hh,
        'nhours_fcst': FCST_LEN_HRS,
        'dt_atmos': DT_ATMOS,
        'cpl': dot_cpl_dot,
        'atmos_nthreads': OMP_NUM_THREADS_RUN_FCST,
        'restart_interval': RESTART_INTERVAL,
        'write_dopost': dot_write_dopost,
        'quilting': dot_quilting_dot,
        'output_grid': WRTCMP_output_grid
    }
    #
    # If the write-component is to be used, then specify a set of computational
    # parameters and a set of grid parameters.  The latter depends on the type
    # (coordinate system) of the grid that the write-component will be using.
    #
    if QUILTING:
        settings.update({
            'write_groups': WRTCMP_write_groups,
            'write_tasks_per_group': WRTCMP_write_tasks_per_group,
            'cen_lon': WRTCMP_cen_lon,
            'cen_lat': WRTCMP_cen_lat,
            'lon1': WRTCMP_lon_lwr_left,
            'lat1': WRTCMP_lat_lwr_left
        })

        if WRTCMP_output_grid == "lambert_conformal":
            settings.update({
                'stdlat1': WRTCMP_stdlat1,
                'stdlat2': WRTCMP_stdlat2,
                'nx': WRTCMP_nx,
                'ny': WRTCMP_ny,
                'dx': WRTCMP_dx,
                'dy': WRTCMP_dy,
                'lon2': "",
                'lat2': "",
                'dlon': "",
                'dlat': "",
            })
        elif WRTCMP_output_grid == "regional_latlon" or \
             WRTCMP_output_grid == "rotated_latlon":
            settings.update({
                'lon2': WRTCMP_lon_upr_rght,
                'lat2': WRTCMP_lat_upr_rght,
                'dlon': WRTCMP_dlon,
                'dlat': WRTCMP_dlat,
                'stdlat1': "",
                'stdlat2': "",
                'nx': "",
                'ny': "",
                'dx': "",
                'dy': ""
            })
    #
    # If sub_hourly_post is set to "TRUE", then the forecast model must be
    # directed to generate output files on a sub-hourly interval.  Do this
    # by specifying the output interval in the model configuration file
    # (MODEL_CONFIG_FN) in units of number of forecat model time steps (nsout).
    # nsout is calculated using the user-specified output time interval
    # dt_subhourly_post_mnts (in units of minutes) and the forecast model's
    # main time step dt_atmos (in units of seconds).  Note that nsout is
    # guaranteed to be an integer because the experiment generation scripts
    # require that dt_subhourly_post_mnts (after conversion to seconds) be
    # evenly divisible by dt_atmos.  Also, in this case, the variable output_fh
    # [which specifies the output interval in hours;
    # see the jinja model_config template file] is set to 0, although this
    # doesn't matter because any positive of nsout will override output_fh.
    #
    # If sub_hourly_post is set to "FALSE", then the workflow is hard-coded
    # (in the jinja model_config template file) to direct the forecast model
    # to output files every hour.  This is done by setting (1) output_fh to 1
    # here, and (2) nsout to -1 here which turns off output by time step interval.
    #
    # Note that the approach used here of separating how hourly and subhourly
    # output is handled should be changed/generalized/simplified such that
    # the user should only need to specify the output time interval (there
    # should be no need to specify a flag like sub_hourly_post); the workflow
    # should then be able to direct the model to output files with that time
    # interval and to direct the post-processor to process those files
    # regardless of whether that output time interval is larger than, equal
    # to, or smaller than one hour.
    #
    if sub_hourly_post:
        nsout = (dt_subhourly_post_mnts * 60) // dt_atmos
        output_fh = 0
    else:
        output_fh = 1
        nsout = -1

    settings.update({'output_fh': output_fh, 'nsout': nsout})

    settings_str = cfg_to_yaml_str(settings)

    print_info_msg(dedent(f'''
        The variable \"settings\" specifying values to be used in the \"{MODEL_CONFIG_FN}\"
        file has been set as follows:\n
        settings =\n\n''') + settings_str,
                   verbose=VERBOSE)
    #
    #-----------------------------------------------------------------------
    #
    # Call a python script to generate the experiment's actual MODEL_CONFIG_FN
    # file from the template file.
    #
    #-----------------------------------------------------------------------
    #
    model_config_fp = os.path.join(run_dir, MODEL_CONFIG_FN)

    try:
        fill_jinja_template([
            "-q", "-u", settings_str, "-t", MODEL_CONFIG_TMPL_FP, "-o",
            model_config_fp
        ])
    except:
        print_err_msg_exit(
            dedent(f'''
            Call to python script fill_jinja_template.py to create a \"{MODEL_CONFIG_FN}\"
            file from a jinja2 template failed.  Parameters passed to this script are:
              Full path to template model config file:
                MODEL_CONFIG_TMPL_FP = \"{MODEL_CONFIG_TMPL_FP}\"
              Full path to output model config file:
                model_config_fp = \"{model_config_fp}\"
              Namelist settings specified on command line:\n
                settings =\n\n''') + settings_str)
        return False

    return True
def get_crontab_contents(called_from_cron):
    """
    #-----------------------------------------------------------------------
    #
    # This function returns the contents of the user's 
    # cron table as well as the command to use to manipulate the cron table
    # (i.e. the "crontab" command, but on some platforms the version or 
    # location of this may change depending on other circumstances, e.g. on
    # Cheyenne, this depends on whether a script that wants to call "crontab"
    # is itself being called from a cron job).  Arguments are as follows:
    #
    # called_from_cron:
    # Boolean flag that specifies whether this function (and the scripts or
    # functions that are calling it) are called as part of a cron job.  Must
    # be set to "TRUE" or "FALSE".
    #
    # outvarname_crontab_cmd:
    # Name of the output variable that will contain the command to issue for
    # the system "crontab" command.
    #
    # outvarname_crontab_contents:
    # Name of the output variable that will contain the contents of the 
    # user's cron table.
    # 
    #-----------------------------------------------------------------------
    """

    print_input_args(locals())

    #import all env vars
    IMPORTS = ["MACHINE", "USER"]
    import_vars(env_vars=IMPORTS)

    #
    # Make sure called_from_cron is set to a valid value.
    #
    check_var_valid_value(called_from_cron, valid_vals_BOOLEAN)

    if MACHINE == "WCOSS_DELL_P3":
        __crontab_cmd__ = ""
        (_, __crontab_contents__,
         _) = run_command(f'''cat "/u/{USER}/cron/mycrontab"''')
    else:
        __crontab_cmd__ = "crontab"
        #
        # On Cheyenne, simply typing "crontab" will launch the crontab command
        # at "/glade/u/apps/ch/opt/usr/bin/crontab".  This is a containerized
        # version of crontab that will work if called from scripts that are
        # themselves being called as cron jobs.  In that case, we must instead
        # call the system version of crontab at /usr/bin/crontab.
        #
        if MACHINE == "CHEYENNE":
            if called_from_cron:
                __crontab_cmd__ = "/usr/bin/crontab"
        (_, __crontab_contents__, _) = run_command(f'''{__crontab_cmd__} -l''')
    #
    # On Cheyenne, the output of the "crontab -l" command contains a 3-line
    # header (comments) at the top that is not actually part of the user's
    # cron table.  This needs to be removed to avoid adding an unnecessary
    # copy of this header to the user's cron table.
    #
    if MACHINE == "CHEYENNE":
        (_, __crontab_contents__, _) = run_command(
            f'''printf "%s" "{__crontab_contents__}" | tail -n +4 ''')

    return __crontab_cmd__, __crontab_contents__
def set_FV3nml_ens_stoch_seeds(cdate):
    """
    This function, for an ensemble-enabled experiment 
    (i.e. for an experiment for which the workflow configuration variable 
    DO_ENSEMBLE has been set to "TRUE"), creates new namelist files with
    unique stochastic "seed" parameters, using a base namelist file in the 
    ${EXPTDIR} directory as a template. These new namelist files are stored 
    within each member directory housed within each cycle directory. Files 
    of any two ensemble members differ only in their stochastic "seed" 
    parameter values.  These namelist files are generated when this file is
    called as part of the RUN_FCST_TN task.  

    Args:
        cdate
    Returns:
        None
    """

    print_input_args(locals())

    # import all environment variables
    import_vars()

    #
    #-----------------------------------------------------------------------
    #
    # For a given cycle and member, generate a namelist file with unique
    # seed values.
    #
    #-----------------------------------------------------------------------
    #
    ensmem_name = f"mem{ENSMEM_INDX}"

    fv3_nml_ensmem_fp = os.path.join(
        CYCLE_BASEDIR,
        f'{date_to_str(cdate,format="%Y%m%d%H")}{os.sep}{ensmem_name}{os.sep}{FV3_NML_FN}'
    )

    ensmem_num = ENSMEM_INDX

    cdate_i = int(cdate.strftime('%Y%m%d%H'))

    settings = {}
    nam_stochy_dict = {}

    if DO_SPPT:
        iseed_sppt = cdate_i * 1000 + ensmem_num * 10 + 1
        nam_stochy_dict.update({'iseed_sppt': iseed_sppt})

    if DO_SHUM:
        iseed_shum = cdate_i * 1000 + ensmem_num * 10 + 2
        nam_stochy_dict.update({'iseed_shum': iseed_shum})

    if DO_SKEB:
        iseed_skeb = cdate_i * 1000 + ensmem_num * 10 + 3
        nam_stochy_dict.update({'iseed_skeb': iseed_skeb})

    settings['nam_stochy'] = nam_stochy_dict

    if DO_SPP:
        num_iseed_spp = len(ISEED_SPP)
        iseed_spp = [None] * num_iseed_spp
        for i in range(num_iseed_spp):
            iseed_spp[i] = cdate_i * 1000 + ensmem_num * 10 + ISEED_SPP[i]

        settings['nam_sppperts'] = {'iseed_spp': iseed_spp}
    else:
        settings['nam_sppperts'] = {}

    if DO_LSM_SPP:
        iseed_lsm_spp = cdate_i * 1000 + ensmem_num * 10 + 9

        settings['nam_sppperts'] = {'iseed_lndp': [iseed_lsm_spp]}

    settings_str = cfg_to_yaml_str(settings)

    print_info_msg(dedent(f'''
        The variable \"settings\" specifying seeds in \"{FV3_NML_FP}\"
        has been set as follows:

        settings =\n\n''') + settings_str,
                   verbose=VERBOSE)

    try:
        set_namelist([
            "-q", "-n", FV3_NML_FP, "-u", settings_str, "-o", fv3_nml_ensmem_fp
        ])
    except:
        print_err_msg_exit(
            dedent(f'''
            Call to python script set_namelist.py to set the variables in the FV3
            namelist file that specify the paths to the surface climatology files
            failed.  Parameters passed to this script are:
              Full path to base namelist file:
                FV3_NML_FP = \"{FV3_NML_FP}\"
              Full path to output namelist file:
                fv3_nml_ensmem_fp = \"{fv3_nml_ensmem_fp}\"
              Namelist settings specified on command line (these have highest precedence):\n
                settings =\n\n''') + settings_str)
Ejemplo n.º 9
0
def set_gridparams_ESGgrid(lon_ctr, lat_ctr, nx, ny, halo_width, delx, dely,
                           pazi):
    """ Sets the parameters for a grid that is to be generated using the "ESGgrid" 
    grid generation method (i.e. GRID_GEN_METHOD set to "ESGgrid").

    Args:
        lon_ctr
        lat_ctr
        nx
        ny
        halo_width
        delx
        dely
        pazi
    Returns:
        Tuple of inputs, and 4 outputs (see return statement)
    """

    print_input_args(locals())

    # get needed environment variables
    IMPORTS = ['RADIUS_EARTH', 'DEGS_PER_RADIAN']
    import_vars(env_vars=IMPORTS)
    #
    #-----------------------------------------------------------------------
    #
    # For a ESGgrid-type grid, the orography filtering is performed by pass-
    # ing to the orography filtering the parameters for an "equivalent" glo-
    # bal uniform cubed-sphere grid.  These are the parameters that a global
    # uniform cubed-sphere grid needs to have in order to have a nominal
    # grid cell size equal to that of the (average) cell size on the region-
    # al grid.  These globally-equivalent parameters include a resolution
    # (in units of number of cells in each of the two horizontal directions)
    # and a stretch factor.  The equivalent resolution is calculated in the
    # script that generates the grid, and the stretch factor needs to be set
    # to 1 because we are considering an equivalent globally UNIFORM grid.
    # However, it turns out that with a non-symmetric regional grid (one in
    # which nx is not equal to ny), setting stretch_factor to 1 fails be-
    # cause the orography filtering program is designed for a global cubed-
    # sphere grid and thus assumes that nx and ny for a given tile are equal
    # when stretch_factor is exactly equal to 1.
    # ^^-- Why is this?  Seems like symmetry btwn x and y should still hold when the stretch factor is not equal to 1.
    # It turns out that the program will work if we set stretch_factor to a
    # value that is not exactly 1.  This is what we do below.
    #
    #-----------------------------------------------------------------------
    #
    stretch_factor = 0.999  # Check whether the orography program has been fixed so that we can set this to 1...
    #
    #-----------------------------------------------------------------------
    #
    # Set parameters needed as inputs to the regional_grid grid generation
    # code.
    #
    #-----------------------------------------------------------------------
    #
    del_angle_x_sg = (delx / (2.0 * RADIUS_EARTH)) * DEGS_PER_RADIAN
    del_angle_y_sg = (dely / (2.0 * RADIUS_EARTH)) * DEGS_PER_RADIAN
    neg_nx_of_dom_with_wide_halo = -(nx + 2 * halo_width)
    neg_ny_of_dom_with_wide_halo = -(ny + 2 * halo_width)
    #
    #-----------------------------------------------------------------------
    #
    # return output variables.
    #
    #-----------------------------------------------------------------------
    #
    return (lon_ctr, lat_ctr, nx, ny, pazi, halo_width, stretch_factor,
            del_angle_x_sg, del_angle_y_sg, int(neg_nx_of_dom_with_wide_halo),
            int(neg_ny_of_dom_with_wide_halo))
Ejemplo n.º 10
0
def set_gridparams_GFDLgrid(lon_of_t6_ctr, lat_of_t6_ctr, res_of_t6g, stretch_factor,
                            refine_ratio_t6g_to_t7g, 
                            istart_of_t7_on_t6g, iend_of_t7_on_t6g,
                            jstart_of_t7_on_t6g, jend_of_t7_on_t6g):
    """ Sets the parameters for a grid that is to be generated using the "GFDLgrid" 
    grid generation method (i.e. GRID_GEN_METHOD set to "ESGgrid").

    Args:
         lon_of_t6_ctr
         lat_of_t6_ctr
         res_of_t6g
         stretch_factor
         refine_ratio_t6g_to_t7g 
         istart_of_t7_on_t6g
         iend_of_t7_on_t6g
         jstart_of_t7_on_t6g
         jend_of_t7_on_t6g):
    Returns:
        Tuple of inputs and outputs (see return statement)
    """

    print_input_args(locals())

    # get needed environment variables
    IMPORTS = ['VERBOSE', 'RUN_ENVIR', 'NH4']
    import_vars(env_vars=IMPORTS)

    #
    #-----------------------------------------------------------------------
    #
    # To simplify the grid setup, we require that tile 7 be centered on tile 
    # 6.  Note that this is not really a restriction because tile 6 can al-
    # ways be moved so that it is centered on tile 7 [the location of tile 6 
    # doesn't really matter because for a regional setup, the forecast model 
    # will only run on tile 7 (not on tiles 1-6)].
    #
    # We now check that tile 7 is centered on tile 6 by checking (1) that 
    # the number of cells (on tile 6) between the left boundaries of these 
    # two tiles is equal to that between their right boundaries and (2) that 
    # the number of cells (on tile 6) between the bottom boundaries of these
    # two tiles is equal to that between their top boundaries.  If not, we 
    # print out an error message and exit.  If so, we set the longitude and 
    # latitude of the center of tile 7 to those of tile 6 and continue.
    #
    #-----------------------------------------------------------------------
    #

    nx_of_t6_on_t6g = res_of_t6g
    ny_of_t6_on_t6g = res_of_t6g

    num_left_margin_cells_on_t6g = istart_of_t7_on_t6g - 1
    num_right_margin_cells_on_t6g = nx_of_t6_on_t6g - iend_of_t7_on_t6g

    # This if-statement can hopefully be removed once EMC agrees to make their
    # GFDLgrid type grids (tile 7) symmetric about tile 6.
    if RUN_ENVIR != "nco":
        if num_left_margin_cells_on_t6g != num_right_margin_cells_on_t6g:
            print_err_msg_exit(f'''
                In order for tile 7 to be centered in the x direction on tile 6, the x-
                direction tile 6 cell indices at which tile 7 starts and ends (given by
                istart_of_t7_on_t6g and iend_of_t7_on_t6g, respectively) must be set 
                such that the number of tile 6 cells in the margin between the left 
                boundaries of tiles 6 and 7 (given by num_left_margin_cells_on_t6g) is
                equal to that in the margin between their right boundaries (given by 
                num_right_margin_cells_on_t6g):
                  istart_of_t7_on_t6g = {istart_of_t7_on_t6g}
                  iend_of_t7_on_t6g = {iend_of_t7_on_t6g}
                  num_left_margin_cells_on_t6g = {num_left_margin_cells_on_t6g}
                  num_right_margin_cells_on_t6g = {num_right_margin_cells_on_t6g}
                Note that the total number of cells in the x-direction on tile 6 is gi-
                ven by:
                  nx_of_t6_on_t6g = {nx_of_t6_on_t6g}
                Please reset istart_of_t7_on_t6g and iend_of_t7_on_t6g and rerun.''')

    num_bot_margin_cells_on_t6g = jstart_of_t7_on_t6g - 1
    num_top_margin_cells_on_t6g = ny_of_t6_on_t6g - jend_of_t7_on_t6g

    # This if-statement can hopefully be removed once EMC agrees to make their
    # GFDLgrid type grids (tile 7) symmetric about tile 6.
    if RUN_ENVIR != "nco":
        if num_bot_margin_cells_on_t6g != num_top_margin_cells_on_t6g:
            print_err_msg_exit(f'''
                In order for tile 7 to be centered in the y direction on tile 6, the y-
                direction tile 6 cell indices at which tile 7 starts and ends (given by
                jstart_of_t7_on_t6g and jend_of_t7_on_t6g, respectively) must be set 
                such that the number of tile 6 cells in the margin between the left 
                boundaries of tiles 6 and 7 (given by num_left_margin_cells_on_t6g) is
                equal to that in the margin between their right boundaries (given by 
                num_right_margin_cells_on_t6g):
                  jstart_of_t7_on_t6g = {jstart_of_t7_on_t6g}
                  jend_of_t7_on_t6g = {jend_of_t7_on_t6g}
                  num_bot_margin_cells_on_t6g = {num_bot_margin_cells_on_t6g}
                  num_top_margin_cells_on_t6g = {num_top_margin_cells_on_t6g}
                Note that the total number of cells in the y-direction on tile 6 is gi-
                ven by:
                  ny_of_t6_on_t6g = {ny_of_t6_on_t6g}
                Please reset jstart_of_t7_on_t6g and jend_of_t7_on_t6g and rerun.''')

    lon_of_t7_ctr = lon_of_t6_ctr
    lat_of_t7_ctr = lat_of_t6_ctr
    #
    #-----------------------------------------------------------------------
    #
    # The grid generation script grid_gen_scr called below in turn calls the
    # make_hgrid utility/executable to construct the regional grid.  make_-
    # hgrid accepts as arguments the index limits (i.e. starting and ending
    # indices) of the regional grid on the supergrid of the regional grid's
    # parent tile.  The regional grid's parent tile is tile 6, and the su-
    # pergrid of any given tile is defined as the grid obtained by doubling
    # the number of cells in each direction on that tile's grid.  We will
    # denote these index limits by
    #
    #   istart_of_t7_on_t6sg
    #   iend_of_t7_on_t6sg
    #   jstart_of_t7_on_t6sg
    #   jend_of_t7_on_t6sg
    #
    # The "_T6SG" suffix in these names is used to indicate that the indices
    # are on the supergrid of tile 6.  Recall, however, that we have as in-
    # puts the index limits of the regional grid on the tile 6 grid, not its
    # supergrid.  These are given by
    #
    #   istart_of_t7_on_t6g
    #   iend_of_t7_on_t6g
    #   jstart_of_t7_on_t6g
    #   jend_of_t7_on_t6g
    #
    # We can obtain the former from the latter by recalling that the super-
    # grid has twice the resolution of the original grid.  Thus,
    #
    #   istart_of_t7_on_t6sg = 2*istart_of_t7_on_t6g - 1
    #   iend_of_t7_on_t6sg = 2*iend_of_t7_on_t6g
    #   jstart_of_t7_on_t6sg = 2*jstart_of_t7_on_t6g - 1
    #   jend_of_t7_on_t6sg = 2*jend_of_t7_on_t6g
    #
    # These are obtained assuming that grid cells on tile 6 must either be
    # completely within the regional domain or completely outside of it,
    # i.e. the boundary of the regional grid must coincide with gridlines
    # on the tile 6 grid; it cannot cut through tile 6 cells.  (Note that
    # this implies that the starting indices on the tile 6 supergrid must be
    # odd while the ending indices must be even; the above expressions sa-
    # tisfy this requirement.)  We perfrom these calculations next.
    #
    #-----------------------------------------------------------------------
    #
    istart_of_t7_on_t6sg = 2*istart_of_t7_on_t6g - 1
    iend_of_t7_on_t6sg = 2*iend_of_t7_on_t6g
    jstart_of_t7_on_t6sg = 2*jstart_of_t7_on_t6g - 1
    jend_of_t7_on_t6sg = 2*jend_of_t7_on_t6g
    #
    #-----------------------------------------------------------------------
    #
    # If we simply pass to make_hgrid the index limits of the regional grid
    # on the tile 6 supergrid calculated above, make_hgrid will generate a
    # regional grid without a halo.  To obtain a regional grid with a halo,
    # we must pass to make_hgrid the index limits (on the tile 6 supergrid)
    # of the regional grid including a halo.  We will let the variables
    #
    #   istart_of_t7_with_halo_on_t6sg
    #   iend_of_t7_with_halo_on_t6sg
    #   jstart_of_t7_with_halo_on_t6sg
    #   jend_of_t7_with_halo_on_t6sg
    #
    # denote these limits.  The reason we include "_wide_halo" in these va-
    # riable names is that the halo of the grid that we will first generate
    # will be wider than the halos that are actually needed as inputs to the
    # FV3LAM model (i.e. the 0-cell-wide, 3-cell-wide, and 4-cell-wide halos
    # described above).  We will generate the grids with narrower halos that
    # the model needs later on by "shaving" layers of cells from this wide-
    # halo grid.  Next, we describe how to calculate the above indices.
    #
    # Let halo_width_on_t7g denote the width of the "wide" halo in units of number of
    # grid cells on the regional grid (i.e. tile 7) that we'd like to have
    # along all four edges of the regional domain (left, right, bottom, and
    # top).  To obtain the corresponding halo width in units of number of
    # cells on the tile 6 grid -- which we denote by halo_width_on_t6g -- we simply di-
    # vide halo_width_on_t7g by the refinement ratio, i.e.
    #
    #   halo_width_on_t6g = halo_width_on_t7g/refine_ratio_t6g_to_t7g
    #
    # The corresponding halo width on the tile 6 supergrid is then given by
    #
    #   halo_width_on_t6sg = 2*halo_width_on_t6g
    #                      = 2*halo_width_on_t7g/refine_ratio_t6g_to_t7g
    #
    # Note that halo_width_on_t6sg must be an integer, but the expression for it de-
    # rived above may not yield an integer.  To ensure that the halo has a
    # width of at least halo_width_on_t7g cells on the regional grid, we round up the
    # result of the expression above for halo_width_on_t6sg, i.e. we redefine halo_width_on_t6sg
    # to be
    #
    #   halo_width_on_t6sg = ceil(2*halo_width_on_t7g/refine_ratio_t6g_to_t7g)
    #
    # where ceil(...) is the ceiling function, i.e. it rounds its floating
    # point argument up to the next larger integer.  Since in bash division
    # of two integers returns a truncated integer and since bash has no
    # built-in ceil(...) function, we perform the rounding-up operation by
    # adding the denominator (of the argument of ceil(...) above) minus 1 to
    # the original numerator, i.e. by redefining halo_width_on_t6sg to be
    #
    #   halo_width_on_t6sg = (2*halo_width_on_t7g + refine_ratio_t6g_to_t7g - 1)/refine_ratio_t6g_to_t7g
    #
    # This trick works when dividing one positive integer by another.
    #
    # In order to calculate halo_width_on_t6g using the above expression, we must
    # first specify halo_width_on_t7g.  Next, we specify an initial value for it by
    # setting it to one more than the largest-width halo that the model ac-
    # tually needs, which is NH4.  We then calculate halo_width_on_t6sg using the
    # above expression.  Note that these values of halo_width_on_t7g and halo_width_on_t6sg will
    # likely not be their final values; their final values will be calcula-
    # ted later below after calculating the starting and ending indices of
    # the regional grid with wide halo on the tile 6 supergrid and then ad-
    # justing the latter to satisfy certain conditions.
    #
    #-----------------------------------------------------------------------
    #
    halo_width_on_t7g = NH4 + 1
    halo_width_on_t6sg = (2*halo_width_on_t7g + refine_ratio_t6g_to_t7g - 1)/refine_ratio_t6g_to_t7g
    #
    #-----------------------------------------------------------------------
    #
    # With an initial value of halo_width_on_t6sg now available, we can obtain the
    # tile 6 supergrid index limits of the regional domain (including the
    # wide halo) from the index limits for the regional domain without a ha-
    # lo by simply subtracting halo_width_on_t6sg from the lower index limits and add-
    # ing halo_width_on_t6sg to the upper index limits, i.e.
    #
    #   istart_of_t7_with_halo_on_t6sg = istart_of_t7_on_t6sg - halo_width_on_t6sg
    #   iend_of_t7_with_halo_on_t6sg = iend_of_t7_on_t6sg + halo_width_on_t6sg
    #   jstart_of_t7_with_halo_on_t6sg = jstart_of_t7_on_t6sg - halo_width_on_t6sg
    #   jend_of_t7_with_halo_on_t6sg = jend_of_t7_on_t6sg + halo_width_on_t6sg
    #
    # We calculate these next.
    #
    #-----------------------------------------------------------------------
    #
    istart_of_t7_with_halo_on_t6sg = int(istart_of_t7_on_t6sg - halo_width_on_t6sg)
    iend_of_t7_with_halo_on_t6sg = int(iend_of_t7_on_t6sg + halo_width_on_t6sg)
    jstart_of_t7_with_halo_on_t6sg = int(jstart_of_t7_on_t6sg - halo_width_on_t6sg)
    jend_of_t7_with_halo_on_t6sg = int(jend_of_t7_on_t6sg + halo_width_on_t6sg)
    #
    #-----------------------------------------------------------------------
    #
    # As for the regional grid without a halo, the regional grid with a wide
    # halo that make_hgrid will generate must be such that grid cells on
    # tile 6 either lie completely within this grid or outside of it, i.e.
    # they cannot lie partially within/outside of it.  This implies that the
    # starting indices on the tile 6 supergrid of the grid with wide halo
    # must be odd while the ending indices must be even.  Thus, below, we
    # subtract 1 from the starting indices if they are even (which ensures
    # that there will be at least halo_width_on_t7g halo cells along the left and bot-
    # tom boundaries), and we add 1 to the ending indices if they are odd
    # (which ensures that there will be at least halo_width_on_t7g halo cells along the
    # right and top boundaries).
    #
    #-----------------------------------------------------------------------
    #
    if istart_of_t7_with_halo_on_t6sg % 2 == 0:
        istart_of_t7_with_halo_on_t6sg = istart_of_t7_with_halo_on_t6sg - 1
    
    if iend_of_t7_with_halo_on_t6sg % 2 == 1:
        iend_of_t7_with_halo_on_t6sg = iend_of_t7_with_halo_on_t6sg + 1
    
    if jstart_of_t7_with_halo_on_t6sg % 2 == 0:
        jstart_of_t7_with_halo_on_t6sg = jstart_of_t7_with_halo_on_t6sg - 1
    
    if jend_of_t7_with_halo_on_t6sg % 2 == 1:
        jend_of_t7_with_halo_on_t6sg = jend_of_t7_with_halo_on_t6sg + 1
    #
    #-----------------------------------------------------------------------
    #
    # Now that the starting and ending tile 6 supergrid indices of the re-
    # gional grid with the wide halo have been calculated (and adjusted), we
    # recalculate the width of the wide halo on:
    #
    # 1) the tile 6 supergrid;
    # 2) the tile 6 grid; and
    # 3) the tile 7 grid.
    #
    # These are the final values of these quantities that are guaranteed to
    # correspond to the starting and ending indices on the tile 6 supergrid.
    #
    #-----------------------------------------------------------------------
    #
    print_info_msg(f'''
        Original values of the halo width on the tile 6 supergrid and on the 
        tile 7 grid are:
          halo_width_on_t6sg = {halo_width_on_t6sg}
          halo_width_on_t7g  = {halo_width_on_t7g}''', verbose=VERBOSE)
    
    halo_width_on_t6sg = istart_of_t7_on_t6sg - istart_of_t7_with_halo_on_t6sg
    halo_width_on_t6g = halo_width_on_t6sg//2
    halo_width_on_t7g = int(halo_width_on_t6g*refine_ratio_t6g_to_t7g)
    
    print_info_msg(f'''
        Values of the halo width on the tile 6 supergrid and on the tile 7 grid 
        AFTER adjustments are:
          halo_width_on_t6sg = {halo_width_on_t6sg}
          halo_width_on_t7g  = {halo_width_on_t7g}''', verbose=VERBOSE)
    #
    #-----------------------------------------------------------------------
    #
    # Calculate the number of cells that the regional domain (without halo)
    # has in each of the two horizontal directions (say x and y).  We denote
    # these by nx_of_t7_on_t7g and ny_of_t7_on_t7g, respectively.  These 
    # will be needed in the "shave" steps in the grid generation task of the
    # workflow.
    #
    #-----------------------------------------------------------------------
    #
    nx_of_t7_on_t6sg = iend_of_t7_on_t6sg - istart_of_t7_on_t6sg + 1
    nx_of_t7_on_t6g = nx_of_t7_on_t6sg/2
    nx_of_t7_on_t7g = int(nx_of_t7_on_t6g*refine_ratio_t6g_to_t7g)
    
    ny_of_t7_on_t6sg = jend_of_t7_on_t6sg - jstart_of_t7_on_t6sg + 1
    ny_of_t7_on_t6g = ny_of_t7_on_t6sg/2
    ny_of_t7_on_t7g = int(ny_of_t7_on_t6g*refine_ratio_t6g_to_t7g)
    #
    # The following are set only for informational purposes.
    #
    nx_of_t6_on_t6sg = 2*nx_of_t6_on_t6g
    ny_of_t6_on_t6sg = 2*ny_of_t6_on_t6g
    
    prime_factors_nx_of_t7_on_t7g = prime_factors(nx_of_t7_on_t7g)
    prime_factors_ny_of_t7_on_t7g = prime_factors(ny_of_t7_on_t7g)
    
    print_info_msg(f'''
        The number of cells in the two horizontal directions (x and y) on the 
        parent tile's (tile 6) grid and supergrid are:
          nx_of_t6_on_t6g = {nx_of_t6_on_t6g}
          ny_of_t6_on_t6g = {ny_of_t6_on_t6g}
          nx_of_t6_on_t6sg = {nx_of_t6_on_t6sg}
          ny_of_t6_on_t6sg = {ny_of_t6_on_t6sg}
        
        The number of cells in the two horizontal directions on the tile 6 grid
        and supergrid that the regional domain (tile 7) WITHOUT A HALO encompas-
        ses are:
          nx_of_t7_on_t6g = {nx_of_t7_on_t6g}
          ny_of_t7_on_t6g = {ny_of_t7_on_t6g}
          nx_of_t7_on_t6sg = {nx_of_t7_on_t6sg}
          ny_of_t7_on_t6sg = {ny_of_t7_on_t6sg}
        
        The starting and ending i and j indices on the tile 6 grid used to gene-
        rate this regional grid are:
          istart_of_t7_on_t6g = {istart_of_t7_on_t6g}
          iend_of_t7_on_t6g   = {iend_of_t7_on_t6g}
          jstart_of_t7_on_t6g = {jstart_of_t7_on_t6g}
          jend_of_t7_on_t6g   = {jend_of_t7_on_t6g}
        
        The corresponding starting and ending i and j indices on the tile 6 su-
        pergrid are:
          istart_of_t7_on_t6sg = {istart_of_t7_on_t6sg}
          iend_of_t7_on_t6sg   = {iend_of_t7_on_t6sg}
          jstart_of_t7_on_t6sg = {jstart_of_t7_on_t6sg}
          jend_of_t7_on_t6sg   = {jend_of_t7_on_t6sg}
        
        The refinement ratio (ratio of the number of cells in tile 7 that abut
        a single cell in tile 6) is:
          refine_ratio_t6g_to_t7g = {refine_ratio_t6g_to_t7g}
        
        The number of cells in the two horizontal directions on the regional do-
        main's (i.e. tile 7's) grid WITHOUT A HALO are:
          nx_of_t7_on_t7g = {nx_of_t7_on_t7g}
          ny_of_t7_on_t7g = {ny_of_t7_on_t7g}
        
        The prime factors of nx_of_t7_on_t7g and ny_of_t7_on_t7g are (useful for
        determining an MPI task layout):
          prime_factors_nx_of_t7_on_t7g: {prime_factors_nx_of_t7_on_t7g}
          prime_factors_ny_of_t7_on_t7g: {prime_factors_ny_of_t7_on_t7g}''', verbose=VERBOSE)
    #
    #-----------------------------------------------------------------------
    #
    # For informational purposes, calculate the number of cells in each di-
    # rection on the regional grid including the wide halo (of width halo_-
    # width_on_t7g cells).  We denote these by nx_of_t7_with_halo_on_t7g and
    # ny_of_t7_with_halo_on_t7g, respectively.
    #
    #-----------------------------------------------------------------------
    #
    nx_of_t7_with_halo_on_t6sg = iend_of_t7_with_halo_on_t6sg - istart_of_t7_with_halo_on_t6sg + 1
    nx_of_t7_with_halo_on_t6g = nx_of_t7_with_halo_on_t6sg/2
    nx_of_t7_with_halo_on_t7g = nx_of_t7_with_halo_on_t6g*refine_ratio_t6g_to_t7g
    
    ny_of_t7_with_halo_on_t6sg = jend_of_t7_with_halo_on_t6sg - jstart_of_t7_with_halo_on_t6sg + 1
    ny_of_t7_with_halo_on_t6g = ny_of_t7_with_halo_on_t6sg/2
    ny_of_t7_with_halo_on_t7g = ny_of_t7_with_halo_on_t6g*refine_ratio_t6g_to_t7g
    
    print_info_msg(f'''
        nx_of_t7_with_halo_on_t7g = {nx_of_t7_with_halo_on_t7g}
        (istart_of_t7_with_halo_on_t6sg = {istart_of_t7_with_halo_on_t6sg},
        iend_of_t7_with_halo_on_t6sg = {iend_of_t7_with_halo_on_t6sg})''', verbose=VERBOSE)
    
    print_info_msg(f'''
        ny_of_t7_with_halo_on_t7g = {ny_of_t7_with_halo_on_t7g}
        (jstart_of_t7_with_halo_on_t6sg = {jstart_of_t7_with_halo_on_t6sg},
        jend_of_t7_with_halo_on_t6sg = {jend_of_t7_with_halo_on_t6sg})''', verbose=VERBOSE)
    #
    #-----------------------------------------------------------------------
    #
    # Return output variables.
    #
    #-----------------------------------------------------------------------
    #
    return (lon_of_t7_ctr, lat_of_t7_ctr, nx_of_t7_on_t7g, ny_of_t7_on_t7g,
            halo_width_on_t7g, stretch_factor,
            istart_of_t7_with_halo_on_t6sg,
            iend_of_t7_with_halo_on_t6sg,
            jstart_of_t7_with_halo_on_t6sg,
            jend_of_t7_with_halo_on_t6sg)
def set_ozone_param(ccpp_phys_suite_fp):
    """ Function that does the following:
    (1) Determines the ozone parameterization being used by checking in the
        CCPP physics suite XML.
   
    (2) Sets the name of the global ozone production/loss file in the FIXgsm
        FIXgsm system directory to copy to the experiment's FIXam directory.
   
    (3) Resets the last element of the workflow array variable
        FIXgsm_FILES_TO_COPY_TO_FIXam that contains the files to copy from
        FIXgsm to FIXam (this last element is initially set to a dummy 
        value) to the name of the ozone production/loss file set in the
        previous step.
   
    (4) Resets the element of the workflow array variable 
        CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING (this array contains the 
        mapping between the symlinks to create in any cycle directory and
        the files in the FIXam directory that are their targets) that 
        specifies the mapping for the ozone symlink/file such that the 
        target FIXam file name is set to the name of the ozone production/
        loss file set above.

    Args:
        ccpp_phys_suite_fp: full path to CCPP physics suite
    Returns:
        ozone_param: a string
    """

    print_input_args(locals())

    # import all environment variables
    import_vars()

    #
    #-----------------------------------------------------------------------
    #
    # Get the name of the ozone parameterization being used.  There are two
    # possible ozone parameterizations:
    #
    # (1) A parameterization developed/published in 2015.  Here, we refer to
    #     this as the 2015 parameterization.  If this is being used, then we
    #     set the variable ozone_param to the string "ozphys_2015".
    #
    # (2) A parameterization developed/published sometime after 2015.  Here,
    #     we refer to this as the after-2015 parameterization.  If this is
    #     being used, then we set the variable ozone_param to the string
    #     "ozphys".
    #
    # We check the CCPP physics suite definition file (SDF) to determine the
    # parameterization being used.  If this file contains the line
    #
    #   <scheme>ozphys_2015</scheme>
    #
    # then the 2015 parameterization is being used.  If it instead contains
    # the line
    #
    #   <scheme>ozphys</scheme>
    #
    # then the after-2015 parameterization is being used.  (The SDF should
    # contain exactly one of these lines; not both nor neither; we check for
    # this.)
    #
    #-----------------------------------------------------------------------
    #
    tree = load_xml_file(ccpp_phys_suite_fp)
    ozone_param = ""
    if has_tag_with_value(tree, "scheme", "ozphys_2015"):
        fixgsm_ozone_fn = "ozprdlos_2015_new_sbuvO3_tclm15_nuchem.f77"
        ozone_param = "ozphys_2015"
    elif has_tag_with_value(tree, "scheme", "ozphys"):
        fixgsm_ozone_fn = "global_o3prdlos.f77"
        ozone_param = "ozphys"
    else:
        print_err_msg_exit(f'''
        Unknown or no ozone parameterization
        specified in the CCPP physics suite file (ccpp_phys_suite_fp):
          ccpp_phys_suite_fp = \"{ccpp_phys_suite_fp}\"
          ozone_param = \"{ozone_param}\"''')
    #
    #-----------------------------------------------------------------------
    #
    # Set the last element of the array FIXgsm_FILES_TO_COPY_TO_FIXam to the
    # name of the ozone production/loss file to copy from the FIXgsm to the
    # FIXam directory.
    #
    #-----------------------------------------------------------------------
    #
    i = len(FIXgsm_FILES_TO_COPY_TO_FIXam) - 1
    FIXgsm_FILES_TO_COPY_TO_FIXam[i] = f"{fixgsm_ozone_fn}"
    #
    #-----------------------------------------------------------------------
    #
    # Set the element in the array CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING that
    # specifies the mapping between the symlink for the ozone production/loss
    # file that must be created in each cycle directory and its target in the
    # FIXam directory.  The name of the symlink is alrady in the array, but
    # the target is not because it depends on the ozone parameterization that
    # the physics suite uses.  Since we determined the ozone parameterization
    # above, we now set the target of the symlink accordingly.
    #
    #-----------------------------------------------------------------------
    #
    ozone_symlink = "global_o3prdlos.f77"
    fixgsm_ozone_fn_is_set = False
    regex_search = "^[ ]*([^| ]*)[ ]*[|][ ]*([^| ]*)[ ]*$"
    num_symlinks = len(CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING)

    for i in range(num_symlinks):
        mapping = CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING[i]
        symlink = find_pattern_in_str(regex_search, mapping)
        if symlink is not None:
            symlink = symlink[0]
        if symlink == ozone_symlink:
            regex_search = "^[ ]*([^| ]+[ ]*)[|][ ]*([^| ]*)[ ]*$"
            mapping_ozone = find_pattern_in_str(regex_search, mapping)[0]
            mapping_ozone = f"{mapping_ozone}| {fixgsm_ozone_fn}"
            CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING[i] = f"{mapping_ozone}"
            fixgsm_ozone_fn_is_set = True
            break
    #
    #-----------------------------------------------------------------------
    #
    # If fixgsm_ozone_fn_is_set is set to True, then the appropriate element
    # of the array CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING was set successfully.
    # In this case, print out the new version of this array.  Otherwise, print
    # out an error message and exit.
    #
    #-----------------------------------------------------------------------
    #
    if fixgsm_ozone_fn_is_set:

        msg = dedent(f'''
        After setting the file name of the ozone production/loss file in the
        FIXgsm directory (based on the ozone parameterization specified in the
        CCPP suite definition file), the array specifying the mapping between
        the symlinks that need to be created in the cycle directories and the
        files in the FIXam directory is:
        
        ''')
        msg += dedent(f'''
          CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING = {list_to_str(CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING)}
        ''')
        print_info_msg(msg, verbose=VERBOSE)

    else:

        print_err_msg_exit(f'''
        Unable to set name of the ozone production/loss file in the FIXgsm directory
        in the array that specifies the mapping between the symlinks that need to
        be created in the cycle directories and the files in the FIXgsm directory:
          fixgsm_ozone_fn_is_set = \"{fixgsm_ozone_fn_is_set}\"''')

    EXPORTS = [
        "CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING",
        "FIXgsm_FILES_TO_COPY_TO_FIXam"
    ]
    export_vars(env_vars=EXPORTS)

    return ozone_param
Ejemplo n.º 12
0
def link_fix(verbose, file_group):
    """ This file defines a function that ...
    Args:
        verbose: True or False
        file_group: could be on of ["grid", "orog", "sfc_climo"]
    Returns:
        a string: resolution
    """

    print_input_args(locals())

    valid_vals_file_group = ["grid", "orog", "sfc_climo"]
    check_var_valid_value(file_group, valid_vals_file_group)

    #import all environement variables
    import_vars()

    #
    #-----------------------------------------------------------------------
    #
    # Create symlinks in the FIXLAM directory pointing to the grid files.
    # These symlinks are needed by the make_orog, make_sfc_climo, make_ic,
    # make_lbc, and/or run_fcst tasks.
    #
    # Note that we check that each target file exists before attempting to
    # create symlinks.  This is because the "ln" command will create sym-
    # links to non-existent targets without returning with a nonzero exit
    # code.
    #
    #-----------------------------------------------------------------------
    #
    print_info_msg(
        f'Creating links in the FIXLAM directory to the grid files...',
        verbose=verbose)
    #
    #-----------------------------------------------------------------------
    #
    # Create globbing patterns for grid, orography, and surface climatology
    # files.
    #
    #
    # For grid files (i.e. file_group set to "grid"), symlinks are created
    # in the FIXLAM directory to files (of the same names) in the GRID_DIR.
    # These symlinks/files and the reason each is needed is listed below:
    #
    # 1) "C*.mosaic.halo${NHW}.nc"
    #    This mosaic file for the wide-halo grid (i.e. the grid with a ${NHW}-
    #    cell-wide halo) is needed as an input to the orography filtering
    #    executable in the orography generation task.  The filtering code
    #    extracts from this mosaic file the name of the file containing the
    #    grid on which it will generate filtered topography.  Note that the
    #    orography generation and filtering are both performed on the wide-
    #    halo grid.  The filtered orography file on the wide-halo grid is then
    #    shaved down to obtain the filtered orography files with ${NH3}- and
    #    ${NH4}-cell-wide halos.
    #
    #    The raw orography generation step in the make_orog task requires the
    #    following symlinks/files:
    #
    #    a) C*.mosaic.halo${NHW}.nc
    #       The script for the make_orog task extracts the name of the grid
    #       file from this mosaic file; this name should be
    #       "C*.grid.tile${TILE_RGNL}.halo${NHW}.nc".
    #
    #    b) C*.grid.tile${TILE_RGNL}.halo${NHW}.nc
    #       This is the
    #       The script for the make_orog task passes the name of the grid
    #       file (extracted above from the mosaic file) to the orography
    #       generation executable.  The executable then
    #       reads in this grid file and generates a raw orography
    #       file on the grid.  The raw orography file is initially renamed "out.oro.nc",
    #       but for clarity, it is then renamed "C*.raw_orog.tile${TILE_RGNL}.halo${NHW}.nc".
    #
    #    c) The fixed files thirty.second.antarctic.new.bin, landcover30.fixed,
    #       and gmted2010.30sec.int.
    #
    #    The orography filtering step in the make_orog task requires the
    #    following symlinks/files:
    #
    #    a) C*.mosaic.halo${NHW}.nc
    #       This is the mosaic file for the wide-halo grid.  The orography
    #       filtering executable extracts from this file the name of the grid
    #       file containing the wide-halo grid (which should be
    #       "${CRES}.grid.tile${TILE_RGNL}.halo${NHW}.nc").  The executable then
    #       looks for this grid file IN THE DIRECTORY IN WHICH IT IS RUNNING.
    #       Thus, before running the executable, the script creates a symlink in this run directory that
    #       points to the location of the actual wide-halo grid file.
    #
    #    b) C*.raw_orog.tile${TILE_RGNL}.halo${NHW}.nc
    #       This is the raw orography file on the wide-halo grid.  The script
    #       for the make_orog task copies this file to a new file named
    #       "C*.filtered_orog.tile${TILE_RGNL}.halo${NHW}.nc" that will be
    #       used as input to the orography filtering executable.  The executable
    #       will then overwrite the contents of this file with the filtered orography.
    #       Thus, the output of the orography filtering executable will be
    #       the file C*.filtered_orog.tile${TILE_RGNL}.halo${NHW}.nc.
    #
    #    The shaving step in the make_orog task requires the following:
    #
    #    a) C*.filtered_orog.tile${TILE_RGNL}.halo${NHW}.nc
    #       This is the filtered orography file on the wide-halo grid.
    #       This gets shaved down to two different files:
    #
    #        i) ${CRES}.oro_data.tile${TILE_RGNL}.halo${NH0}.nc
    #           This is the filtered orography file on the halo-0 grid.
    #
    #       ii) ${CRES}.oro_data.tile${TILE_RGNL}.halo${NH4}.nc
    #           This is the filtered orography file on the halo-4 grid.
    #
    #       Note that the file names of the shaved files differ from that of
    #       the initial unshaved file on the wide-halo grid in that the field
    #       after ${CRES} is now "oro_data" (not "filtered_orog") to comply
    #       with the naming convention used more generally.
    #
    # 2) "C*.mosaic.halo${NH4}.nc"
    #    This mosaic file for the grid with a 4-cell-wide halo is needed as
    #    an input to the surface climatology generation executable.  The
    #    surface climatology generation code reads from this file the number
    #    of tiles (which should be 1 for a regional grid) and the tile names.
    #    More importantly, using the ESMF function ESMF_GridCreateMosaic(),
    #    it creates a data object of type esmf_grid; the grid information
    #    in this object is obtained from the grid file specified in the mosaic
    #    file, which should be "C*.grid.tile${TILE_RGNL}.halo${NH4}.nc".  The
    #    dimensions specified in this grid file must match the ones specified
    #    in the (filtered) orography file "C*.oro_data.tile${TILE_RGNL}.halo${NH4}.nc"
    #    that is also an input to the surface climatology generation executable.
    #    If they do not, then the executable will crash with an ESMF library
    #    error (something like "Arguments are incompatible").
    #
    #    Thus, for the make_sfc_climo task, the following symlinks/files must
    #    exist:
    #    a) "C*.mosaic.halo${NH4}.nc"
    #    b) "C*.grid.tile${TILE_RGNL}.halo${NH4}.nc"
    #    c) "C*.oro_data.tile${TILE_RGNL}.halo${NH4}.nc"
    #
    # 3)
    #
    #
    #-----------------------------------------------------------------------
    #
    #
    if file_group == "grid":
        fns = [
            f"C*{DOT_OR_USCORE}mosaic.halo{NHW}.nc",
            f"C*{DOT_OR_USCORE}mosaic.halo{NH4}.nc",
            f"C*{DOT_OR_USCORE}mosaic.halo{NH3}.nc",
            f"C*{DOT_OR_USCORE}grid.tile{TILE_RGNL}.halo{NHW}.nc",
            f"C*{DOT_OR_USCORE}grid.tile{TILE_RGNL}.halo{NH3}.nc",
            f"C*{DOT_OR_USCORE}grid.tile{TILE_RGNL}.halo{NH4}.nc"
        ]
        fps = [os.path.join(GRID_DIR, itm) for itm in fns]
        run_task = f"{RUN_TASK_MAKE_GRID}"
    #
    elif file_group == "orog":
        fns = [
            f"C*{DOT_OR_USCORE}oro_data.tile{TILE_RGNL}.halo{NH0}.nc",
            f"C*{DOT_OR_USCORE}oro_data.tile{TILE_RGNL}.halo{NH4}.nc"
        ]
        if CCPP_PHYS_SUITE == "FV3_HRRR":
            fns += [
                f"C*{DOT_OR_USCORE}oro_data_ss.tile{TILE_RGNL}.halo{NH0}.nc",
                f"C*{DOT_OR_USCORE}oro_data_ls.tile{TILE_RGNL}.halo{NH0}.nc",
            ]
        fps = [os.path.join(OROG_DIR, itm) for itm in fns]
        run_task = f"{RUN_TASK_MAKE_OROG}"
    #
    # The following list of symlinks (which have the same names as their
    # target files) need to be created made in order for the make_ics and
    # make_lbcs tasks (i.e. tasks involving chgres_cube) to work.
    #
    elif file_group == "sfc_climo":
        num_fields = len(SFC_CLIMO_FIELDS)
        fns = [None] * (2 * num_fields)
        for i in range(num_fields):
            ii = 2 * i
            fns[ii] = f"C*.{SFC_CLIMO_FIELDS[i]}.tile{TILE_RGNL}.halo{NH0}.nc"
            fns[ii +
                1] = f"C*.{SFC_CLIMO_FIELDS[i]}.tile{TILE_RGNL}.halo{NH4}.nc"
        fps = [os.path.join(SFC_CLIMO_DIR, itm) for itm in fns]
        run_task = f"{RUN_TASK_MAKE_SFC_CLIMO}"
    #

    #
    #-----------------------------------------------------------------------
    #
    # Find all files matching the globbing patterns and make sure that they
    # all have the same resolution (an integer) in their names.
    #
    #-----------------------------------------------------------------------
    #
    i = 0
    res_prev = ""
    res = ""
    fp_prev = ""

    for pattern in fps:
        files = glob.glob(pattern)
        for fp in files:

            fn = os.path.basename(fp)

            regex_search = "^C([0-9]*).*"
            res = find_pattern_in_str(regex_search, fn)
            if res is None:
                print_err_msg_exit(f'''
                The resolution could not be extracted from the current file's name.  The
                full path to the file (fp) is:
                  fp = \"{fp}\"
                This may be because fp contains the * globbing character, which would
                imply that no files were found that match the globbing pattern specified
                in fp.''')
            else:
                res = res[0]

            if (i > 0) and (res != res_prev):
                print_err_msg_exit(f'''
                The resolutions (as obtained from the file names) of the previous and 
                current file (fp_prev and fp, respectively) are different:
                  fp_prev = \"{fp_prev}\"
                  fp      = \"{fp}\"
                Please ensure that all files have the same resolution.''')

            i = i + 1
            fp_prev = f"{fp}"
            res_prev = res
    #
    #-----------------------------------------------------------------------
    #
    # Replace the * globbing character in the set of globbing patterns with
    # the resolution.  This will result in a set of (full paths to) specific
    # files.
    #
    #-----------------------------------------------------------------------
    #
    fps = [itm.replace('*', res) for itm in fps]
    #
    #-----------------------------------------------------------------------
    #
    # In creating the various symlinks below, it is convenient to work in
    # the FIXLAM directory.  We will change directory back to the original
    # later below.
    #
    #-----------------------------------------------------------------------
    #
    SAVE_DIR = os.getcwd()
    cd_vrfy(FIXLAM)
    #
    #-----------------------------------------------------------------------
    #
    # Use the set of full file paths generated above as the link targets to
    # create symlinks to these files in the FIXLAM directory.
    #
    #-----------------------------------------------------------------------
    #
    # If the task in consideration (which will be one of the pre-processing
    # tasks MAKE_GRID_TN, MAKE_OROG_TN, and MAKE_SFC_CLIMO_TN) was run, then
    # the target files will be located under the experiment directory.  In
    # this case, we use relative symlinks in order the experiment directory
    # more portable and the symlinks more readable.  However, if the task
    # was not run, then pregenerated grid, orography, or surface climatology
    # files will be used, and those will be located in an arbitrary directory
    # (specified by the user) that is somwehere outside the experiment
    # directory.  Thus, in this case, there isn't really an advantage to using
    # relative symlinks, so we use symlinks with absolute paths.
    #
    if run_task:
        relative_link_flag = True
    else:
        relative_link_flag = False

    for fp in fps:
        fn = os.path.basename(fp)
        create_symlink_to_file(fp, fn, relative_link_flag)
    #
    #-----------------------------------------------------------------------
    #
    # Set the C-resolution based on the resolution appearing in the file
    # names.
    #
    #-----------------------------------------------------------------------
    #
    cres = f"C{res}"
    #
    #-----------------------------------------------------------------------
    #
    # If considering grid files, create a symlink to the halo4 grid file
    # that does not contain the halo size in its name.  This is needed by
    # the tasks that generate the initial and lateral boundary condition
    # files.
    #
    #-----------------------------------------------------------------------
    #
    if file_group == "grid":

        target = f"{cres}{DOT_OR_USCORE}grid.tile{TILE_RGNL}.halo{NH4}.nc"
        symlink = f"{cres}{DOT_OR_USCORE}grid.tile{TILE_RGNL}.nc"
        create_symlink_to_file(target, symlink, True)
        #
        # The surface climatology file generation code looks for a grid file
        # having a name of the form "C${GFDLgrid_RES}_grid.tile7.halo4.nc" (i.e.
        # the C-resolution used in the name of this file is the number of grid
        # points per horizontal direction per tile, just like in the global model).
        # Thus, if we are running the MAKE_SFC_CLIMO_TN task, if the grid is of
        # GFDLgrid type, and if we are not using GFDLgrid_RES in filenames (i.e.
        # we are using the equivalent global uniform grid resolution instead),
        # then create a link whose name uses the GFDLgrid_RES that points to the
        # link whose name uses the equivalent global uniform resolution.
        #
        if RUN_TASK_MAKE_SFC_CLIMO and \
           GRID_GEN_METHOD == "GFDLgrid" and \
           not GFDLgrid_USE_GFDLgrid_RES_IN_FILENAMES:
            target = f"{cres}{DOT_OR_USCORE}grid.tile{TILE_RGNL}.halo{NH4}.nc"
            symlink = f"C{GFDLgrid_RES}{DOT_OR_USCORE}grid.tile{TILE_RGNL}.nc"
            create_symlink_to_file(target, symlink, relative)
    #
    #-----------------------------------------------------------------------
    #
    # If considering surface climatology files, create symlinks to the surface
    # climatology files that do not contain the halo size in their names.
    # These are needed by the task that generates the initial condition files.
    #
    #-----------------------------------------------------------------------
    #
    if file_group == "sfc_climo":

        tmp = [f"{cres}.{itm}" for itm in SFC_CLIMO_FIELDS]
        fns_sfc_climo_with_halo_in_fn = [
            f"{itm}.tile{TILE_RGNL}.halo{NH4}.nc" for itm in tmp
        ]
        fns_sfc_climo_no_halo_in_fn = [
            f"{itm}.tile{TILE_RGNL}.nc" for itm in tmp
        ]

        for i in range(num_fields):
            target = f"{fns_sfc_climo_with_halo_in_fn[i]}"
            symlink = f"{fns_sfc_climo_no_halo_in_fn[i]}"
            create_symlink_to_file(target, symlink, True)
    #
    # In order to be able to specify the surface climatology file names in
    # the forecast model's namelist file, in the FIXLAM directory a symlink
    # must be created for each surface climatology field that has "tile1" in
    # its name (and no "halo") and which points to the corresponding "tile7.halo0"
    # file.
    #
        tmp = [f"{cres}.{itm}" for itm in SFC_CLIMO_FIELDS]
        fns_sfc_climo_tile7_halo0_in_fn = [
            f"{itm}.tile{TILE_RGNL}.halo{NH0}.nc" for itm in tmp
        ]
        fns_sfc_climo_tile1_no_halo_in_fn = [f"{itm}.tile1.nc" for itm in tmp]

        for i in range(num_fields):
            target = f"{fns_sfc_climo_tile7_halo0_in_fn[i]}"
            symlink = f"{fns_sfc_climo_tile1_no_halo_in_fn[i]}"
            create_symlink_to_file(target, symlink, True)
    #
    #-----------------------------------------------------------------------
    #
    # Change directory back to original one.
    #
    #-----------------------------------------------------------------------
    #
    cd_vrfy(SAVE_DIR)

    return res
Ejemplo n.º 13
0
def create_diag_table_file(run_dir):
    """ Creates a diagnostic table file for each cycle to be run

    Args:
        run_dir: run directory
    Returns:
        Boolean
    """

    print_input_args(locals())

    #import all environment variables
    import_vars()

    #create a diagnostic table file within the specified run directory
    print_info_msg(f'''
        Creating a diagnostics table file (\"{DIAG_TABLE_FN}\") in the specified
        run directory...
        
          run_dir = \"{run_dir}\"''',
                   verbose=VERBOSE)

    diag_table_fp = os.path.join(run_dir, DIAG_TABLE_FN)

    print_info_msg(f'''
        
        Using the template diagnostics table file:
        
            diag_table_tmpl_fp = {DIAG_TABLE_TMPL_FP}
        
        to create:
        
            diag_table_fp = \"{diag_table_fp}\"''',
                   verbose=VERBOSE)

    settings = {'starttime': CDATE, 'cres': CRES}
    settings_str = cfg_to_yaml_str(settings)

    print_info_msg(dedent(f'''
        The variable \"settings\" specifying values to be used in the \"{DIAG_TABLE_FN}\"
        file has been set as follows:\n
        settings =\n\n''') + settings_str,
                   verbose=VERBOSE)

    #call fill jinja
    try:
        fill_jinja_template([
            "-q", "-u", settings_str, "-t", DIAG_TABLE_TMPL_FP, "-o",
            diag_table_fp
        ])
    except:
        print_err_msg_exit(
            dedent(f'''
            Call to python script fill_jinja_template.py to create a \"{DIAG_TABLE_FN}\"
            file from a jinja2 template failed.  Parameters passed to this script are:
              Full path to template diag table file:
                DIAG_TABLE_TMPL_FP = \"{DIAG_TABLE_TMPL_FP}\"
              Full path to output diag table file:
                diag_table_fp = \"{diag_table_fp}\"
              Namelist settings specified on command line:\n
                settings =\n\n''') + settings_str)
        return False
    return True
Ejemplo n.º 14
0
def set_thompson_mp_fix_files(ccpp_phys_suite_fp, thompson_mp_climo_fn):
    """ Function that first checks whether the Thompson
    microphysics parameterization is being called by the selected physics
    suite.  If not, it sets the output variable whose name is specified by
    output_varname_sdf_uses_thompson_mp to "FALSE" and exits.  If so, it 
    sets this variable to "TRUE" and modifies the workflow arrays
    FIXgsm_FILES_TO_COPY_TO_FIXam and CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING
    to ensure that fixed files needed by the Thompson microphysics
    parameterization are copied to the FIXam directory and that appropriate
    symlinks to these files are created in the run directories.

    Args:
        ccpp_phys_suite_fp: full path to CCPP physics suite
        thompson_mp_climo_fn: netcdf file for thompson microphysics
    Returns:
        boolean: sdf_uses_thompson_mp
    """

    print_input_args(locals())

    # import all environment variables
    import_vars()

    #
    #-----------------------------------------------------------------------
    #
    # Check the suite definition file to see whether the Thompson microphysics
    # parameterization is being used.
    #
    #-----------------------------------------------------------------------
    #
    tree = load_xml_file(ccpp_phys_suite_fp)
    sdf_uses_thompson_mp = has_tag_with_value(tree, "scheme", "mp_thompson")
    #
    #-----------------------------------------------------------------------
    #
    # If the Thompson microphysics parameterization is being used, then...
    #
    #-----------------------------------------------------------------------
    #
    if sdf_uses_thompson_mp:
    #
    #-----------------------------------------------------------------------
    #
    # Append the names of the fixed files needed by the Thompson microphysics
    # parameterization to the workflow array FIXgsm_FILES_TO_COPY_TO_FIXam, 
    # and append to the workflow array CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING 
    # the mappings between these files and the names of the corresponding 
    # symlinks that need to be created in the run directories.
    #
    #-----------------------------------------------------------------------
    #
        thompson_mp_fix_files=[
          "CCN_ACTIVATE.BIN",
          "freezeH2O.dat",
          "qr_acr_qg.dat",
          "qr_acr_qs.dat",
          "qr_acr_qgV2.dat",
          "qr_acr_qsV2.dat"
        ]
    
        if (EXTRN_MDL_NAME_ICS  != "HRRR" and EXTRN_MDL_NAME_ICS  != "RAP") or \
           (EXTRN_MDL_NAME_LBCS != "HRRR" and EXTRN_MDL_NAME_LBCS != "RAP"):
          thompson_mp_fix_files.append(thompson_mp_climo_fn)
    
        FIXgsm_FILES_TO_COPY_TO_FIXam.extend(thompson_mp_fix_files)
    
        for fix_file in thompson_mp_fix_files:
          mapping=f"{fix_file} | {fix_file}"
          CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING.append(mapping)
    
        msg=dedent(f'''
            Since the Thompson microphysics parameterization is being used by this 
            physics suite (CCPP_PHYS_SUITE), the names of the fixed files needed by
            this scheme have been appended to the array FIXgsm_FILES_TO_COPY_TO_FIXam, 
            and the mappings between these files and the symlinks that need to be 
            created in the cycle directories have been appended to the array
            CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING.  After these modifications, the 
            values of these parameters are as follows:
            
            ''')
        msg+=dedent(f'''
                CCPP_PHYS_SUITE = \"{CCPP_PHYS_SUITE}\"
            
                FIXgsm_FILES_TO_COPY_TO_FIXam = {list_to_str(FIXgsm_FILES_TO_COPY_TO_FIXam)}
            ''')
        msg+=dedent(f'''
                CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING = {list_to_str(CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING)}
            ''')
        print_info_msg(msg)

        EXPORTS = [ "CYCLEDIR_LINKS_TO_FIXam_FILES_MAPPING", "FIXgsm_FILES_TO_COPY_TO_FIXam" ]
        export_vars(env_vars=EXPORTS)

    return sdf_uses_thompson_mp