diff --git a/compass/landice/extrapolate.py b/compass/landice/extrapolate.py index fb388976e4..3e43737404 100644 --- a/compass/landice/extrapolate.py +++ b/compass/landice/extrapolate.py @@ -4,7 +4,9 @@ from netCDF4 import Dataset -def extrapolate_variable(nc_file, var_name, extrap_method, set_value=None): +def extrapolate_variable(nc_file, var_name, extrap_method='auto', # noqa: C901 + valid_region_method='auto', + set_value=None): """ Function to extrapolate variable values into undefined regions @@ -19,6 +21,9 @@ def extrapolate_variable(nc_file, var_name, extrap_method, set_value=None): extrap_method : str idw, min, or value method of extrapolation + valid_region_method : str + choice of how to define region of valid data + set_value : float value to set variable to outside keepCellMask when using -v value @@ -36,13 +41,33 @@ def extrapolate_variable(nc_file, var_name, extrap_method, set_value=None): xCell = dataset.variables["yCell"][:] yCell = dataset.variables["xCell"][:] + # Define extrap method + if extrap_method == 'auto': + if var_name in ["effectivePressure", "beta", "muFriction"]: + extrap_method = 'min' + elif var_name in ["floatingBasalMassBal"]: + extrap_method = 'idw' + else: + extrap_method = 'idw' + # Define region of good data to extrapolate from. - # Different methods for different variables - if var_name in ["effectivePressure", "beta", "muFriction"]: + if valid_region_method == 'auto': + if var_name in ["effectivePressure", "beta", "muFriction"]: + valid_region_method = 'grounded_ice' + elif var_name in ["floatingBasalMassBal"]: + valid_region_method = 'floating_ice' + else: + valid_region_method = 'ice' + + print(f"Start {var_name} extrapolation using {extrap_method} method, " + f"defining good data using {valid_region_method} method") + + # Calculate seed mask baed on method for defining valid data + if valid_region_method == 'positive_val': + keepCellMask = (varValue[:] > 0.0) + elif valid_region_method == 'grounded_ice': groundedMask = (thickness > (-1028.0 / 910.0 * bed)) keepCellMask = np.copy(groundedMask) - extrap_method == "min" - # grow mask by one cell oceanward of GL for iCell in range(nCells): for n in range(nEdgesOnCell[iCell]): @@ -51,13 +76,19 @@ def extrapolate_variable(nc_file, var_name, extrap_method, set_value=None): keepCellMask[iCell] = 1 continue # ensure zero muFriction does not get extrapolated - keepCellMask *= (varValue > 0) - elif var_name in ["floatingBasalMassBal"]: + keepCellMask *= (varValue > 0.0) + elif valid_region_method == 'floating_ice': floatingMask = (thickness <= (-1028.0 / 910.0 * bed)) keepCellMask = floatingMask * (varValue != 0.0) - extrap_method == "idw" - else: + elif valid_region_method == 'ice': keepCellMask = (thickness > 0.0) + else: + sys.exit("Unexpected value of valid_region_method encountered " + "in landice/extrapolate.py") + + if keepCellMask.sum() == 0: + sys.exit("In landice/extrapolate.py, initial keepCellMask has " + "no valid cells") # make a copy to edit that will be used later keepCellMaskNew = np.copy(keepCellMask) @@ -72,8 +103,6 @@ def extrapolate_variable(nc_file, var_name, extrap_method, set_value=None): # 5) Update mask # 6) go to step 1) - print("Start {} extrapolation using {} method".format(var_name, - extrap_method)) if extrap_method == 'value': varValue[np.where(np.logical_not(keepCellMask))] = float(set_value) else: diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py index 07c6a4ebec..8488ca87eb 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py @@ -10,6 +10,9 @@ from compass.landice.tests.ensemble_generator.ensemble_manager import ( EnsembleManager, ) +from compass.landice.tests.ensemble_generator.ensemble_template import ( + get_branch_template_package, +) from compass.testcase import TestCase @@ -59,6 +62,8 @@ def configure(self): """ config = self.config + resource_module = get_branch_template_package(config) + section = config['branch_ensemble'] spinup_test_dir = section.get('spinup_test_dir') @@ -89,7 +94,8 @@ def configure(self): else: print(f"Adding {run_name}") # use this run - self.add_step(BranchRun(test_case=self, run_num=run_num)) + self.add_step(BranchRun(test_case=self, run_num=run_num, + resource_module=resource_module)) # Note: do not add to steps_to_run; ensemble_manager # will handle submitting and running the runs diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/branch_ensemble.cfg b/compass/landice/tests/ensemble_generator/branch_ensemble/branch_ensemble.cfg index 78953eda17..761685344f 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/branch_ensemble.cfg +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/branch_ensemble.cfg @@ -1,27 +1,3 @@ -# config options for branching an ensemble -[branch_ensemble] - -# start and end numbers for runs to set up and run -# branch runs. -# It is assumed that spinup runs have already been -# conducted for these runs. -start_run = 0 -end_run = 3 - -# Path to thermal forcing file for the mesh to be used in the branch run -TF_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/ocean_thermal_forcing/UKESM1-0-LL_SSP585/1995-2300/Amery_4to20km_TF_UKESM1-0-LL_SSP585_2300.nc - -# Path to SMB forcing file for the mesh to be used in the branch run -SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/atmosphere_forcing/UKESM1-0-LL_SSP585/1995-2300/Amery_4to20km_SMB_UKESM1-0-LL_SSP585_2300_noBareLandAdvance.nc - -# location of spinup ensemble to branch from -spinup_test_dir = /pscratch/sd/h/hoffman2/AMERY_corrected_forcing_6param_ensemble_2023-03-18/landice/ensemble_generator/ensemble - -# year of spinup simulation from which to branch runs -branch_year = 2050 - -# whether to only set up branch runs for filtered runs or all runs -set_up_filtered_only = True - -# path to pickle file containing filtering information generated by plot_ensemble.py -ensemble_pickle_file = None +# branch_ensemble options are loaded from the selected model configuration +# package under: +# compass.landice.tests.ensemble_generator.ensemble_templates..branch diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/branch_run.py b/compass/landice/tests/ensemble_generator/branch_ensemble/branch_run.py index 864a751ff0..360fc1bded 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/branch_run.py +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/branch_run.py @@ -28,36 +28,9 @@ class BranchRun(Step): input_file_name : str name of the input file that was read from the config - basal_fric_exp : float - value of basal friction exponent to use - - mu_scale : float - value to scale muFriction by - - stiff_scale : float - value to scale stiffnessFactor by - - von_mises_threshold : float - value of von Mises stress threshold to use - - calv_spd_lim : float - value of calving speed limit to use - - gamma0 : float - value of gamma0 to use in ISMIP6 ice-shelf basal melt param. - - deltaT : float - value of deltaT to use in ISMIP6 ice-shelf basal melt param. """ - def __init__(self, test_case, run_num, - basal_fric_exp=None, - mu_scale=None, - stiff_scale=None, - von_mises_threshold=None, - calv_spd_lim=None, - gamma0=None, - deltaT=None): + def __init__(self, test_case, run_num, resource_module): """ Creates a new run within an ensemble @@ -68,8 +41,13 @@ def __init__(self, test_case, run_num, run_num : integer the run number for this ensemble member + + resource_module : str + Package containing configuration-specific branch namelist and + streams templates """ self.run_num = run_num + self.resource_module = resource_module # define step (run) name self.name = f'run{run_num:03}' @@ -108,9 +86,10 @@ def setup(self): with open(os.path.join(self.work_dir, 'restart_timestamp'), 'w') as f: f.write('2015-01-01_00:00:00') - # yaml file - shutil.copy(os.path.join(spinup_dir, 'albany_input.yaml'), - self.work_dir) + # albany_input.yaml may be absent in templates that do not use Albany. + albany_input = os.path.join(spinup_dir, 'albany_input.yaml') + if os.path.isfile(albany_input): + shutil.copy(albany_input, self.work_dir) # set up namelist # start with the namelist from the spinup @@ -120,8 +99,7 @@ def setup(self): 'namelist.landice')) # use the namelist in this module to update the spinup namelist options = compass.namelist.parse_replacements( - 'compass.landice.tests.ensemble_generator.branch_ensemble', - 'namelist.landice') + self.resource_module, 'namelist.landice') namelist = compass.namelist.replace(namelist, options) compass.namelist.write(namelist, os.path.join(self.work_dir, 'namelist.landice')) @@ -132,7 +110,7 @@ def setup(self): stream_replacements['TF_file_path'] = TF_file_path SMB_file_path = section.get('SMB_file_path') stream_replacements['SMB_file_path'] = SMB_file_path - strm_src = 'compass.landice.tests.ensemble_generator.branch_ensemble' + strm_src = self.resource_module self.add_streams_file(strm_src, 'streams.landice', out_name='streams.landice', diff --git a/compass/landice/tests/ensemble_generator/ensemble_member.py b/compass/landice/tests/ensemble_generator/ensemble_member.py index ca08833cff..83256ea093 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_member.py +++ b/compass/landice/tests/ensemble_generator/ensemble_member.py @@ -4,7 +4,7 @@ from importlib import resources import netCDF4 -import yaml +import numpy as np from compass.io import symlink from compass.job import write_job_script @@ -40,12 +40,6 @@ class EnsembleMember(Step): stiff_scale : float value to scale stiffnessFactor by - von_mises_threshold : float - value of von Mises stress threshold to use - - calv_spd_lim : float - value of calving speed limit to use - gamma0 : float value of gamma0 to use in ISMIP6 ice-shelf basal melt param. @@ -54,11 +48,12 @@ class EnsembleMember(Step): """ def __init__(self, test_case, run_num, + resource_module, + namelist_option_values=None, + namelist_parameter_values=None, basal_fric_exp=None, mu_scale=None, stiff_scale=None, - von_mises_threshold=None, - calv_spd_lim=None, gamma0=None, meltflux=None, deltaT=None): @@ -73,6 +68,18 @@ def __init__(self, test_case, run_num, run_num : integer the run number for this ensemble member + resource_module : str + Package containing configuration-specific namelist, streams, + and albany input files + + namelist_option_values : dict, optional + A dictionary of namelist option names and values to be + overridden for this ensemble member + + namelist_parameter_values : dict, optional + A dictionary of run-info parameter names and values that + correspond to entries in ``namelist_option_values`` + basal_fric_exp : float value of basal friction exponent to use @@ -82,13 +89,6 @@ def __init__(self, test_case, run_num, stiff_scale : float value to scale stiffnessFactor by - von_mises_threshold : float - value of von Mises stress threshold to use - assumes same value for grounded and floating ice - - calv_spd_lim : float - value of calving speed limit to use - gamma0 : float value of gamma0 to use in ISMIP6 ice-shelf basal melt param. @@ -96,13 +96,18 @@ def __init__(self, test_case, run_num, value of deltaT to use in ISMIP6 ice-shelf basal melt param. """ self.run_num = run_num + self.resource_module = resource_module + if namelist_option_values is None: + namelist_option_values = {} + if namelist_parameter_values is None: + namelist_parameter_values = {} + self.namelist_option_values = dict(namelist_option_values) + self.namelist_parameter_values = dict(namelist_parameter_values) # store assigned param values for this run self.basal_fric_exp = basal_fric_exp self.mu_scale = mu_scale self.stiff_scale = stiff_scale - self.von_mises_threshold = von_mises_threshold - self.calv_spd_lim = calv_spd_lim self.gamma0 = gamma0 self.meltflux = meltflux self.deltaT = deltaT @@ -127,11 +132,12 @@ def setup(self): "'compass setup' again to set this experiment up.") return - resource_module = 'compass.landice.tests.ensemble_generator' + resource_module = self.resource_module # Get config for info needed for setting up simulation config = self.config - section = config['ensemble'] + section = config['ensemble_generator'] + spinup_section = config['spinup_ensemble'] # Create a python config (not compass config) file # for run-specific info useful for analysis/viz @@ -151,14 +157,17 @@ def setup(self): # Set up base run configuration self.add_namelist_file(resource_module, 'namelist.landice') - # copy over albany yaml file - # cannot use add_input functionality because need to modify the file - # in this function, and inputs don't get processed until after this - # function - with resources.path(resource_module, - 'albany_input.yaml') as package_path: - target = str(package_path) - shutil.copy(target, self.work_dir) + # albany_input.yaml is optional unless fric_exp perturbations are used. + albany_input_name = 'albany_input.yaml' + albany_input_path = os.path.join(self.work_dir, albany_input_name) + albany_source = resources.files(resource_module).joinpath( + albany_input_name) + # Materialize a real filesystem path in case the package is not + # directly on the filesystem (e.g., zip/loader-backed). + with resources.as_file(albany_source) as albany_source_path: + has_albany_input = albany_source_path.is_file() + if has_albany_input: + shutil.copy(str(albany_source_path), self.work_dir) self.add_model_as_input() @@ -171,41 +180,47 @@ def setup(self): options['config_adaptive_timestep_CFL_fraction'] = \ f'{self.cfl_fraction}' - # von Mises stress threshold - if self.von_mises_threshold is not None: - options['config_grounded_von_Mises_threshold_stress'] = \ - f'{self.von_mises_threshold}' - options['config_floating_von_Mises_threshold_stress'] = \ - f'{self.von_mises_threshold}' - run_info_cfg.set('run_info', 'von_mises_threshold', - f'{self.von_mises_threshold}') - - # calving speed limit - if self.calv_spd_lim is not None: - options['config_calving_speed_limit'] = \ - f'{self.calv_spd_lim}' - run_info_cfg.set('run_info', 'calv_spd_limit', - f'{self.calv_spd_lim}') + # apply generic namelist float parameter perturbations + for option_name, value in self.namelist_option_values.items(): + options[option_name] = f'{value}' + for parameter_name, value in self.namelist_parameter_values.items(): + run_info_cfg.set('run_info', parameter_name, f'{value}') # adjust basal friction exponent # rename and copy base file - input_file_path = section.get('input_file_path') + input_file_path = spinup_section.get('input_file_path') input_file_name = input_file_path.split('/')[-1] - base_fname = input_file_name.split('.')[:-1][0] - new_input_fname = f'{base_fname}_MODIFIED.nc' + base_fname = ".".join(input_file_name.split('.')[:-1]) + new_input_fname = f'{base_fname}_MODIFIED_run{self.run_num:03}.nc' self.input_file_name = new_input_fname # store for run method shutil.copy(input_file_path, os.path.join(self.work_dir, new_input_fname)) # set input filename in streams and create streams file stream_replacements = {'input_file_init_cond': new_input_fname} + if config.has_option('spinup_ensemble', 'fric_samples_file'): + fric_samples_file = spinup_section.get('fric_samples_file') + fric_map_file = spinup_section.get('fric_map_file') + mpas_cellid_file = spinup_section.get('mpas_cellid_file') + fric_sample_scale = spinup_section.getfloat('fric_sample_scale') + _apply_fric_sample( + sample_num=self.run_num, + sample_file=fric_samples_file, + mu_opt_file=fric_map_file, + mpas_cellid_file=mpas_cellid_file, + ic_file=os.path.join(self.work_dir, new_input_fname), + scaling=fric_sample_scale) + run_info_cfg.set('run_info', 'fric_sample_num', f'{self.run_num}') if self.basal_fric_exp is not None: + if not has_albany_input: + raise ValueError( + "Parameter 'fric_exp' requires 'albany_input.yaml' " + f"in template package '{resource_module}'.") # adjust mu and exponent - orig_fric_exp = section.getfloat('orig_fric_exp') + orig_fric_exp = spinup_section.getfloat('orig_fric_exp') _adjust_friction_exponent(orig_fric_exp, self.basal_fric_exp, os.path.join(self.work_dir, new_input_fname), - os.path.join(self.work_dir, - 'albany_input.yaml')) + albany_input_path) run_info_cfg.set('run_info', 'basal_fric_exp', f'{self.basal_fric_exp}') @@ -225,17 +240,25 @@ def setup(self): f.close() run_info_cfg.set('run_info', 'stiff_scale', f'{self.stiff_scale}') - # adjust gamma0 and deltaT - # (only need to check one of these params) - basal_melt_param_file_path = section.get('basal_melt_param_file_path') - basal_melt_param_file_name = basal_melt_param_file_path.split('/')[-1] - base_fname = basal_melt_param_file_name.split('.')[:-1][0] - new_fname = f'{base_fname}_MODIFIED.nc' - shutil.copy(basal_melt_param_file_path, - os.path.join(self.work_dir, new_fname)) - _adjust_basal_melt_params(os.path.join(self.work_dir, new_fname), - self.gamma0, self.deltaT) - stream_replacements['basal_melt_param_file_name'] = new_fname + # adjust gamma0 and deltaT (if a basal melt parameter file is used) + if config.has_option('spinup_ensemble', 'basal_melt_param_file_path'): + basal_melt_param_file_path = spinup_section.get( + 'basal_melt_param_file_path') + basal_melt_param_file_name = \ + basal_melt_param_file_path.split('/')[-1] + base_fname = ".".join(basal_melt_param_file_name.split('.')[:-1]) + new_fname = f'{base_fname}_MODIFIED_run{self.run_num:03}.nc' + shutil.copy(basal_melt_param_file_path, + os.path.join(self.work_dir, new_fname)) + _adjust_basal_melt_params(os.path.join(self.work_dir, new_fname), + self.gamma0, self.deltaT) + stream_replacements['basal_melt_param_file_name'] = new_fname + elif any(value is not None for value in + (self.gamma0, self.meltflux, self.deltaT)): + raise ValueError( + "Parameters 'gamma0' and/or 'meltflux' require " + "'basal_melt_param_file_path' in [spinup_ensemble].") + if self.gamma0 is not None: run_info_cfg.set('run_info', 'gamma0', f'{self.gamma0}') if self.deltaT is not None: @@ -243,10 +266,13 @@ def setup(self): run_info_cfg.set('run_info', 'deltaT', f'{self.deltaT}') # set up forcing files (unmodified) - TF_file_path = section.get('TF_file_path') + TF_file_path = spinup_section.get('TF_file_path') stream_replacements['TF_file_path'] = TF_file_path - SMB_file_path = section.get('SMB_file_path') + SMB_file_path = spinup_section.get('SMB_file_path') stream_replacements['SMB_file_path'] = SMB_file_path + if config.has_option('spinup_ensemble', 'runoff_file_path'): + stream_replacements['runoff_file_path'] = spinup_section.get( + 'runoff_file_path') # store accumulated namelist and streams options self.add_namelist_options(options=options, @@ -300,6 +326,8 @@ def _adjust_friction_exponent(orig_fric_exp, new_fric_exp, filename, uX = f.variables['uReconstructX'][0, :, -1] uY = f.variables['uReconstructY'][0, :, -1] spd = (uX**2 + uY**2)**0.5 * (60. * 60. * 24. * 365.) + if np.max(spd) == 0.0: + raise ValueError("uReconstructX/Y yielding max speed of 0") mu = mu * spd**(orig_fric_exp - new_fric_exp) # The previous step leads to infs or nans in ice-free areas. # Set them all to 0.0 for the extrapolation step @@ -307,24 +335,24 @@ def _adjust_friction_exponent(orig_fric_exp, new_fric_exp, filename, f.variables['muFriction'][0, :] = mu[:] f.close() - extrapolate_variable(filename, 'muFriction', 'min') + extrapolate_variable(filename, 'muFriction', + extrap_method='min', + valid_region_method='positive_val') # now set exp in albany yaml file - with open(albany_input_yaml, 'r') as stream: - try: - loaded = yaml.safe_load(stream) - except yaml.YAMLError as exc: - print(exc) - # Change value - (loaded['ANONYMOUS']['Problem']['LandIce BCs']['BC 0'] - ['Basal Friction Coefficient'] - ['Power Exponent']) = float(new_fric_exp) - # write out again - with open(albany_input_yaml, 'w') as stream: - try: - yaml.dump(loaded, stream, default_flow_style=False) - except yaml.YAMLError as exc: - print(exc) + # using text replacement rather than yaml parser because yaml + # parser can sometimes mangle Albany yaml files due to formatting not + # being fully yaml-compliant. It also writes out the yaml in an + # arbitrary ordering, which is inconvenient. + with open(albany_input_yaml, 'r') as file: + yamldata = file.readlines() + for linenum in range(len(yamldata)): + line = yamldata[linenum] + if 'Power Exponent' in line: + yamldata[linenum] = (line.split(':')[0] + + f': {float(new_fric_exp)}\n') + with open(albany_input_yaml, 'w') as file: + file.writelines(yamldata) def _adjust_basal_melt_params(filename, gamma0=None, deltaT=None): @@ -341,3 +369,31 @@ def _adjust_basal_melt_params(filename, gamma0=None, deltaT=None): if deltaT is not None: f.variables['ismip6shelfMelt_deltaT'][:] = deltaT f.close() + + +def _apply_fric_sample(sample_num, sample_file, mu_opt_file, + mpas_cellid_file, ic_file, scaling): + """Apply a precomputed friction sample to ``muFriction`` in IC file.""" + samples = np.load(sample_file) + sample = samples[sample_num, :] + + log_mu_opt = np.loadtxt(mu_opt_file) + log_mu_opt = log_mu_opt[1:] # skip first row containing array size + + mpas_map = np.loadtxt(mpas_cellid_file, dtype='int') + mpas_map = mpas_map[1:] # skip first row containing array size + + f = netCDF4.Dataset(ic_file, 'r+') + f.set_auto_mask(False) + n_cells = len(f.dimensions['nCells']) + + # Set baseline mu outside of the Albany inverse region. + mu = -1.0 * np.ones(n_cells) + mu[mpas_map[:] - 1] = np.exp(scaling * sample + log_mu_opt) + + f.variables['muFriction'][0, :] = mu[:] + f.close() + + extrapolate_variable(ic_file, 'muFriction', + extrap_method='min', + valid_region_method='positive_val') diff --git a/compass/landice/tests/ensemble_generator/ensemble_template.py b/compass/landice/tests/ensemble_generator/ensemble_template.py new file mode 100644 index 0000000000..31a1926296 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_template.py @@ -0,0 +1,70 @@ +def get_ensemble_template_name(config): + """ + Get the configured ensemble template name. + + Parameters + ---------- + config : compass.config.CompassConfigParser + Configuration options for a test case + + Returns + ------- + str + The selected ensemble template name + """ + section = 'ensemble_generator' + option = 'ensemble_template' + + if not config.has_section(section): + raise ValueError( + f"Missing required config section '{section}' for ensemble " + "generator configuration selection.") + + if not config.has_option(section, option): + raise ValueError( + f"Missing required config option '{option}' in section " + f"'{section}'.") + + template = config.get(section, option).strip() + if template == '': + raise ValueError('ensemble_template cannot be empty.') + + return template + + +def get_spinup_template_package(config): + """ + Get the package containing spinup ensemble template resources. + + Parameters + ---------- + config : compass.config.CompassConfigParser + Configuration options for a test case + + Returns + ------- + str + Package path for spinup resources + """ + template = get_ensemble_template_name(config) + return ('compass.landice.tests.ensemble_generator.ensemble_templates.' + f'{template}.spinup') + + +def get_branch_template_package(config): + """ + Get the package containing branch ensemble template resources. + + Parameters + ---------- + config : compass.config.CompassConfigParser + Configuration options for a test case + + Returns + ------- + str + Package path for branch resources + """ + template = get_ensemble_template_name(config) + return ('compass.landice.tests.ensemble_generator.ensemble_templates.' + f'{template}.branch') diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/branch_ensemble.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/branch_ensemble.cfg new file mode 100644 index 0000000000..709c9fbd68 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/branch_ensemble.cfg @@ -0,0 +1,33 @@ +# selector for ensemble template resources +[ensemble_generator] + +# subdirectory within ensemble_templates/ where branch_ensemble options are located +ensemble_template = default + +# config options for branching an ensemble +[branch_ensemble] + +# start and end numbers for runs to set up and run +# branch runs. +# It is assumed that spinup runs have already been +# conducted for these runs. +start_run = 0 +end_run = 3 + +# Path to thermal forcing file for the mesh to be used in the branch run +TF_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/ocean_thermal_forcing/UKESM1-0-LL_SSP585/1995-2300/Amery_4to20km_TF_UKESM1-0-LL_SSP585_2300.nc + +# Path to SMB forcing file for the mesh to be used in the branch run +SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/atmosphere_forcing/UKESM1-0-LL_SSP585/1995-2300/Amery_4to20km_SMB_UKESM1-0-LL_SSP585_2300_noBareLandAdvance.nc + +# location of spinup ensemble to branch from +spinup_test_dir = /pscratch/sd/h/hoffman2/AMERY_corrected_forcing_6param_ensemble_2023-03-18/landice/ensemble_generator/ensemble + +# year of spinup simulation from which to branch runs +branch_year = 2050 + +# whether to only set up branch runs for filtered runs or all runs +set_up_filtered_only = True + +# path to pickle file containing filtering information generated by plot_ensemble.py +ensemble_pickle_file = None diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/namelist.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/namelist.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/branch_ensemble/namelist.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/namelist.landice diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/streams.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/streams.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/branch_ensemble/streams.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/branch/streams.landice diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/albany_input.yaml b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/albany_input.yaml similarity index 100% rename from compass/landice/tests/ensemble_generator/albany_input.yaml rename to compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/albany_input.yaml diff --git a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/ensemble_generator.cfg similarity index 63% rename from compass/landice/tests/ensemble_generator/ensemble_generator.cfg rename to compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/ensemble_generator.cfg index 4cbab8b830..035c871f5e 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/ensemble_generator.cfg @@ -1,28 +1,36 @@ +# selector for ensemble template resources +[ensemble_generator] + +# subdirectory within ensemble_templates/ where branch_ensemble options are located +ensemble_template = default + # config options for setting up an ensemble -[ensemble] # start and end numbers for runs to set up and run # Run numbers should be zero-based. # Additional runs can be added and run to an existing ensemble # without affecting existing runs, but trying to set up a run # that already exists will generate a warning and skip that run. -# If using uniform sampling, start_run should be 0 and end_run should be -# equal to (max_samples - 1), otherwise unexpected behavior may result. +# If using uniform or log-uniform sampling, start_run should be 0 and +# end_run should be equal to (max_samples - 1), otherwise unexpected +# behavior may result. # These values do not affect viz/analysis, which will include any # runs it finds. start_run = 0 end_run = 3 -# sampling_method can be either 'sobol' for a space-filling Sobol sequence -# or 'uniform' for uniform sampling. Uniform sampling is most appropriate -# for a single parameter sensitivity study. It will sample uniformly across -# all dimensions simultaneously, thus sampling only a small fraction of -# parameter space +# sampling_method can be 'sobol' for a space-filling Sobol sequence, +# 'uniform' for linear sampling, or 'log-uniform' for logarithmic sampling. +# Uniform and log-uniform are most appropriate for a single-parameter +# sensitivity study because they sample each active parameter using the +# same rank ordering, thus sampling only a small fraction of parameter space +# in higher dimensions. sampling_method = sobol # maximum number of samples to be considered. # max_samples needs to be greater or equal to (end_run + 1) -# When using uniform sampling, max_samples should equal (end_run + 1). +# When using uniform or log-uniform sampling, max_samples should equal +# (end_run + 1). # When using Sobol sequence, max_samples ought to be a power of 2. # max_samples should not be changed after the first set of ensemble. # So, when using Sobol sequence, max_samples might be set larger than @@ -53,6 +61,12 @@ basin = ISMIP6BasinBC # to inform the choice for a large production ensemble. cfl_fraction = 0.7 +# number of tasks that each ensemble member should be run with +# Eventually, compass could determine this, but we want explicit control for now +ntasks = 128 + +[spinup_ensemble] + # Path to the initial condition input file. # Eventually this could be hard-coded to use files on the input data # server, but initially we want flexibility to experiment with different @@ -72,65 +86,37 @@ TF_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_fr # Path to SMB forcing file for the mesh to be used SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/atmosphere_forcing/RACMO_climatology_1995-2017/Amery_4to20km_RACMO2.3p2_ANT27_smb_climatology_1995-2017_no_xtime_noBareLandAdvance.nc -# number of tasks that each ensemble member should be run with -# Eventually, compass could determine this, but we want explicit control for now -ntasks = 128 +# Optional friction-sample inputs from Albany optimization products. +# When these options are present, run N uses sample index N. +# fric_samples_file = /path/to/postVarSamples-5000.npy +# fric_map_file = /path/to/mu_log_opt.ascii +# mpas_cellid_file = /path/to/mpas_cellID.ascii +# fric_sample_scale = 0.25 -# whether basal friction exponent is being varied -# [unitless] -use_fric_exp = True -# min value to vary over -fric_exp_min = 0.1 -# max value to vary over -fric_exp_max = 0.33333 - -# whether a scaling factor on muFriction is being varied -# [unitless: 1.0=no scaling] -use_mu_scale = True -# min value to vary over -mu_scale_min = 0.8 -# max value to vary over -mu_scale_max = 1.2 - -# whether a scaling factor on stiffnessFactor is being varied -# [unitless: 1.0=no scaling] -use_stiff_scale = True -# min value to vary over -stiff_scale_min = 0.8 -# max value to vary over -stiff_scale_max = 1.2 - -# whether the von Mises threshold stress (sigma_max) is being varied -# [units: Pa] -use_von_mises_threshold = True -# min value to vary over -von_mises_threshold_min = 80.0e3 -# max value to vary over -von_mises_threshold_max = 180.0e3 - -# whether the calving speed limit is being varied -# [units: km/yr] -use_calv_limit = False -# min value to vary over -calv_limit_min = 5.0 -# max value to vary over -calv_limit_max = 50.0 - -# whether ocean melt parameterization coefficient is being varied -# [units: m/yr] -use_gamma0 = True -# min value to vary over -gamma0_min = 9620.0 -# max value to vary over -gamma0_max = 471000.0 - -# whether target ice-shelf basal melt flux is being varied -# [units: Gt/yr] -use_meltflux = True -# min value to vary over -meltflux_min = 12. -# max value to vary over -meltflux_max = 58. -# ice-shelf area associated with target melt rates -# [units: m^2] +# For meltflux perturbations, this observed ice-shelf area is used when +# converting target melt flux to deltaT. iceshelf_area_obs = 60654.e6 + +# Parameter definitions are listed in this section in sampling order. +# Use the prefix "nl." for float parameters that map to namelist options. +# Each parameter must define " = min, max". +# Namelist parameters must also define +# ".option_name = namelist_option". +[ensemble.parameters] + +# special parameters (handled by custom code) +fric_exp = 0.1, 0.33333 +mu_scale = 0.8, 1.2 +stiff_scale = 0.8, 1.2 +gamma0 = 9620.0, 471000.0 +meltflux = 12.0, 58.0 + +# namelist float parameters (generic handling) +nl.von_mises_threshold = 80.0e3, 180.0e3 +nl.von_mises_threshold.option_name = \ + config_grounded_von_Mises_threshold_stress, \ + config_floating_von_Mises_threshold_stress + +# example for calving speed limit (units must match namelist units) +# nl.calv_spd_limit = 0.0001585, 0.001585 +# nl.calv_spd_limit.option_name = config_calving_speed_limit diff --git a/compass/landice/tests/ensemble_generator/namelist.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/namelist.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/namelist.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/namelist.landice diff --git a/compass/landice/tests/ensemble_generator/streams.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/streams.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/streams.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/amery4km_probproj_2024/spinup/streams.landice diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/branch_ensemble.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/branch_ensemble.cfg new file mode 100644 index 0000000000..57ca54112c --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/branch_ensemble.cfg @@ -0,0 +1,33 @@ +# selector for ensemble template resources +[ensemble_generator] + +# subdirectory within ensemble_templates/ where branch_ensemble options are located +ensemble_template = kanger_frictionuq_2026 + +# config options for branching an ensemble +[branch_ensemble] + +# start and end numbers for runs to set up and run +# branch runs. +# It is assumed that spinup runs have already been +# conducted for these runs. +start_run = 0 +end_run = 7 + +# Path to thermal forcing file for the mesh to be used in the branch run +TF_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Kanger_configuration_2025/Kanger_MIROC5-rcp85_oceanThermalForcing_AND_runoff_1960-1989_mean.nc + +# Path to SMB forcing file for the mesh to be used in the branch run +SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Kanger_configuration_2025/Kanger_MIROC5_refSMB_minus1_bare_land.nc + +# location of spinup ensemble to branch from +spinup_test_dir = /pscratch/sd/h/hoffman2/KANGER_uq_spinup/landice/ensemble_generator/spinup_ensemble + +# year of spinup simulation from which to branch runs +branch_year = 2050 + +# whether to only set up branch runs for filtered runs or all runs +set_up_filtered_only = True + +# path to pickle file containing filtering information generated by plot_ensemble.py +ensemble_pickle_file = None diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/namelist.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/namelist.landice new file mode 100644 index 0000000000..9564610e57 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/namelist.landice @@ -0,0 +1,8 @@ +config_do_restart = .true. +config_start_time = 'file' +config_stop_time = '2300-01-01_00:00:00' +config_grounded_von_Mises_threshold_stress = 1.0e9 +config_min_adaptive_timestep = 21600 +config_calving_error_threshold = 1.0e9 +config_front_mass_bal_grounded = 'ismip6' +config_use_3d_thermal_forcing_for_face_melt = .true. diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/streams.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/streams.landice new file mode 100644 index 0000000000..f1cb2af75f --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/branch/streams.landice @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/albany_input.yaml b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/albany_input.yaml new file mode 100644 index 0000000000..cb533ba271 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/albany_input.yaml @@ -0,0 +1,236 @@ +%YAML 1.1 +--- +ANONYMOUS: + Problem: + Depth Integrated Model: true + LandIce Field Norm: + sliding_velocity_basalside: + Regularization Type: Given Value + Regularization Value: 1.0e-4 + LandIce BCs: + BC 0: + Basal Friction Coefficient: + Type: Power Law + Power Exponent: 0.33333333 + Mu Type: Field + Effective Pressure Type: Hydrostatic Computed at Nodes + Zero Effective Pressure On Floating Ice At Nodes: true + +# Discretization Description +# Discretization: + #Exodus Output File Name: albany_output.exo + + Piro: +# Nonlinear Solver Information + NOX: + Nonlinear Solver: Line Search Based + Line Search: + Full Step: + Full Step: 1.0e+00 + Method: Backtrack + Solver Options: + Status Test Check Type: Minimal + Status Tests: + Test Type: Combo + Combo Type: OR + Number of Tests: 2 + Test 0: + Test Type: NormF + Norm Type: Two Norm + Scale Type: Scaled + Tolerance: 1.0e-05 + Test 1: + Test Type: MaxIters + Maximum Iterations: 70 + Printing: + Output Precision: 3 + Output Processor: 0 + Output Information: + Error: true + Warning: true + Outer Iteration: true + Parameters: false + Details: false + Linear Solver Details: false + Stepper Iteration: true + Stepper Details: true + Stepper Parameters: true + + Direction: + Method: Newton + Newton: + Forcing Term Method: Type 2 + Rescue Bad Newton Solve: true + Linear Solver: + Write Linear System: false + Tolerance: 1.0e-8 + + Stratimikos Linear Solver: + Stratimikos: + +# Linear Solver Information + Linear Solver Type: Belos + Linear Solver Types: + Belos: + Solver Type: Block GMRES + Solver Types: + Block GMRES: + Output Frequency: 20 + Output Style: 1 + Verbosity: 33 + Maximum Iterations: 200 + Block Size: 1 + Num Blocks: 200 + Flexible Gmres: false + VerboseObject: + Output File: none + Verbosity Level: low + +# Preconditioner Information + Preconditioner Type: MueLu + Preconditioner Types: + + Ifpack2: + Overlap: 1 + Prec Type: ILUT + + MueLu: + Matrix: + PDE equations: 2 + Factories: + myLineDetectionFact: + factory: LineDetectionFactory + 'linedetection: orientation': coordinates + mySemiCoarsenPFact1: + factory: SemiCoarsenPFactory + 'semicoarsen: coarsen rate': 14 + UncoupledAggregationFact2: + factory: UncoupledAggregationFactory + 'aggregation: ordering': graph + 'aggregation: max selected neighbors': 0 + 'aggregation: min agg size': 3 + 'aggregation: phase3 avoid singletons': true + MyCoarseMap2: + factory: CoarseMapFactory + Aggregates: UncoupledAggregationFact2 + myTentativePFact2: + 'tentative: calculate qr': true + factory: TentativePFactory + Aggregates: UncoupledAggregationFact2 + CoarseMap: MyCoarseMap2 + mySaPFact2: + 'sa: eigenvalue estimate num iterations': 10 + 'sa: damping factor': 1.33333e+00 + factory: SaPFactory + P: myTentativePFact2 + myTransferCoordinatesFact: + factory: CoordinatesTransferFactory + CoarseMap: MyCoarseMap2 + Aggregates: UncoupledAggregationFact2 + myTogglePFact: + factory: TogglePFactory + 'semicoarsen: number of levels': 2 + TransferFactories: + P1: mySemiCoarsenPFact1 + P2: mySaPFact2 + Ptent1: mySemiCoarsenPFact1 + Ptent2: myTentativePFact2 + Nullspace1: mySemiCoarsenPFact1 + Nullspace2: myTentativePFact2 + myRestrictorFact: + factory: TransPFactory + P: myTogglePFact + myToggleTransferCoordinatesFact: + factory: ToggleCoordinatesTransferFactory + Chosen P: myTogglePFact + TransferFactories: + Coordinates1: mySemiCoarsenPFact1 + Coordinates2: myTransferCoordinatesFact + myRAPFact: + factory: RAPFactory + P: myTogglePFact + R: myRestrictorFact + TransferFactories: + For Coordinates: myToggleTransferCoordinatesFact + myRepartitionHeuristicFact: + factory: RepartitionHeuristicFactory + A: myRAPFact + 'repartition: min rows per proc': 3000 + 'repartition: max imbalance': 1.327e+00 + 'repartition: start level': 1 + myZoltanInterface: + factory: ZoltanInterface + A: myRAPFact + Coordinates: myToggleTransferCoordinatesFact + number of partitions: myRepartitionHeuristicFact + myRepartitionFact: + factory: RepartitionFactory + A: myRAPFact + Partition: myZoltanInterface + 'repartition: remap parts': true + number of partitions: myRepartitionHeuristicFact + myRebalanceProlongatorFact: + factory: RebalanceTransferFactory + type: Interpolation + P: myTogglePFact + Coordinates: myToggleTransferCoordinatesFact + Nullspace: myTogglePFact + myRebalanceRestrictionFact: + factory: RebalanceTransferFactory + type: Restriction + R: myRestrictorFact + myRebalanceAFact: + factory: RebalanceAcFactory + A: myRAPFact + TransferFactories: { } + mySmoother1: + factory: TrilinosSmoother + type: LINESMOOTHING_BANDEDRELAXATION + 'smoother: pre or post': both + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 1 + 'relaxation: damping factor': 1.0 + mySmoother3: + factory: TrilinosSmoother + type: RELAXATION + 'smoother: pre or post': both + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 1 + 'relaxation: damping factor': 1.0 + mySmoother4: + factory: TrilinosSmoother + type: RELAXATION + 'smoother: pre or post': pre + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 4 + 'relaxation: damping factor': 1.0 + Hierarchy: + max levels: 7 + 'coarse: max size': 2000 + verbosity: None + Finest: + Smoother: mySmoother1 + CoarseSolver: mySmoother4 + P: myRebalanceProlongatorFact + Nullspace: myRebalanceProlongatorFact + CoarseNumZLayers: myLineDetectionFact + LineDetection_Layers: myLineDetectionFact + LineDetection_VertLineIds: myLineDetectionFact + A: myRebalanceAFact + Coordinates: myRebalanceProlongatorFact + Importer: myRepartitionFact + All: + startLevel: 1 + Smoother: mySmoother4 + CoarseSolver: mySmoother4 + P: myRebalanceProlongatorFact + Nullspace: myRebalanceProlongatorFact + CoarseNumZLayers: myLineDetectionFact + LineDetection_Layers: myLineDetectionFact + LineDetection_VertLineIds: myLineDetectionFact + A: myRebalanceAFact + Coordinates: myRebalanceProlongatorFact + Importer: myRepartitionFact diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/ensemble_generator.cfg new file mode 100644 index 0000000000..8b2cfc60db --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/ensemble_generator.cfg @@ -0,0 +1,105 @@ +# selector for ensemble template resources +[ensemble_generator] + +# subdirectory within ensemble_templates/ where branch_ensemble options are located +ensemble_template = kanger_frictionuq_2026 + +# config options for setting up an ensemble + +# start and end numbers for runs to set up and run +# Run numbers should be zero-based. +# Additional runs can be added and run to an existing ensemble +# without affecting existing runs, but trying to set up a run +# that already exists will generate a warning and skip that run. +# If using uniform or log-uniform sampling, start_run should be 0 and +# end_run should be equal to (max_samples - 1), otherwise unexpected +# behavior may result. +# These values do not affect viz/analysis, which will include any +# runs it finds. +start_run = 0 +end_run = 7 + +# sampling_method can be 'sobol' for a space-filling Sobol sequence, +# 'uniform' for linear sampling, or 'log-uniform' for logarithmic sampling. +# Uniform and log-uniform are most appropriate for a single-parameter +# sensitivity study because they sample each active parameter using the +# same rank ordering, thus sampling only a small fraction of parameter space +# in higher dimensions. +sampling_method = sobol + +# maximum number of samples to be considered. +# max_samples needs to be greater or equal to (end_run + 1) +# When using uniform or log-uniform sampling, max_samples should equal +# (end_run + 1). +# When using Sobol sequence, max_samples ought to be a power of 2. +# max_samples should not be changed after the first set of ensemble. +# So, when using Sobol sequence, max_samples might be set larger than +# (end_run + 1) if you plan to add more samples to the ensemble later. +max_samples = 1024 + +# basin for comparing model results with observational estimates in +# visualization script. +# Basin options are defined in compass/landice/ais_observations.py +# If desired basin does not exist, it can be added to that dataset. +# (They need not be mutually exclusive.) +# If a basin is not provided, observational comparisons will not be made. +basin = None + +# fraction of CFL-limited time step to be used by the adaptive timestepper +cfl_fraction = 0.5 + +# number of tasks that each ensemble member should be run with +ntasks = 128 + +[spinup_ensemble] + +# Path to the initial condition input file. +input_file_path = /global/cfs/cdirs/fanssie/MALI_input_files/Kangerlussuaq_1to10km_r03/Kangerlussuaq_1to10km_r03_20251201.nc + +# the value of the friction exponent used for the calculation of muFriction +# in the input file +orig_fric_exp = 0.3333333333 + +# Path to thermal forcing file for the mesh to be used +TF_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Kanger_configuration_2025/Kanger_MIROC5-rcp85_oceanThermalForcing_AND_runoff_1960-1989_mean.nc + +# Path to SMB forcing file for the mesh to be used +SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Kanger_configuration_2025/Kanger_MIROC5_refSMB_minus1_bare_land.nc + +# Path to runoff forcing file for the mesh to be used +runoff_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Kanger_configuration_2025/Kanger_MIROC5-rcp85_oceanThermalForcing_AND_runoff_1960-1989_mean.nc + +# Basal friction sample inputs from Albany optimization products. +# Run N consumes sample index N from fric_samples_file. +mpas_cellid_file = /global/cfs/cdirs/fanssie/users/mperego/forMatt/KangerUQ/mesh/mpas_cellID.ascii +fric_samples_file = /global/cfs/cdirs/fanssie/users/mperego/laplace-ensemble/kanger_data/postVarSamples_s1-l10-mlhfwd-moderronly.npy +fric_map_file = /global/cfs/cdirs/fanssie/users/mperego/laplace-ensemble/kanger_data/mu_log_opt_s1-l10-mlhfwd-moderronly.ascii +fric_sample_scale = 1.0 + +# Parameter definitions are listed in this section in sampling order. +# Use the prefix "nl." for float parameters that map to namelist options. +# Each parameter must define " = min, max". +# Namelist parameters must also define +# ".option_name = namelist_option". +[ensemble.parameters] + +# special parameters (handled by custom code) +stiff_scale = 0.8, 1.2 + +#fric_exp = 0.1, 0.333333 + +# namelist float parameters (generic handling) +nl.crev_water_depth = 0.0, 30.0 +nl.crev_water_depth.option_name = config_crevasse_water_depth + +nl.calving_multiplier = -0.5, 10.0 +nl.calving_multiplier.option_name = config_calving_strainrate_scaling + + + +[parallel] +account = m4274 +qos = regular + +[job] +wall_time = 1:00:00 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/namelist.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/namelist.landice new file mode 100644 index 0000000000..62fa397f35 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/namelist.landice @@ -0,0 +1,56 @@ + config_velocity_solver = 'FO' + config_unrealistic_velocity = 0.00317 ! 100 km/yr + config_nonconvergence_error = .false. + config_flowParamA_calculation = 'PB1982' ! required for VM calving + + config_thickness_advection = 'fo' + config_tracer_advection = 'fo' + + config_calving = 'crevasse_depth' + config_crevasse_water_depth = 0.0 + config_apply_facemelt_strainrate_enhancement = .true. + config_calving_strainrate_scaling = 0.0 + + config_calving_speed_limit = 0.000952 ! 30 km/yr + config_calculate_damage = .false. + config_restore_calving_front = .false. + config_remove_icebergs = .true. + config_remove_small_islands = .true. + config_distribute_unablatedVolumeDynCell = .true. + config_calving_error_threshold = 9.0e36 + + config_thermal_solver = 'temperature' + config_thermal_calculate_bmb = .true. + config_temperature_init = 'file' + config_thermal_thickness = 0.0 + config_surface_air_temperature_source = 'file' + config_basal_heat_flux_source = 'file' + + config_basal_mass_bal_float = 'none' + config_front_mass_bal_grounded = 'ismip6' + config_use_3d_thermal_forcing_for_face_melt = .false. + config_subglacial_discharge_intercept = 0.15 + + config_ice_density = 910.0 + config_ocean_density = 1028.0 + config_dynamic_thickness = 10.0 + + config_adaptive_timestep = .true. + config_adaptive_timestep_calvingCFL_fraction = 0.8 + config_adaptive_timestep_include_calving = .true. + config_adaptive_timestep_CFL_fraction = 0.2 + config_adaptive_timestep_force_interval = '0001-00-00_00:00:00' + + config_do_restart = .false. + config_restart_timestamp_name = 'restart_timestamp' + config_start_time = '2007-07-01_00:00:00' + config_stop_time = '2100-07-01_00:00:00' + + config_pio_num_iotasks = 1 + config_pio_stride = 32 + + config_AM_globalStats_enable = .true. + config_AM_globalStats_compute_interval = 'output_interval' + config_AM_globalStats_stream_name = 'globalStats' + config_AM_globalStats_compute_on_startup = .true. + config_AM_globalStats_write_on_startup = .true. diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/streams.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/streams.landice new file mode 100644 index 0000000000..6db8bbb8e6 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/kanger_frictionuq_2026/spinup/streams.landice @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compass/landice/tests/ensemble_generator/plot_ensemble.py b/compass/landice/tests/ensemble_generator/plot_ensemble.py index 14cdbaade4..23033d45ad 100644 --- a/compass/landice/tests/ensemble_generator/plot_ensemble.py +++ b/compass/landice/tests/ensemble_generator/plot_ensemble.py @@ -133,12 +133,13 @@ sys.exit("A usable cfg file for the ensemble was not found. " "Please correct the configuration or disable this check.") ens_cfg.read(ens_cfg_file) -ens_info = ens_cfg['ensemble'] +ens_info = ens_cfg['ensemble_generator'] if 'basin' in ens_info: basin = ens_info['basin'] if basin == 'None': basin = None -input_file_path = ens_info['input_file_path'] +spinup_info = ens_cfg['spinup_ensemble'] +input_file_path = spinup_info['input_file_path'] if basin is None: print("No basin found. Not using observational data.") else: @@ -379,7 +380,7 @@ def filter_run(): valid_run = False # calculate qoi's requiring spatial output - DS = xr.open_mfdataset(run + '/output/' + 'output_*.nc', + DS = xr.open_mfdataset(run + '/output/' + 'output*.nc', combine='nested', concat_dim='Time', decode_timedelta=False, chunks={"Time": 10}) diff --git a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index 1b6aae8a80..9121aaf7f9 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -10,8 +10,10 @@ from compass.landice.tests.ensemble_generator.ensemble_member import ( EnsembleMember, ) +from compass.landice.tests.ensemble_generator.ensemble_template import ( + get_spinup_template_package, +) from compass.testcase import TestCase -from compass.validate import compare_variables class SpinupEnsemble(TestCase): @@ -59,132 +61,56 @@ def configure(self): configure phase, we must explicitly add the steps to steps_to_run. """ - # Define some constants - rhoi = 910.0 - rhosw = 1028.0 - cp_seawater = 3.974e3 - latent_heat_ice = 335.0e3 - sec_in_yr = 3600.0 * 24.0 * 365.0 - c_melt = (rhosw * cp_seawater / (rhoi * latent_heat_ice))**2 + config = self.config + resource_module = get_spinup_template_package(config) - section = self.config['ensemble'] + section = config['ensemble_generator'] + spinup_section_name = 'spinup_ensemble' + if not config.has_section(spinup_section_name): + raise ValueError( + f"Missing required config section '{spinup_section_name}'.") + spinup_section = config[spinup_section_name] + parameter_section_name = 'ensemble.parameters' + if not config.has_section(parameter_section_name): + raise ValueError( + f"Missing required config section '{parameter_section_name}'.") + param_section = config[parameter_section_name] # Determine start and end run numbers being requested self.start_run = section.getint('start_run') self.end_run = section.getint('end_run') - # Define parameters being sampled and their ranges - param_list = ['fric_exp', 'mu_scale', 'stiff_scale', - 'von_mises_threshold', 'calv_limit', 'gamma0', - 'meltflux'] - - # Determine how many and which parameters are being used - n_params = 0 - param_dict = {} - for param in param_list: - param_dict[param] = {} - param_dict[param]['active'] = section.getboolean(f'use_{param}') - n_params += param_dict[param]['active'] + parameter_specs = _get_parameter_specs(param_section) + + # Determine how many parameters are being sampled. + n_params = len(parameter_specs) if n_params == 0: sys.exit("ERROR: At least one parameter must be specified.") - # Generate unit parameter vectors - either uniform or Sobol - sampling_method = section.get('sampling_method') max_samples = section.getint('max_samples') if max_samples < self.end_run: sys.exit("ERROR: max_samples is exceeded by end_run") - if sampling_method == 'sobol': - # Generate unit Sobol sequence for number of parameters being used - print(f"Generating Sobol sequence for {n_params} parameter(s)") - sampler = qmc.Sobol(d=n_params, scramble=True, seed=4) - param_unit_values = sampler.random(n=max_samples) - elif sampling_method == 'uniform': - print(f"Generating uniform sampling for {n_params} parameter(s)") - samples = np.linspace(0.0, 1.0, max_samples).reshape(-1, 1) - param_unit_values = np.tile(samples, (1, n_params)) - else: - sys.exit("ERROR: Unsupported sampling method specified.") - - # Define parameter vectors for each param being used - idx = 0 - for param in param_list: - if param_dict[param]['active']: - print('Including parameter ' + param) - min_val = section.getfloat(f'{param}_min') - max_val = section.getfloat(f'{param}_max') - param_dict[param]['vec'] = param_unit_values[:, idx] * \ - (max_val - min_val) + min_val - idx += 1 - else: - param_dict[param]['vec'] = np.full((max_samples,), None) - - # Deal with a few special cases - - # change units on calving speed limit from m/yr to s/yr - if param_dict['calv_limit']['active']: - param_dict['calv_limit']['vec'] = \ - param_dict['calv_limit']['vec'][:] / sec_in_yr - - # melt flux needs to be converted to deltaT - if param_dict['meltflux']['active']: - # First calculate mean TF for this domain - iceshelf_area_obs = section.getfloat('iceshelf_area_obs') - input_file_path = section.get('input_file_path') - TF_file_path = section.get('TF_file_path') - mean_TF, iceshelf_area = calc_mean_TF(input_file_path, - TF_file_path) - - # Adjust observed melt flux for ice-shelf area in init. condition - print(f'IS area: model={iceshelf_area}, Obs={iceshelf_area_obs}') - area_correction = iceshelf_area / iceshelf_area_obs - print(f"Ice-shelf area correction is {area_correction}.") - if (np.absolute(area_correction - 1.0) > 0.2): - print("WARNING: ice-shelf area correction is larger than " - "20%. Check data consistency before proceeding.") - param_dict['meltflux']['vec'] *= iceshelf_area / iceshelf_area_obs - - # Set up an array of TF values to use for linear interpolation - # Make it span a large enough range to capture deltaT what would - # be needed for the range of gamma0 values considered. - # Not possible to know a priori, so pick a wide range. - TFs = np.linspace(-5.0, 10.0, num=int(15.0 / 0.01)) - deltaT_vec = np.zeros(max_samples) - # For each run, calculate the deltaT needed to obtain the target - # melt flux - for ii in range(self.start_run, self.end_run + 1): - # spatially averaged version of ISMIP6 melt param.: - meltfluxes = (param_dict['gamma0']['vec'][ii] * c_melt * TFs * - np.absolute(TFs) * - iceshelf_area) * rhoi / 1.0e12 # Gt/yr - # interpolate deltaT value. Use nan values outside of range - # so out of range results get detected - deltaT_vec[ii] = np.interp(param_dict['meltflux']['vec'][ii], - meltfluxes, TFs, - left=np.nan, - right=np.nan) - mean_TF - if np.isnan(deltaT_vec[ii]): - sys.exit("ERROR: interpolated deltaT out of range. " - "Adjust definition of 'TFs'") - else: - deltaT_vec = [None] * max_samples - - # add runs as steps based on the run range requested - if self.end_run > max_samples: - sys.exit("Error: end_run specified in config exceeds maximum " - "sample size available in param_vector_filename") - for run_num in range(self.start_run, self.end_run + 1): - self.add_step(EnsembleMember( - test_case=self, run_num=run_num, - basal_fric_exp=param_dict['fric_exp']['vec'][run_num], - mu_scale=param_dict['mu_scale']['vec'][run_num], - stiff_scale=param_dict['stiff_scale']['vec'][run_num], - von_mises_threshold=param_dict['von_mises_threshold']['vec'][run_num], # noqa - calv_spd_lim=param_dict['calv_limit']['vec'][run_num], - gamma0=param_dict['gamma0']['vec'][run_num], - meltflux=param_dict['meltflux']['vec'][run_num], - deltaT=deltaT_vec[run_num])) - # Note: do not add to steps_to_run, because ensemble_manager - # will handle submitting and running the runs + sampling_method = section.get('sampling_method') + parameter_specs = _populate_parameter_vectors( + parameter_specs=parameter_specs, + sampling_method=sampling_method, + max_samples=max_samples) + if config.has_option('spinup_ensemble', 'fric_samples_file'): + print("Including friction samples from file. One friction " + "sample will be mapped to each run in the ensemble.") + + spec_by_name = {spec['name']: spec for spec in parameter_specs} + + deltaT_vec = _compute_delta_t_vec( + config=config, spinup_section=spinup_section, + spec_by_name=spec_by_name, + max_samples=max_samples, start_run=self.start_run, + end_run=self.end_run) + + _add_member_steps( + test_case=self, parameter_specs=parameter_specs, + spec_by_name=spec_by_name, deltaT_vec=deltaT_vec, + resource_module=resource_module, max_samples=max_samples) # Have 'compass run' only run the run_manager but not any actual runs. # This is because the individual runs will be submitted as jobs @@ -194,3 +120,279 @@ def configure(self): # no run() method is needed # no validate() method is needed + + +def _get_parameter_specs(section): + """Build parameter specification dictionaries from config options. + + Parameters with an ``nl.`` prefix are treated as namelist parameters and + include one or more target namelist option names. Other parameters are + interpreted as supported special parameters (for example ``gamma0``). + + Returns + ------- + list of dict + Ordered parameter metadata with sampled bounds and placeholders for + populated sample vectors. + """ + specs = [] + special_params = {'fric_exp', 'mu_scale', 'stiff_scale', + 'gamma0', 'meltflux'} + + for option_name, raw_value in section.items(): + if option_name.endswith('.option_name'): + continue + parameter_name = option_name + bounds = _parse_range(raw_value, parameter_name) + + if parameter_name.startswith('nl.'): + option_key = f'{parameter_name}.option_name' + if option_key not in section: + raise ValueError( + f"Namelist parameter '{parameter_name}' must define " + f"'{option_key}'.") + namelist_options = _split_entries(section[option_key]) + if len(namelist_options) == 0: + raise ValueError( + f"Namelist parameter '{parameter_name}' has no " + "option names configured.") + specs.append({ + 'name': parameter_name, + 'type': 'namelist', + 'run_info_name': parameter_name[len('nl.'):], + 'option_names': namelist_options, + 'min': bounds[0], + 'max': bounds[1], + 'vec': None + }) + else: + if parameter_name not in special_params: + raise ValueError( + f"Unsupported special parameter '{parameter_name}'.") + specs.append({ + 'name': parameter_name, + 'type': 'special', + 'min': bounds[0], + 'max': bounds[1], + 'vec': None + }) + + return specs + + +def _populate_parameter_vectors(parameter_specs, sampling_method, + max_samples): + """Generate and scale samples to each parameter range. + + This function updates each ``spec['vec']`` in ``parameter_specs`` and + returns the same list for explicit readability at call site. + ``sobol`` creates a space-filling sequence in unit space, + ``uniform`` creates linearly spaced samples, and ``log-uniform`` samples + linearly in log10 space (requiring strictly positive bounds). + + Returns + ------- + list of dict + The same ``parameter_specs`` list with each ``spec['vec']`` populated. + """ + n_params = len(parameter_specs) + if sampling_method == 'sobol': + print(f"Generating Sobol sequence for {n_params} parameter(s)") + sampler = qmc.Sobol(d=n_params, scramble=True, seed=4) + param_unit_values = sampler.random(n=max_samples) + elif sampling_method in {'uniform', 'log-uniform'}: + print(f"Generating {sampling_method} sampling for " + f"{n_params} parameter(s)") + samples = np.linspace(0.0, 1.0, max_samples).reshape(-1, 1) + param_unit_values = np.tile(samples, (1, n_params)) + else: + sys.exit("ERROR: Unsupported sampling method specified.") + + if sampling_method == 'log-uniform': + for spec in parameter_specs: + if spec['min'] <= 0.0 or spec['max'] <= 0.0: + sys.exit( + "ERROR: log-uniform sampling requires positive min/max " + f"for parameter '{spec['name']}'.") + + for idx, spec in enumerate(parameter_specs): + print('Including parameter ' + spec['name']) + if sampling_method == 'log-uniform': + log_min = np.log10(spec['min']) + log_max = np.log10(spec['max']) + spec['vec'] = 10.0 ** (param_unit_values[:, idx] * + (log_max - log_min) + log_min) + else: + spec['vec'] = param_unit_values[:, idx] * \ + (spec['max'] - spec['min']) + spec['min'] + return parameter_specs + + +def _compute_delta_t_vec(config, spinup_section, spec_by_name, max_samples, + start_run, end_run): + """Compute per-run ``deltaT`` values when ``meltflux`` is active. + + If ``meltflux`` is not sampled, this returns a list of ``None`` values. + When active, the function applies ice-shelf area correction to sampled + melt flux and interpolates the ``deltaT`` needed to match each target + melt flux over the requested run range. + + Returns + ------- + list or numpy.ndarray + ``[None] * max_samples`` when ``meltflux`` is inactive, otherwise a + ``numpy.ndarray`` containing per-run ``deltaT`` values. + """ + if 'meltflux' not in spec_by_name: + return [None] * max_samples + + if 'gamma0' not in spec_by_name: + sys.exit("ERROR: parameter 'meltflux' requires 'gamma0'.") + if not config.has_option('spinup_ensemble', 'iceshelf_area_obs'): + sys.exit( + "ERROR: parameter 'meltflux' requires " + "'iceshelf_area_obs' in [spinup_ensemble].") + + iceshelf_area_obs = spinup_section.getfloat('iceshelf_area_obs') + input_file_path = spinup_section.get('input_file_path') + TF_file_path = spinup_section.get('TF_file_path') + mean_TF, iceshelf_area = calc_mean_TF(input_file_path, TF_file_path) + + print(f'IS area: model={iceshelf_area}, Obs={iceshelf_area_obs}') + area_correction = iceshelf_area / iceshelf_area_obs + print(f"Ice-shelf area correction is {area_correction}.") + if np.absolute(area_correction - 1.0) > 0.2: + print("WARNING: ice-shelf area correction is larger than " + "20%. Check data consistency before proceeding.") + + spec_by_name['meltflux']['vec'] *= area_correction + + rhoi = 910.0 + rhosw = 1028.0 + cp_seawater = 3.974e3 + latent_heat_ice = 335.0e3 + c_melt = (rhosw * cp_seawater / (rhoi * latent_heat_ice))**2 + TFs = np.linspace(-5.0, 10.0, num=int(15.0 / 0.01)) + deltaT_vec = np.zeros(max_samples) + for ii in range(start_run, end_run + 1): + meltfluxes = (spec_by_name['gamma0']['vec'][ii] * c_melt * + TFs * np.absolute(TFs) * iceshelf_area) * \ + rhoi / 1.0e12 # Gt/yr + deltaT_vec[ii] = np.interp( + spec_by_name['meltflux']['vec'][ii], meltfluxes, TFs, + left=np.nan, right=np.nan) - mean_TF + if np.isnan(deltaT_vec[ii]): + sys.exit("ERROR: interpolated deltaT out of range. " + "Adjust definition of 'TFs'") + + return deltaT_vec + + +def _build_namelist_values(parameter_specs, run_num): + """For parameter specs of type 'namelist', + collect namelist option values for a given run number + and save them in a dictionary keyed by namelist option name. + These will be applied when the runs are set up. + + Returns + ------- + tuple of dict + ``(namelist_option_values, namelist_parameter_values)`` for the + requested ``run_num``. + """ + namelist_option_values = {} + namelist_parameter_values = {} + + for spec in parameter_specs: + if spec['type'] != 'namelist': + continue + value = spec['vec'][run_num] + for namelist_option in spec['option_names']: + namelist_option_values[namelist_option] = value + namelist_parameter_values[spec['run_info_name']] = value + + return namelist_option_values, namelist_parameter_values + + +def _add_member_steps(test_case, parameter_specs, spec_by_name, deltaT_vec, + resource_module, max_samples): + """Create and register ``EnsembleMember`` steps for requested runs. + + This helper assembles namelist and special-parameter values for each run + and adds one member step per run to ``test_case``. + """ + if test_case.end_run > max_samples: + sys.exit("Error: end_run specified in config exceeds maximum " + "sample size available in param_vector_filename") + + print("--- Identifying required parameters is complete ---") + for run_num in range(test_case.start_run, test_case.end_run + 1): + namelist_option_values, namelist_parameter_values = \ + _build_namelist_values(parameter_specs, run_num) + + fric_exp = _get_special_value(spec_by_name, 'fric_exp', run_num) + mu_scale = _get_special_value(spec_by_name, 'mu_scale', run_num) + stiff_scale = _get_special_value(spec_by_name, 'stiff_scale', + run_num) + gamma0 = _get_special_value(spec_by_name, 'gamma0', run_num) + meltflux = _get_special_value(spec_by_name, 'meltflux', run_num) + + test_case.add_step(EnsembleMember( + test_case=test_case, run_num=run_num, + basal_fric_exp=fric_exp, + mu_scale=mu_scale, + stiff_scale=stiff_scale, + gamma0=gamma0, + meltflux=meltflux, + deltaT=deltaT_vec[run_num], + namelist_option_values=namelist_option_values, + namelist_parameter_values=namelist_parameter_values, + resource_module=resource_module)) + # Note: do not add to steps_to_run, because ensemble_manager + # will handle submitting and running the runs + + +def _split_entries(raw): + """Split comma- or whitespace-delimited config lists. + + Backslash-newline sequences used for line continuation are stripped so + that multi-line values are treated as a single logical line. Remaining + backslashes are also removed to avoid spurious option tokens. + + Returns + ------- + list of str + Non-empty parsed entries. + """ + cleaned = raw.replace('\\\r\n', ' ').replace('\\\n', ' ') + cleaned = cleaned.replace('\\', ' ') + return [entry for entry in cleaned.replace(',', ' ').split() if entry] + + +def _parse_range(raw, parameter_name): + """Parse parameter min,max bounds from a comma-delimited value. + + Returns + ------- + tuple of float + ``(min_value, max_value)`` parsed from ``raw``. + """ + values = [entry.strip() for entry in raw.split(',') if entry.strip()] + if len(values) != 2: + raise ValueError( + f"Parameter '{parameter_name}' must contain exactly " + "two comma-separated values.") + return float(values[0]), float(values[1]) + + +def _get_special_value(spec_by_name, name, run_num): + """Get sampled value for a special parameter or ``None`` if inactive. + + Returns + ------- + float or None + Sampled value for ``name`` at ``run_num`` when present. + """ + if name not in spec_by_name: + return None + return spec_by_name[name]['vec'][run_num] diff --git a/docs/developers_guide/landice/api.rst b/docs/developers_guide/landice/api.rst index ae9736f688..5a11405506 100644 --- a/docs/developers_guide/landice/api.rst +++ b/docs/developers_guide/landice/api.rst @@ -192,6 +192,11 @@ ensemble_generator ensemble_member.EnsembleMember.setup ensemble_member.EnsembleMember.run + ensemble_template.get_ensemble_template_name + ensemble_template.get_spinup_template_package + ensemble_template.get_branch_template_package + ensemble_template.add_template_file + spinup_ensemble.SpinupEnsemble spinup_ensemble.SpinupEnsemble.configure diff --git a/docs/developers_guide/landice/test_groups/ensemble_generator.rst b/docs/developers_guide/landice/test_groups/ensemble_generator.rst index 9f62d40fc5..4222129283 100644 --- a/docs/developers_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/developers_guide/landice/test_groups/ensemble_generator.rst @@ -18,6 +18,17 @@ framework The shared config options for the ``ensemble_generator`` test group are described in :ref:`landice_ensemble_generator` in the User's Guide. +Model-specific inputs for this test group now live under: + +.. code-block:: none + + compass.landice.tests.ensemble_generator.ensemble_templates. + +with ``spinup`` and ``branch`` subpackages that each contain their own cfg, +namelist, and streams resources (plus ``albany_input.yaml`` for spinup). +The selected template name comes from +``[ensemble_generator] ensemble_template``. + ensemble_member ~~~~~~~~~~~~~~~ The class :py:class:`compass.landice.tests.ensemble_generator.EnsembleMember` @@ -66,6 +77,13 @@ Similarly, because changing gamma0 and deltaT require modifying a basal melt parameter file, the baseline file path needs to be specified in the config and that file is copied, renamed, and modified in each run directory. +Optionally, spinup runs can also apply precomputed friction samples from +Albany inversion products. If ``fric_samples_file``, ``fric_map_file``, and +``mpas_cellid_file`` are provided in ``[spinup_ensemble]`` (with optional +``fric_sample_scale``), run ``N`` reads sample ``N`` from +``fric_samples_file`` and applies it to ``muFriction`` in the copied run +input file. + Additionally, a job script is written for the run so that the run can be submitted as a slurm job independent of other runs in the ensemble. This also allows a user to easily run a single ensemble member by submitting the job @@ -80,7 +98,9 @@ There is a function ``_adjust_friction_exponent`` that modifies the friction exponent in the ``albany_input.yaml`` file and adjusts muFriction in the input file to maintain an unchanged basal shear stress. Similarly, there is a function ``_adjust_basal_melt_params`` that modifes gamma0 and -deltaT in a basal melt parameter file. +deltaT in a basal melt parameter file. There is also an +``_apply_fric_sample`` helper that maps an Albany friction sample onto MPAS +cell IDs and writes the resulting ``muFriction`` field. ensemble_manager ~~~~~~~~~~~~~~~~ @@ -104,11 +124,32 @@ phase. Also, by waiting until configure to define the ensemble members, it is possible to have the start and end run numbers set in the config, because the config is not parsed by the constructor. -The ``configure`` method is where most of the work happens. Here, the start -and end run numbers are read from the config, a parameter array is generated, -and the parameters to be varied and over what range are defined. +The ``configure`` method is where most of the work happens. +There is no default configuration for this test case, so the user must +provide a cfg file with the necessary options. This will typically be the +cfg located in the desired template directory or a user-modified copy of it. +With the cfg provided, the individual ensemble members will be set up. +Spinup run-control options (for example, ``start_run``, ``end_run``, +``sampling_method``, ``max_samples``, ``cfl_fraction``, and ``ntasks``) +are read from ``[ensemble_generator]``, while spinup resource paths and +related values (for example ``input_file_path`` and ``iceshelf_area_obs``) +are read from ``[spinup_ensemble]``. +Optional spinup friction-sample options +(``fric_samples_file``, ``fric_map_file``, ``mpas_cellid_file``, and +``fric_sample_scale``) are also read from ``[spinup_ensemble]`` and applied +per run number when present. +Supported sampling methods are ``sobol``, ``uniform``, and ``log-uniform``. The values for each parameter are passed to the ``EnsembleMember`` constructor to define each run. + +Parameter definitions now come from ``[ensemble.parameters]`` where each +parameter uses `` = min, max`` and ordering follows the order in +that section. Parameters with names prefixed by ``nl.`` are interpreted as +generic float-valued namelist perturbations and must define +``.option_name`` with one or more namelist options. Parameters without +the ``nl.`` prefix are reserved for special perturbations that use custom +logic (currently ``fric_exp``, ``mu_scale``, ``stiff_scale``, ``gamma0``, +and ``meltflux``). Finally, each run is now added to the test case as a step to run, because they were not automatically added by compass during the test case constructor phase. @@ -134,13 +175,17 @@ The constructor adds the ensemble_manager as a step, as with the spinup_ensemble The ``configure`` method searches over the range of runs requested and assesses if the corresponding spinup_ensemble member reached the requested branch time. -If so, and if the branch_ensemble memebr directory does not already exist, that +If so, and if the branch_ensemble member directory does not already exist, that run is added as a step. Within each run (step), the restart file from the branch year is copied to the branch run directory. The time stamp is reassigned to 2015 (this could be made a cfg option in the future). Also copied over are -the namelist and albany_input.yamlm files. The namelist is updated with -settings specific to the branch ensemble, and a streams file specific to the -branch run is added. Finally, details for managing runs are set up, including -a job script. +the namelist and, when present (for Albany-based configurations), the +``albany_input.yaml`` file. The namelist is updated with settings specific to +the branch ensemble, and a streams file specific to the branch run is added. +Finally, details for managing runs are set up, including a job script. + +As in spinup, the branch configure method first loads +``ensemble_templates//branch/branch_ensemble.cfg`` based on +``[ensemble_generator] ensemble_template``. As in the spinup_ensemble, the ``run`` step just runs the model. diff --git a/docs/users_guide/landice/test_groups/ensemble_generator.rst b/docs/users_guide/landice/test_groups/ensemble_generator.rst index d8f77e4a4c..3b263df871 100644 --- a/docs/users_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/users_guide/landice/test_groups/ensemble_generator.rst @@ -6,7 +6,8 @@ ensemble_generator The ``landice/ensemble_generator`` test group creates ensembles of MALI simulations with different parameter values. The ensemble framework sets up a user-defined number of simulations with parameter values selected -from either uniform sampling or a space-filling Sobol sequence. +from uniform sampling, log-uniform sampling, or a space-filling Sobol +sequence. A test case in this test group consists of a number of ensemble members, and one ensemble manager. @@ -23,28 +24,62 @@ look as expected before spending time on a larger ensemble. This also allows one to add more ensemble members from the Sobol sequence later if UQ analysis indicates the original sample size was insufficient. -A number of possible parameters are supported and whether they are active and -what parameter value ranges should be used are specified in a user-supplied -config file. Currently these parameters are supported: +Parameter types +--------------- + +Parameters are defined in ``[ensemble.parameters]`` and fall into two +categories: + +* ``special`` parameters: parameters without the ``nl.`` prefix that use + custom setup logic beyond namelist replacement + +* ``namelist`` parameters: parameters prefixed with ``nl.`` that map directly + to one or more float namelist options through ``.option_name``. + Note that only float namelist options are currently supported, but the framework + does not validate that the options defined in the config file are actually float + namelist options. Typically, ``.option_name`` will indicate a single + namelist option, but it can indicate multiple options if the same parameter + should be applied to multiple namelist options (e.g., for grounded and + floating von Mises threshold stresses). + +The currently supported special parameters are: + +* ``fric_exp``: basal friction power-law exponent (requires modifying + ``muFriction`` and ``albany_input.yaml``) + +* ``mu_scale``: multiplicative scale factor for ``muFriction`` in the + modified input file + +* ``stiff_scale``: multiplicative scale factor for ``stiffnessFactor`` in the + modified input file -* basal friction power law exponent +* ``gamma0``: ISMIP6-AIS basal-melt sensitivity coefficient -* scaling factor on muFriction +* ``meltflux``: target ice-shelf basal melt flux, converted to ``deltaT`` + using ``gamma0`` and domain-mean thermal forcing -* scaling factor on stiffnessFactor +In addition, spinup runs can optionally apply precomputed friction samples +from Albany inversion products through ``[spinup_ensemble]`` config options +(``fric_samples_file``, ``fric_map_file``, ``mpas_cellid_file``, and +``fric_sample_scale``). When these options are provided, run ``N`` uses +sample index ``N`` from ``fric_samples_file`` and writes the resulting +``muFriction`` field into that run's copied input file. -* von Mises threshold stress for calving +Test cases +---------- -* calving rate speed limit +The test group includes two test cases: -* gamma0 melt sensitivity parameter in ISMIP6-AIS ice-shelf basal melting - parameterization +* ``spinup_ensemble``: a set of simulations from the same initial condition + but with different parameter values. This could either be fixed climate + relaxation spinup or forced by time-evolving historical conditions. -* target ice-shelf basal melt rate for ISMIP6-AIS ice-shelf basal melting - parameterization. In the model setup, the deltaT thermal forcing bias - adjustment is adjusted to obtain the target melt rate for a given gamma0 +* ``branch_ensemble``: a set of simulations branched from each member of the + spinup_ensemble in a specified year with a different forcing. Multiple + branch ensembles can be branched from one spinup_ensemble -Additional parameters can be easily added in the future. +Test case operations +-------------------- ``compass setup`` will set up the simulations and the ensemble manager. ``compass run`` from the test case work directory will submit each run as a @@ -72,57 +107,85 @@ Future improvements may include: * safety checks or warnings before submitting ensembles that will use large amounts of computing resources -* a method for maintaining namelist, streams, and albany_input.yaml files for - different ensembles. Currently, these input files are specific to the Amery - Ice Shelf ensemble run in 2023. +Ensemble templates +------------------ + +This test group uses a template-based configuration workflow. +Instead of maintaining one set of test-group resource files, each model +configuration lives in its own subdirectory under +``ensemble_templates/`` with separate spinup and branch +cfg/namelist/streams resources. Users typically select a template via the +``[ensemble_generator] ensemble_template`` option or create a new template. +The user may also provide custom overrides in a user cfg file. +A new ensemble template should be added for each new study by creating +a new subdirectory under ``ensemble_templates/`` with the same structure as +the existing templates and following a naming convention like: +````, e.g., ``amery4km_probproj_2024`` or +``ais4km_hydro_2026``. + +The selected template controls which config files and model resource files are +used for the spinup and branch cases. The package layout is: + +.. code-block:: none + + compass/landice/tests/ensemble_generator/ensemble_templates// + spinup/ + ensemble_generator.cfg + namelist.landice + streams.landice + albany_input.yaml + branch/ + branch_ensemble.cfg + namelist.landice + streams.landice -The test group includes two test cases: +config options +-------------- -* ``spinup_ensemble``: a set of simulations from the same initial condition - but with different parameter values. This could either be fixed climate - relaxation spinup or forced by time-evolving historical conditions. +The shared config option for this test group is: -* ``branch_ensemble``: a set of simulations branched from each member of the - spinup_ensemble in a specified year with a different forcing. Multiple - branch ensembles can be branched from one spinup_ensemble +.. code-block:: cfg -config options --------------- -Test cases in this test group have the following common config options. + [ensemble_generator] -This test group is intended for expert users, and it is expected that it -will typically be run with a customized cfg file. Note the default run -numbers create a small ensemble, but uncertainty quantification applications -will typically need dozens or more simulations. + # name of the ensemble template to use + # resources are loaded from: + # compass.landice.tests.ensemble_generator.ensemble_templates. + ensemble_template = amery4km_probproj_2024 -The test-case-specific config options are: +The template-specific spinup config options (from +``ensemble_templates//spinup/ensemble_generator.cfg``) are: .. code-block:: cfg - [ensemble] + [ensemble_generator] # start and end numbers for runs to set up and run # Run numbers should be zero-based. # Additional runs can be added and run to an existing ensemble # without affecting existing runs, but trying to set up a run # that already exists will generate a warning and skip that run. - # If using uniform sampling, start_run should be 0 and end_run should be - # equal to (max_samples - 1), otherwise unexpected behavior may result. + # If using uniform or log-uniform sampling, start_run should be 0 and + # end_run should be equal to (max_samples - 1), otherwise unexpected + # behavior may result. # These values do not affect viz/analysis, which will include any # runs it finds. start_run = 0 end_run = 3 - # sampling_method can be either 'sobol' for a space-filling Sobol sequence - # or 'uniform' for uniform sampling. Uniform sampling is most appropriate - # for a single parameter sensitivity study. It will sample uniformly across - # all dimensions simultaneously, thus sampling only a small fraction of - # parameter space + # sampling_method can be 'sobol' for a space-filling Sobol sequence, + # 'uniform' for linear sampling, or 'log-uniform' for logarithmic + # sampling between min and max parameter bounds. + # Uniform and log-uniform are most appropriate for a single-parameter + # sensitivity study because they sample each active parameter using the + # same rank ordering, thus sampling only a small fraction of parameter + # space in higher dimensions. sampling_method = sobol # maximum number of samples to be considered. # max_samples needs to be greater or equal to (end_run + 1) - # When using uniform sampling, max_samples should equal (end_run + 1). + # When using uniform or log-uniform sampling, max_samples should equal + # (end_run + 1). # When using Sobol sequence, max_samples ought to be a power of 2. # max_samples should not be changed after the first set of ensemble. # So, when using Sobol sequence, max_samples might be set larger than @@ -153,90 +216,70 @@ The test-case-specific config options are: # to inform the choice for a large production ensemble. cfl_fraction = 0.7 - # Path to the initial condition input file. - # Eventually this could be hard-coded to use files on the input data - # server, but initially we want flexibility to experiment with different - # inputs and forcings - input_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/Amery.nc + # number of tasks that each ensemble member should be run with + # Eventually, compass could determine this, but we want explicit control for now + ntasks = 128 - # the value of the friction exponent used for the calculation of muFriction - # in the input file - orig_fric_exp = 0.2 + [spinup_ensemble] - # Path to ISMIP6 ice-shelf basal melt parameter input file. - basal_melt_param_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/basal_melt/parameterizations/Amery_4to20km_basin_and_coeff_gamma0_DeltaT_quadratic_non_local_median_allBasin2.nc + # Path to the initial condition input file. + # Eventually this could be hard-coded to use files on the input data + # server, but initially we want flexibility to experiment with different + # inputs and forcings + input_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/Amery.nc - # Path to thermal forcing file for the mesh to be used - TF_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/ocean_thermal_forcing/obs/Amery_4to20km_obs_TF_1995-2017_8km_x_60m.nc + # the value of the friction exponent used for the calculation of muFriction + # in the input file + orig_fric_exp = 0.2 - # Path to SMB forcing file for the mesh to be used - SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/atmosphere_forcing/RACMO_climatology_1995-2017/Amery_4to20km_RACMO2.3p2_ANT27_smb_climatology_1995-2017_no_xtime_noBareLandAdvance.nc + # Path to ISMIP6 ice-shelf basal melt parameter input file. + basal_melt_param_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/basal_melt/parameterizations/Amery_4to20km_basin_and_coeff_gamma0_DeltaT_quadratic_non_local_median_allBasin2.nc - # number of tasks that each ensemble member should be run with - # Eventually, compass could determine this, but we want explicit control for now - ntasks = 128 + # Path to thermal forcing file for the mesh to be used + TF_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/ocean_thermal_forcing/obs/Amery_4to20km_obs_TF_1995-2017_8km_x_60m.nc + + # Path to SMB forcing file for the mesh to be used + SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/atmosphere_forcing/RACMO_climatology_1995-2017/Amery_4to20km_RACMO2.3p2_ANT27_smb_climatology_1995-2017_no_xtime_noBareLandAdvance.nc + + # Optional friction-sample inputs from Albany optimization products. + # When these options are present, run N uses sample index N. + # fric_samples_file = /path/to/postVarSamples-5000.npy + # fric_map_file = /path/to/mu_log_opt.ascii + # mpas_cellid_file = /path/to/mpas_cellID.ascii + # fric_sample_scale = 0.25 + + # For meltflux perturbations, this observed ice-shelf area is used when + # converting target melt flux to deltaT. + iceshelf_area_obs = 60654.e6 + +The parameter sampling definitions live in a separate section, +``[ensemble.parameters]``. The order listed sets the sampling +dimension ordering, special parameters are unprefixed, and namelist +parameters use the ``nl.`` prefix with a companion ``.option_name``. + +For ``log-uniform`` sampling, each parameter bound must be strictly +positive because sampling is performed in log space. + +.. code-block:: cfg + + [ensemble.parameters] + + # special parameters (handled by custom code) + fric_exp = 0.1, 0.33333 + mu_scale = 0.8, 1.2 + stiff_scale = 0.8, 1.2 + gamma0 = 9620.0, 471000.0 + meltflux = 12.0, 58.0 + + # namelist float parameters (generic handling) + nl.von_mises_threshold = 80.0e3, 180.0e3 + nl.von_mises_threshold.option_name = + config_grounded_von_Mises_threshold_stress, + config_floating_von_Mises_threshold_stress + + nl.calv_spd_limit = 0.0001585, 0.001585 + nl.calv_spd_limit.option_name = config_calving_speed_limit - # whether basal friction exponent is being varied - # [unitless] - use_fric_exp = True - # min value to vary over - fric_exp_min = 0.1 - # max value to vary over - fric_exp_max = 0.33333 - - # whether a scaling factor on muFriction is being varied - # [unitless: 1.0=no scaling] - use_mu_scale = True - # min value to vary over - mu_scale_min = 0.8 - # max value to vary over - mu_scale_max = 1.2 - - # whether a scaling factor on stiffnessFactor is being varied - # [unitless: 1.0=no scaling] - use_stiff_scale = True - # min value to vary over - stiff_scale_min = 0.8 - # max value to vary over - stiff_scale_max = 1.2 - - # whether the von Mises threshold stress (sigma_max) is being varied - # [units: Pa] - use_von_mises_threshold = True - # min value to vary over - von_mises_threshold_min = 80.0e3 - # max value to vary over - von_mises_threshold_max = 180.0e3 - - # whether the calving speed limit is being varied - # [units: km/yr] - use_calv_limit = False - # min value to vary over - calv_limit_min = 5.0 - # max value to vary over - calv_limit_max = 50.0 - - # whether ocean melt parameterization coefficient is being varied - # [units: m/yr] - use_gamma0 = True - # min value to vary over - gamma0_min = 9620.0 - # max value to vary over - gamma0_max = 471000.0 - - # whether target ice-shelf basal melt flux is being varied - # [units: Gt/yr] - use_meltflux = True - # min value to vary over - meltflux_min = 12. - # max value to vary over - meltflux_max = 58. - # ice-shelf area associated with target melt rates - # [units: m^2] - iceshelf_area_obs = 60654.e6 - -A user should copy the default config file to a user-defined config file -before setting up the test case and any necessary adjustments made. Importantly, the user-defined config should be modified to also include the following options that will be used for submitting the jobs for each ensemble member. @@ -263,25 +306,12 @@ spinup_ensemble ``landice/ensemble_generator/spinup_ensemble`` uses the ensemble framework to create an ensemble of simulations integrated over a specified time range. The test case -can be applied to any domain and set of input files. If the default namelist -and streams settings are not appropriate, they can be adjusted or a new test -case can be set up mirroring the existing one. - -The default model configuration uses: - -* first-order velocity solver - -* power law basal friction - -* evolving temperature +can be applied to any domain and set of input files using the ensemble templates +discussed above. -* von Mises calving - -* ISMIP6 surface mass balance and sub-ice-shelf melting using climatological - mean forcing - -The initial condition and forcing files are specified in the -``ensemble_generator.cfg`` file or a user modification of it. +The initial condition and forcing files are specified in the selected +template file +``compass/landice/tests/ensemble_generator/ensemble_templates//spinup/ensemble_generator.cfg`` branch_ensemble --------------- @@ -291,52 +321,25 @@ an ensemble of simulations that are branched from corresponding runs of the ``spinup_ensemble`` at a specified year with a different forcing. In general, any namelist or streams modifications can be applied to the branch runs. -The branch_ensemble test-case-specific config options are: - -.. code-block:: cfg - - # config options for setting up an ensemble - - # config options for branching an ensemble - [branch_ensemble] - - # start and end numbers for runs to set up and run - # branch runs. - # It is assumed that spinup runs have already been - # conducted for these runs. - start_run = 0 - end_run = 3 - - # Path to thermal forcing file for the mesh to be used in the branch run - TF_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/ocean_thermal_forcing/UKESM1-0-LL_SSP585/1995-2300/Amery_4to20km_TF_UKESM1-0-LL_SSP585_2300.nc - - # Path to SMB forcing file for the mesh to be used in the branch run - SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/forcing/atmosphere_forcing/UKESM1-0-LL_SSP585/1995-2300/Amery_4to20km_SMB_UKESM1-0-LL_SSP585_2300_noBareLandAdvance.nc - - # location of spinup ensemble to branch from - spinup_test_dir = /pscratch/sd/h/hoffman2/AMERY_corrected_forcing_6param_ensemble_2023-03-18/landice/ensemble_generator/ensemble - - # year of spinup simulation from which to branch runs - branch_year = 2050 - - # whether to only set up branch runs for filtered runs or all runs - set_up_filtered_only = True - - # path to pickle file containing filtering information generated by plot_ensemble.py - ensemble_pickle_file = None +The branch_ensemble config options are read from the selected template file +``compass/landice/tests/ensemble_generator/ensemble_templates//branch/branch_ensemble.cfg``. -Steps for setting up and running an ensmble -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Steps for setting up and running an ensemble +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. With a compass conda environment set up, run, e.g., - ``compass setup -t landice/ensemble_generator/spinup_ensemble -w WORK_DIR_PATH -f USER.cfg`` + ``compass setup -t landice/ensemble_generator/spinup_ensemble -w WORK_DIR_PATH -f TEMPLATE.cfg`` where ``WORK_DIR_PATH`` is a location that can store the whole - ensemble (typically a scratch drive) and ``USER.cfg`` is the - user-defined config described in the previous section that includes - options for ``[parallel]`` and ``[job]``, as well as any required - modifications to the ``[ensemble]`` section. Likely, most or all - attributes in the ``[ensemble]`` section need to be customized for a - given application. + ensemble (typically a scratch drive) and ``TEMPLATE.cfg`` is the + config for the ensemble template to be set up, i.e., located at: + ``landice/tests/ensemble_generator/ensemble_templates/TEMPLATE_NAME/spinup/ensemble_generator.cfg`` + The cfg file should include options for ``[parallel]`` and ``[job]``. + The cfg files for each template should in general be committed in a + state that is ready to use, but users should review the contents + before setting up an ensemble, including the ``start_run`` and + ``end_run`` values. If adjustments are needed, the user can make + adjustments to the cfg file in place, or make a copy and point to + that directly. 2. After ``compass setup`` completes and all runs are set up, go to the ``WORK_DIR_PATH`` and change to the