diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py index 07c6a4ebec..07d8c9db62 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py @@ -10,6 +10,10 @@ from compass.landice.tests.ensemble_generator.ensemble_manager import ( EnsembleManager, ) +from compass.landice.tests.ensemble_generator.ensemble_template import ( + add_template_file, + get_branch_template_package, +) from compass.testcase import TestCase @@ -59,6 +63,9 @@ def configure(self): """ config = self.config + resource_module = get_branch_template_package(config) + add_template_file(config, resource_module, 'branch_ensemble.cfg') + section = config['branch_ensemble'] spinup_test_dir = section.get('spinup_test_dir') @@ -89,7 +96,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..16554a0355 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_member.py +++ b/compass/landice/tests/ensemble_generator/ensemble_member.py @@ -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,25 +180,15 @@ 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' @@ -199,13 +198,16 @@ def setup(self): # set input filename in streams and create streams file stream_replacements = {'input_file_init_cond': new_input_fname} 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}') @@ -227,7 +229,8 @@ def setup(self): # 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_path = spinup_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' @@ -243,9 +246,9 @@ 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 # store accumulated namelist and streams options 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..a3ae60083d --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_template.py @@ -0,0 +1,95 @@ +from importlib.util import find_spec + + +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') + + +def add_template_file(config, package, filename): + """ + Add a config file from the selected ensemble template package. + + Parameters + ---------- + config : compass.config.CompassConfigParser + Configuration options for a test case + + package : str + The package containing the requested configuration file + + filename : str + The configuration filename to add from the package + """ + if find_spec(package) is None: + raise ValueError( + f"Ensemble template package '{package}' was not found.") + + config.add_from_package(package, filename, exception=True) 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/default/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/default/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/branch_ensemble.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/branch_ensemble.cfg new file mode 100644 index 0000000000..709c9fbd68 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/default/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/default/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/default/branch/namelist.landice diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/streams.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/default/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/default/branch/streams.landice diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/default/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/default/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/default/spinup/albany_input.yaml diff --git a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg similarity index 64% rename from compass/landice/tests/ensemble_generator/ensemble_generator.cfg rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg index 4cbab8b830..72786e6a0c 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/default/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,30 @@ 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 - -# 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/default/spinup/namelist.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/namelist.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/namelist.landice diff --git a/compass/landice/tests/ensemble_generator/streams.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/streams.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/streams.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/streams.landice diff --git a/compass/landice/tests/ensemble_generator/plot_ensemble.py b/compass/landice/tests/ensemble_generator/plot_ensemble.py index 14cdbaade4..ba65e5dc5e 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: diff --git a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index 1b6aae8a80..7f4403e7f8 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -10,8 +10,11 @@ from compass.landice.tests.ensemble_generator.ensemble_member import ( EnsembleMember, ) +from compass.landice.tests.ensemble_generator.ensemble_template import ( + add_template_file, + get_spinup_template_package, +) from compass.testcase import TestCase -from compass.validate import compare_variables class SpinupEnsemble(TestCase): @@ -59,132 +62,54 @@ 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) + add_template_file(config, resource_module, 'ensemble_generator.cfg') - 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) + + 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 +119,278 @@ 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") + + 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..309d7f80e5 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` @@ -104,11 +115,28 @@ 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]``. +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 +162,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..0304e6d3e7 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,55 @@ 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). -* basal friction power law exponent +The currently supported special parameters are: -* scaling factor on muFriction +* ``fric_exp``: basal friction power-law exponent (requires modifying + ``muFriction`` and ``albany_input.yaml``) -* scaling factor on stiffnessFactor +* ``mu_scale``: multiplicative scale factor for ``muFriction`` in the + modified input file -* von Mises threshold stress for calving +* ``stiff_scale``: multiplicative scale factor for ``stiffnessFactor`` in the + modified input file -* calving rate speed limit +* ``gamma0``: ISMIP6-AIS basal-melt sensitivity coefficient -* gamma0 melt sensitivity parameter in ISMIP6-AIS ice-shelf basal melting - parameterization +* ``meltflux``: target ice-shelf basal melt flux, converted to ``deltaT`` + using ``gamma0`` and domain-mean thermal forcing -* 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 +Test cases +---------- -Additional parameters can be easily added in the future. +The test group includes two test cases: + +* ``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. + +* ``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 + +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 +100,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 default template 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 = default -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 +209,63 @@ 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 + + # 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 +292,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: +can be applied to any domain and set of input files using the ensemble templates +discussed above. -* first-order velocity solver - -* power law basal friction - -* evolving temperature - -* 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,11 +307,17 @@ 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: +The branch_ensemble config options are read from the selected template file +``compass/landice/tests/ensemble_generator/ensemble_templates//branch/branch_ensemble.cfg``. +The default template options are: .. code-block:: cfg - # config options for setting up an ensemble + # 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] @@ -325,8 +347,8 @@ The branch_ensemble test-case-specific config options are: # path to pickle file containing filtering information generated by plot_ensemble.py ensemble_pickle_file = None -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`` @@ -334,9 +356,9 @@ Steps for setting up and running an ensmble 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. + modifications to the ``[ensemble_generator]`` and ``[spinup_ensemble]`` + sections. Likely, most or all attributes in these sections need to be + customized for a given application. 2. After ``compass setup`` completes and all runs are set up, go to the ``WORK_DIR_PATH`` and change to the