From e3b966894f60a381e2968e2eae28ac4bfb25ac0e Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 7 Mar 2026 15:18:20 -0700 Subject: [PATCH 01/13] Refactor ensemble generator configuration and branching setup Generalize ensemble generator to support multiple model configurations used for different studies. * Introduced a new configuration module to handle model configurations for ensemble generation. * Updated `BranchRun` and `EnsembleMember` classes to accept a `resource_module` parameter for dynamic configuration loading. * Created default configuration files for branch and spinup ensembles, including necessary namelists and stream definitions. * Modified the main ensemble generator configuration to streamline the setup process and improve clarity. * Enhanced error handling for missing configuration sections and options. * Updated the `SpinupEnsemble` class to utilize the new configuration methods for improved modularity and maintainability. --- .../branch_ensemble/__init__.py | 10 +- .../branch_ensemble/branch_ensemble.cfg | 30 +--- .../branch_ensemble/branch_run.py | 12 +- .../ensemble_generator/configurations.py | 95 ++++++++++++ .../configurations/__init__.py | 0 .../configurations/default/__init__.py | 0 .../configurations/default/branch/__init__.py | 0 .../default/branch/branch_ensemble.cfg | 27 ++++ .../default/branch}/namelist.landice | 0 .../default/branch}/streams.landice | 0 .../configurations/default/spinup/__init__.py | 0 .../default/spinup}/albany_input.yaml | 0 .../default/spinup/ensemble_generator.cfg | 136 +++++++++++++++++ .../default/spinup}/namelist.landice | 0 .../default/spinup}/streams.landice | 0 .../ensemble_generator/ensemble_generator.cfg | 141 +----------------- .../ensemble_generator/ensemble_member.py | 8 +- .../spinup_ensemble/__init__.py | 14 +- 18 files changed, 303 insertions(+), 170 deletions(-) create mode 100644 compass/landice/tests/ensemble_generator/configurations.py create mode 100644 compass/landice/tests/ensemble_generator/configurations/__init__.py create mode 100644 compass/landice/tests/ensemble_generator/configurations/default/__init__.py create mode 100644 compass/landice/tests/ensemble_generator/configurations/default/branch/__init__.py create mode 100644 compass/landice/tests/ensemble_generator/configurations/default/branch/branch_ensemble.cfg rename compass/landice/tests/ensemble_generator/{branch_ensemble => configurations/default/branch}/namelist.landice (100%) rename compass/landice/tests/ensemble_generator/{branch_ensemble => configurations/default/branch}/streams.landice (100%) create mode 100644 compass/landice/tests/ensemble_generator/configurations/default/spinup/__init__.py rename compass/landice/tests/ensemble_generator/{ => configurations/default/spinup}/albany_input.yaml (100%) create mode 100644 compass/landice/tests/ensemble_generator/configurations/default/spinup/ensemble_generator.cfg rename compass/landice/tests/ensemble_generator/{ => configurations/default/spinup}/namelist.landice (100%) rename compass/landice/tests/ensemble_generator/{ => configurations/default/spinup}/streams.landice (100%) diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py index 07c6a4ebec..e988aa7338 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py @@ -4,6 +4,10 @@ import numpy as np +from compass.landice.tests.ensemble_generator.configurations import ( + add_configuration_file, + get_branch_configuration_package, +) from compass.landice.tests.ensemble_generator.branch_ensemble.branch_run import ( # noqa BranchRun, ) @@ -59,6 +63,9 @@ def configure(self): """ config = self.config + resource_module = get_branch_configuration_package(config) + add_configuration_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..6ff95d0528 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.configurations..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..1d35f16e29 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/branch_run.py +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/branch_run.py @@ -50,7 +50,7 @@ class BranchRun(Step): value of deltaT to use in ISMIP6 ice-shelf basal melt param. """ - def __init__(self, test_case, run_num, + def __init__(self, test_case, run_num, resource_module, basal_fric_exp=None, mu_scale=None, stiff_scale=None, @@ -68,8 +68,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}' @@ -120,8 +125,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 +136,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/configurations.py b/compass/landice/tests/ensemble_generator/configurations.py new file mode 100644 index 0000000000..a18cb74474 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/configurations.py @@ -0,0 +1,95 @@ +from importlib.util import find_spec + + +def get_model_configuration_name(config): + """ + Get the configured model configuration name. + + Parameters + ---------- + config : compass.config.CompassConfigParser + Configuration options for a test case + + Returns + ------- + str + The selected model configuration name + """ + section = 'ensemble_generator' + option = 'model_configuration' + + 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}'.") + + configuration = config.get(section, option).strip() + if configuration == '': + raise ValueError('model_configuration cannot be empty.') + + return configuration + + +def get_spinup_configuration_package(config): + """ + Get the package containing spinup ensemble resources. + + Parameters + ---------- + config : compass.config.CompassConfigParser + Configuration options for a test case + + Returns + ------- + str + Package path for spinup resources + """ + configuration = get_model_configuration_name(config) + return ('compass.landice.tests.ensemble_generator.configurations.' + f'{configuration}.spinup') + + +def get_branch_configuration_package(config): + """ + Get the package containing branch ensemble resources. + + Parameters + ---------- + config : compass.config.CompassConfigParser + Configuration options for a test case + + Returns + ------- + str + Package path for branch resources + """ + configuration = get_model_configuration_name(config) + return ('compass.landice.tests.ensemble_generator.configurations.' + f'{configuration}.branch') + + +def add_configuration_file(config, package, filename): + """ + Add a configuration file from a selected configuration 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"Model configuration package '{package}' was not found.") + + config.add_from_package(package, filename, exception=True) diff --git a/compass/landice/tests/ensemble_generator/configurations/__init__.py b/compass/landice/tests/ensemble_generator/configurations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/configurations/default/__init__.py b/compass/landice/tests/ensemble_generator/configurations/default/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/configurations/default/branch/__init__.py b/compass/landice/tests/ensemble_generator/configurations/default/branch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compass/landice/tests/ensemble_generator/configurations/default/branch/branch_ensemble.cfg b/compass/landice/tests/ensemble_generator/configurations/default/branch/branch_ensemble.cfg new file mode 100644 index 0000000000..78953eda17 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/configurations/default/branch/branch_ensemble.cfg @@ -0,0 +1,27 @@ +# 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/configurations/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/configurations/default/branch/namelist.landice diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/streams.landice b/compass/landice/tests/ensemble_generator/configurations/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/configurations/default/branch/streams.landice diff --git a/compass/landice/tests/ensemble_generator/configurations/default/spinup/__init__.py b/compass/landice/tests/ensemble_generator/configurations/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/configurations/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/configurations/default/spinup/albany_input.yaml diff --git a/compass/landice/tests/ensemble_generator/configurations/default/spinup/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/configurations/default/spinup/ensemble_generator.cfg new file mode 100644 index 0000000000..4cbab8b830 --- /dev/null +++ b/compass/landice/tests/ensemble_generator/configurations/default/spinup/ensemble_generator.cfg @@ -0,0 +1,136 @@ +# 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. +# 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 = 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 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 = ISMIP6BasinBC + +# fraction of CFL-limited time step to be used by the adaptive timestepper +# This value is explicitly included here to force the user to consciously +# select the value to use. Model run time tends to be inversely proportional +# to scaling this value (e.g., 0.2 will be ~4x more expensive than 0.8). +# Value should be less than or equal to 1.0, and values greater than 0.9 are +# not recommended. +# Values of 0.7-0.9 typically work for most simulations, but some runs may +# fail. Values of 0.2-0.5 are more conservative and will allow more runs +# to succeed, but will result in substantially more expensive runs +# However, because the range of parameter combinations being simulated +# are likely to stress the model, a smaller number than usual may be +# necessary to effectively cover parameter space. +# A user may want to do a few small ensembles with different values +# 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 + +# the value of the friction exponent used for the calculation of muFriction +# in the input file +orig_fric_exp = 0.2 + +# 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 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 + +# 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] +iceshelf_area_obs = 60654.e6 diff --git a/compass/landice/tests/ensemble_generator/namelist.landice b/compass/landice/tests/ensemble_generator/configurations/default/spinup/namelist.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/namelist.landice rename to compass/landice/tests/ensemble_generator/configurations/default/spinup/namelist.landice diff --git a/compass/landice/tests/ensemble_generator/streams.landice b/compass/landice/tests/ensemble_generator/configurations/default/spinup/streams.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/streams.landice rename to compass/landice/tests/ensemble_generator/configurations/default/spinup/streams.landice diff --git a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_generator.cfg index 4cbab8b830..f033dcbac6 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg +++ b/compass/landice/tests/ensemble_generator/ensemble_generator.cfg @@ -1,136 +1,7 @@ -# config options for setting up an ensemble -[ensemble] +# global options for ensemble_generator test cases +[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. -# 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 = 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 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 = ISMIP6BasinBC - -# fraction of CFL-limited time step to be used by the adaptive timestepper -# This value is explicitly included here to force the user to consciously -# select the value to use. Model run time tends to be inversely proportional -# to scaling this value (e.g., 0.2 will be ~4x more expensive than 0.8). -# Value should be less than or equal to 1.0, and values greater than 0.9 are -# not recommended. -# Values of 0.7-0.9 typically work for most simulations, but some runs may -# fail. Values of 0.2-0.5 are more conservative and will allow more runs -# to succeed, but will result in substantially more expensive runs -# However, because the range of parameter combinations being simulated -# are likely to stress the model, a smaller number than usual may be -# necessary to effectively cover parameter space. -# A user may want to do a few small ensembles with different values -# 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 - -# the value of the friction exponent used for the calculation of muFriction -# in the input file -orig_fric_exp = 0.2 - -# 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 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 - -# 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] -iceshelf_area_obs = 60654.e6 +# name of the model configuration to use +# resources are loaded from: +# compass.landice.tests.ensemble_generator.configurations. +model_configuration = default diff --git a/compass/landice/tests/ensemble_generator/ensemble_member.py b/compass/landice/tests/ensemble_generator/ensemble_member.py index ca08833cff..fbf4a38d88 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_member.py +++ b/compass/landice/tests/ensemble_generator/ensemble_member.py @@ -54,6 +54,7 @@ class EnsembleMember(Step): """ def __init__(self, test_case, run_num, + resource_module, basal_fric_exp=None, mu_scale=None, stiff_scale=None, @@ -73,6 +74,10 @@ 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 + basal_fric_exp : float value of basal friction exponent to use @@ -96,6 +101,7 @@ 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 # store assigned param values for this run self.basal_fric_exp = basal_fric_exp @@ -127,7 +133,7 @@ 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 diff --git a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index 1b6aae8a80..2bd42a35d0 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -4,6 +4,10 @@ from scipy.stats import qmc from compass.landice.iceshelf_melt import calc_mean_TF +from compass.landice.tests.ensemble_generator.configurations import ( + add_configuration_file, + get_spinup_configuration_package, +) from compass.landice.tests.ensemble_generator.ensemble_manager import ( EnsembleManager, ) @@ -67,7 +71,12 @@ def configure(self): sec_in_yr = 3600.0 * 24.0 * 365.0 c_melt = (rhosw * cp_seawater / (rhoi * latent_heat_ice))**2 - section = self.config['ensemble'] + config = self.config + resource_module = get_spinup_configuration_package(config) + add_configuration_file(config, resource_module, + 'ensemble_generator.cfg') + + section = config['ensemble'] # Determine start and end run numbers being requested self.start_run = section.getint('start_run') @@ -182,7 +191,8 @@ def configure(self): 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])) + deltaT=deltaT_vec[run_num], + resource_module=resource_module)) # Note: do not add to steps_to_run, because ensemble_manager # will handle submitting and running the runs From ea5af8613fd84e22662909f9d95092e749eeff2e Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 7 Mar 2026 15:26:14 -0700 Subject: [PATCH 02/13] Rename 'configuration' to 'ensemble_template' Variations on the word configuration are already too widespread so this change should reduce confusion. --- .../branch_ensemble/__init__.py | 10 ++--- .../branch_ensemble/branch_ensemble.cfg | 2 +- .../ensemble_generator/ensemble_generator.cfg | 6 +-- ...configurations.py => ensemble_template.py} | 42 +++++++++---------- .../__init__.py | 0 .../default/__init__.py | 0 .../default/branch/__init__.py | 0 .../default/branch/branch_ensemble.cfg | 0 .../default/branch/namelist.landice | 0 .../default/branch/streams.landice | 0 .../default/spinup/__init__.py | 0 .../default/spinup/albany_input.yaml | 0 .../default/spinup/ensemble_generator.cfg | 0 .../default/spinup/namelist.landice | 0 .../default/spinup/streams.landice | 0 .../spinup_ensemble/__init__.py | 11 +++-- 16 files changed, 35 insertions(+), 36 deletions(-) rename compass/landice/tests/ensemble_generator/{configurations.py => ensemble_template.py} (57%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/__init__.py (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/__init__.py (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/branch/__init__.py (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/branch/branch_ensemble.cfg (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/branch/namelist.landice (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/branch/streams.landice (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/spinup/__init__.py (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/spinup/albany_input.yaml (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/spinup/ensemble_generator.cfg (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/spinup/namelist.landice (100%) rename compass/landice/tests/ensemble_generator/{configurations => ensemble_templates}/default/spinup/streams.landice (100%) diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py index e988aa7338..a7fb8e1281 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py @@ -4,9 +4,9 @@ import numpy as np -from compass.landice.tests.ensemble_generator.configurations import ( - add_configuration_file, - get_branch_configuration_package, +from compass.landice.tests.ensemble_generator.ensemble_template import ( + add_template_file, + get_branch_template_package, ) from compass.landice.tests.ensemble_generator.branch_ensemble.branch_run import ( # noqa BranchRun, @@ -63,8 +63,8 @@ def configure(self): """ config = self.config - resource_module = get_branch_configuration_package(config) - add_configuration_file(config, resource_module, 'branch_ensemble.cfg') + resource_module = get_branch_template_package(config) + add_template_file(config, resource_module, 'branch_ensemble.cfg') section = config['branch_ensemble'] 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 6ff95d0528..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,3 +1,3 @@ # branch_ensemble options are loaded from the selected model configuration # package under: -# compass.landice.tests.ensemble_generator.configurations..branch +# compass.landice.tests.ensemble_generator.ensemble_templates..branch diff --git a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_generator.cfg index f033dcbac6..05acbf3b21 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg +++ b/compass/landice/tests/ensemble_generator/ensemble_generator.cfg @@ -1,7 +1,7 @@ # global options for ensemble_generator test cases [ensemble_generator] -# name of the model configuration to use +# name of the ensemble template to use # resources are loaded from: -# compass.landice.tests.ensemble_generator.configurations. -model_configuration = default +# compass.landice.tests.ensemble_generator.ensemble_templates. +ensemble_template = default diff --git a/compass/landice/tests/ensemble_generator/configurations.py b/compass/landice/tests/ensemble_generator/ensemble_template.py similarity index 57% rename from compass/landice/tests/ensemble_generator/configurations.py rename to compass/landice/tests/ensemble_generator/ensemble_template.py index a18cb74474..a3ae60083d 100644 --- a/compass/landice/tests/ensemble_generator/configurations.py +++ b/compass/landice/tests/ensemble_generator/ensemble_template.py @@ -1,9 +1,9 @@ from importlib.util import find_spec -def get_model_configuration_name(config): +def get_ensemble_template_name(config): """ - Get the configured model configuration name. + Get the configured ensemble template name. Parameters ---------- @@ -13,10 +13,10 @@ def get_model_configuration_name(config): Returns ------- str - The selected model configuration name + The selected ensemble template name """ section = 'ensemble_generator' - option = 'model_configuration' + option = 'ensemble_template' if not config.has_section(section): raise ValueError( @@ -28,16 +28,16 @@ def get_model_configuration_name(config): f"Missing required config option '{option}' in section " f"'{section}'.") - configuration = config.get(section, option).strip() - if configuration == '': - raise ValueError('model_configuration cannot be empty.') + template = config.get(section, option).strip() + if template == '': + raise ValueError('ensemble_template cannot be empty.') - return configuration + return template -def get_spinup_configuration_package(config): +def get_spinup_template_package(config): """ - Get the package containing spinup ensemble resources. + Get the package containing spinup ensemble template resources. Parameters ---------- @@ -49,14 +49,14 @@ def get_spinup_configuration_package(config): str Package path for spinup resources """ - configuration = get_model_configuration_name(config) - return ('compass.landice.tests.ensemble_generator.configurations.' - f'{configuration}.spinup') + template = get_ensemble_template_name(config) + return ('compass.landice.tests.ensemble_generator.ensemble_templates.' + f'{template}.spinup') -def get_branch_configuration_package(config): +def get_branch_template_package(config): """ - Get the package containing branch ensemble resources. + Get the package containing branch ensemble template resources. Parameters ---------- @@ -68,14 +68,14 @@ def get_branch_configuration_package(config): str Package path for branch resources """ - configuration = get_model_configuration_name(config) - return ('compass.landice.tests.ensemble_generator.configurations.' - f'{configuration}.branch') + template = get_ensemble_template_name(config) + return ('compass.landice.tests.ensemble_generator.ensemble_templates.' + f'{template}.branch') -def add_configuration_file(config, package, filename): +def add_template_file(config, package, filename): """ - Add a configuration file from a selected configuration package. + Add a config file from the selected ensemble template package. Parameters ---------- @@ -90,6 +90,6 @@ def add_configuration_file(config, package, filename): """ if find_spec(package) is None: raise ValueError( - f"Model configuration package '{package}' was not found.") + f"Ensemble template package '{package}' was not found.") config.add_from_package(package, filename, exception=True) diff --git a/compass/landice/tests/ensemble_generator/configurations/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/__init__.py similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/__init__.py rename to compass/landice/tests/ensemble_generator/ensemble_templates/__init__.py diff --git a/compass/landice/tests/ensemble_generator/configurations/default/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/default/__init__.py similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/__init__.py rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/__init__.py diff --git a/compass/landice/tests/ensemble_generator/configurations/default/branch/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/__init__.py similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/branch/__init__.py rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/__init__.py diff --git a/compass/landice/tests/ensemble_generator/configurations/default/branch/branch_ensemble.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/branch_ensemble.cfg similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/branch/branch_ensemble.cfg rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/branch_ensemble.cfg diff --git a/compass/landice/tests/ensemble_generator/configurations/default/branch/namelist.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/namelist.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/branch/namelist.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/namelist.landice diff --git a/compass/landice/tests/ensemble_generator/configurations/default/branch/streams.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/streams.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/branch/streams.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/streams.landice diff --git a/compass/landice/tests/ensemble_generator/configurations/default/spinup/__init__.py b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/__init__.py similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/spinup/__init__.py rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/__init__.py diff --git a/compass/landice/tests/ensemble_generator/configurations/default/spinup/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/configurations/default/spinup/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/configurations/default/spinup/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/spinup/ensemble_generator.cfg rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg diff --git a/compass/landice/tests/ensemble_generator/configurations/default/spinup/namelist.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/namelist.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/spinup/namelist.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/namelist.landice diff --git a/compass/landice/tests/ensemble_generator/configurations/default/spinup/streams.landice b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/streams.landice similarity index 100% rename from compass/landice/tests/ensemble_generator/configurations/default/spinup/streams.landice rename to compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/streams.landice diff --git a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index 2bd42a35d0..eed6fdb643 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -4,9 +4,9 @@ from scipy.stats import qmc from compass.landice.iceshelf_melt import calc_mean_TF -from compass.landice.tests.ensemble_generator.configurations import ( - add_configuration_file, - get_spinup_configuration_package, +from compass.landice.tests.ensemble_generator.ensemble_template import ( + add_template_file, + get_spinup_template_package, ) from compass.landice.tests.ensemble_generator.ensemble_manager import ( EnsembleManager, @@ -72,9 +72,8 @@ def configure(self): c_melt = (rhosw * cp_seawater / (rhoi * latent_heat_ice))**2 config = self.config - resource_module = get_spinup_configuration_package(config) - add_configuration_file(config, resource_module, - 'ensemble_generator.cfg') + resource_module = get_spinup_template_package(config) + add_template_file(config, resource_module, 'ensemble_generator.cfg') section = config['ensemble'] From 6f86220396fe5f29db9b43f1d2902754b687d6f8 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 7 Mar 2026 15:31:51 -0700 Subject: [PATCH 03/13] Update docs for refactor --- docs/developers_guide/landice/api.rst | 5 +++ .../test_groups/ensemble_generator.rst | 27 +++++++++-- .../test_groups/ensemble_generator.rst | 45 ++++++++++++++++--- 3 files changed, 66 insertions(+), 11 deletions(-) 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..4598a1614c 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` @@ -105,8 +116,12 @@ 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. +and end run numbers are read from the template-selected config, a parameter +array is generated, and the parameters to be varied and over what range are +defined. +The method first loads +``ensemble_templates//spinup/ensemble_generator.cfg`` based on +``[ensemble_generator] ensemble_template``. The values for each parameter are passed to the ``EnsembleMember`` constructor to define each run. Finally, each run is now added to the test case as a step to run, @@ -134,13 +149,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 +the namelist and albany_input.yaml 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. +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..a316a006aa 100644 --- a/docs/users_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/users_guide/landice/test_groups/ensemble_generator.rst @@ -72,9 +72,8 @@ 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. +* improvements to automatically validate user-provided template names and + report available choices The test group includes two test cases: @@ -95,7 +94,35 @@ 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. -The test-case-specific config options are: +The shared config option for this test group is: + +.. code-block:: cfg + + [ensemble_generator] + + # name of the ensemble template to use + # resources are loaded from: + # compass.landice.tests.ensemble_generator.ensemble_templates. + ensemble_template = default + +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 template-specific spinup config options (from +``ensemble_templates//spinup/ensemble_generator.cfg``) are: .. code-block:: cfg @@ -280,8 +307,10 @@ The default model configuration uses: * 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`` +or in a user override. branch_ensemble --------------- @@ -291,7 +320,9 @@ 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 From 1aac5eaaf6736d76be0c4b9b037b5a5c16ae1b61 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 7 Mar 2026 20:07:58 -0700 Subject: [PATCH 04/13] Refactor ensemble generator parameters and cfg handling The primary new functionality is the ability to support any namelist option as a parameter rather than only pre-defined parameters. The refactor also simplifies how parameter values are specified and puts all parameters in a dedicated cfg section. More details on the format are included in the updated docs. --- .../branch_ensemble/branch_run.py | 29 +-- .../ensemble_generator/ensemble_member.py | 53 +++--- .../default/spinup/ensemble_generator.cfg | 86 +++------ .../spinup_ensemble/__init__.py | 168 +++++++++++++----- .../test_groups/ensemble_generator.rst | 9 + .../test_groups/ensemble_generator.rst | 79 +++----- 6 files changed, 202 insertions(+), 222 deletions(-) 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 1d35f16e29..7519a44ddd 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, resource_module, - 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 diff --git a/compass/landice/tests/ensemble_generator/ensemble_member.py b/compass/landice/tests/ensemble_generator/ensemble_member.py index fbf4a38d88..9e6e761946 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. @@ -55,11 +49,11 @@ 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): @@ -78,6 +72,14 @@ def __init__(self, test_case, run_num, 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 @@ -87,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. @@ -102,13 +97,17 @@ def __init__(self, test_case, run_num, """ 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 @@ -177,21 +176,11 @@ 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 diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg index 4cbab8b830..bb354f0998 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg @@ -72,65 +72,35 @@ 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 +# For meltflux perturbations, this observed ice-shelf area is used when +# converting target melt flux to deltaT. +iceshelf_area_obs = 60654.e6 + # 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] -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/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index eed6fdb643..4e138ddeb3 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -15,7 +15,6 @@ EnsembleMember, ) from compass.testcase import TestCase -from compass.validate import compare_variables class SpinupEnsemble(TestCase): @@ -76,27 +75,23 @@ def configure(self): add_template_file(config, resource_module, 'ensemble_generator.cfg') section = config['ensemble'] + parameter_section_name = 'ensemble.parameters' + if parameter_section_name not in config: + 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: @@ -113,28 +108,23 @@ def configure(self): 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 + # Define parameter vectors + for idx, spec in enumerate(parameter_specs): + print('Including parameter ' + spec['name']) + spec['vec'] = param_unit_values[:, idx] * \ + (spec['max'] - spec['min']) + spec['min'] + + spec_by_name = {spec['name']: spec for spec in parameter_specs} # melt flux needs to be converted to deltaT - if param_dict['meltflux']['active']: + if 'meltflux' in spec_by_name: + if 'gamma0' not in spec_by_name: + sys.exit("ERROR: parameter 'meltflux' requires 'gamma0'.") + if not section.has_option('iceshelf_area_obs'): + sys.exit( + "ERROR: parameter 'meltflux' requires " + "'iceshelf_area_obs' in [ensemble].") + # First calculate mean TF for this domain iceshelf_area_obs = section.getfloat('iceshelf_area_obs') input_file_path = section.get('input_file_path') @@ -149,7 +139,8 @@ def configure(self): 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 + spec_by_name['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 @@ -161,12 +152,12 @@ def configure(self): # 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 + meltfluxes = (spec_by_name['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], + deltaT_vec[ii] = np.interp(spec_by_name['meltflux']['vec'][ii], meltfluxes, TFs, left=np.nan, right=np.nan) - mean_TF @@ -181,16 +172,32 @@ def configure(self): 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): + namelist_option_values = {} + namelist_parameter_values = {} + for spec in parameter_specs: + if spec['type'] == 'namelist': + 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 + + 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) + 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], + 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 @@ -203,3 +210,72 @@ def configure(self): # no run() method is needed # no validate() method is needed + + +def _get_parameter_specs(section): + """Parse ordered perturbation definitions from [ensemble.parameters].""" + 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 _split_entries(raw): + """Split comma- or whitespace-delimited config lists.""" + return [entry for entry in raw.replace(',', ' ').split() if entry] + + +def _parse_range(raw, parameter_name): + """Parse parameter min,max bounds from a comma-delimited value.""" + 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 not active.""" + if name not in spec_by_name: + return None + return spec_by_name[name]['vec'][run_num] diff --git a/docs/developers_guide/landice/test_groups/ensemble_generator.rst b/docs/developers_guide/landice/test_groups/ensemble_generator.rst index 4598a1614c..d4f211a3d8 100644 --- a/docs/developers_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/developers_guide/landice/test_groups/ensemble_generator.rst @@ -124,6 +124,15 @@ The method first loads ``[ensemble_generator] ensemble_template``. 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. diff --git a/docs/users_guide/landice/test_groups/ensemble_generator.rst b/docs/users_guide/landice/test_groups/ensemble_generator.rst index a316a006aa..d1acd66404 100644 --- a/docs/users_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/users_guide/landice/test_groups/ensemble_generator.rst @@ -199,68 +199,31 @@ The template-specific spinup config options (from # 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 + # 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] - 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. + [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 + 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. From d5896ab6b405d83b33bf7022eedf62ea030c7ea9 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 7 Mar 2026 20:49:51 -0700 Subject: [PATCH 05/13] More docs updates --- .../test_groups/ensemble_generator.rst | 156 ++++++++++-------- 1 file changed, 85 insertions(+), 71 deletions(-) diff --git a/docs/users_guide/landice/test_groups/ensemble_generator.rst b/docs/users_guide/landice/test_groups/ensemble_generator.rst index d1acd66404..ac5164293a 100644 --- a/docs/users_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/users_guide/landice/test_groups/ensemble_generator.rst @@ -23,28 +23,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,27 +99,40 @@ Future improvements may include: * safety checks or warnings before submitting ensembles that will use large amounts of computing resources -* improvements to automatically validate user-provided template names and - report available choices +Ensemble templates +------------------ + +This test group uses an ``ensemble_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 test group includes two test cases: +The selected template controls which config files and model resource files are +used for the spinup and branch cases. The package layout is: -* ``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. +.. code-block:: none -* ``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 + 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 config options -------------- -Test cases in this test group have the following common config options. - -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. The shared config option for this test group is: @@ -105,22 +145,6 @@ The shared config option for this test group is: # compass.landice.tests.ensemble_generator.ensemble_templates. ensemble_template = default -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 template-specific spinup config options (from ``ensemble_templates//spinup/ensemble_generator.cfg``) are: @@ -199,16 +223,21 @@ The template-specific spinup config options (from # 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 + # For meltflux perturbations, this observed ice-shelf area is used when + # converting target melt flux to deltaT. + iceshelf_area_obs = 60654.e6 # 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 - # Parameter definitions are listed in this section in sampling order. - # Use the prefix "nl." for float parameters that map to namelist options. +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``. + +.. code-block:: cfg + [ensemble.parameters] # special parameters (handled by custom code) @@ -224,9 +253,9 @@ The template-specific spinup config options (from 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 -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. @@ -253,27 +282,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 - -* von Mises calving - -* ISMIP6 surface mass balance and sub-ice-shelf melting using climatological - mean forcing +can be applied to any domain and set of input files using the ensemble templates +discussed above. The initial condition and forcing files are specified in the selected template file ``compass/landice/tests/ensemble_generator/ensemble_templates//spinup/ensemble_generator.cfg`` -or in a user override. branch_ensemble --------------- @@ -319,8 +333,8 @@ The default template 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`` From 415f0d07caac1f08bcede7a25ca393ea902950f2 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 7 Mar 2026 21:38:32 -0700 Subject: [PATCH 06/13] Simplify ensemble cfg Remove unnecessary extra section level, Separate spinup_ensemble options from options general to the whole test group. --- .../ensemble_generator/ensemble_generator.cfg | 7 --- .../ensemble_generator/ensemble_member.py | 14 +++-- .../default/branch/branch_ensemble.cfg | 6 ++ .../default/spinup/ensemble_generator.cfg | 17 ++++-- .../tests/ensemble_generator/plot_ensemble.py | 5 +- .../spinup_ensemble/__init__.py | 17 ++++-- .../test_groups/ensemble_generator.rst | 17 +++--- .../test_groups/ensemble_generator.rst | 56 ++++++++++--------- 8 files changed, 81 insertions(+), 58 deletions(-) delete mode 100644 compass/landice/tests/ensemble_generator/ensemble_generator.cfg diff --git a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_generator.cfg deleted file mode 100644 index 05acbf3b21..0000000000 --- a/compass/landice/tests/ensemble_generator/ensemble_generator.cfg +++ /dev/null @@ -1,7 +0,0 @@ -# global options for ensemble_generator test cases -[ensemble_generator] - -# name of the ensemble template to use -# resources are loaded from: -# compass.landice.tests.ensemble_generator.ensemble_templates. -ensemble_template = default diff --git a/compass/landice/tests/ensemble_generator/ensemble_member.py b/compass/landice/tests/ensemble_generator/ensemble_member.py index 9e6e761946..8659cbbe12 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_member.py +++ b/compass/landice/tests/ensemble_generator/ensemble_member.py @@ -136,7 +136,8 @@ def setup(self): # 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 @@ -184,7 +185,7 @@ def setup(self): # 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' @@ -195,7 +196,7 @@ def setup(self): stream_replacements = {'input_file_init_cond': new_input_fname} if self.basal_fric_exp is not None: # 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), @@ -222,7 +223,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' @@ -238,9 +240,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_templates/default/branch/branch_ensemble.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/branch/branch_ensemble.cfg index 78953eda17..709c9fbd68 100644 --- 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 @@ -1,3 +1,9 @@ +# 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] diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg index bb354f0998..7fd87b9bb4 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg @@ -1,5 +1,10 @@ +# 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. @@ -53,6 +58,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 @@ -76,10 +87,6 @@ SMB_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_f # converting target melt flux to deltaT. iceshelf_area_obs = 60654.e6 -# 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 - # 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". 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 4e138ddeb3..a5dce985ce 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -74,7 +74,12 @@ def configure(self): resource_module = get_spinup_template_package(config) add_template_file(config, resource_module, 'ensemble_generator.cfg') - section = config['ensemble'] + section = config['ensemble_generator'] + spinup_section_name = 'spinup_ensemble' + if spinup_section_name not in config: + raise ValueError( + f"Missing required config section '{spinup_section_name}'.") + spinup_section = config[spinup_section_name] parameter_section_name = 'ensemble.parameters' if parameter_section_name not in config: raise ValueError( @@ -120,15 +125,15 @@ def configure(self): if 'meltflux' in spec_by_name: if 'gamma0' not in spec_by_name: sys.exit("ERROR: parameter 'meltflux' requires 'gamma0'.") - if not section.has_option('iceshelf_area_obs'): + if not spinup_section.has_option('iceshelf_area_obs'): sys.exit( "ERROR: parameter 'meltflux' requires " - "'iceshelf_area_obs' in [ensemble].") + "'iceshelf_area_obs' in [spinup_ensemble].") # 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') + 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) diff --git a/docs/developers_guide/landice/test_groups/ensemble_generator.rst b/docs/developers_guide/landice/test_groups/ensemble_generator.rst index d4f211a3d8..482da68c3f 100644 --- a/docs/developers_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/developers_guide/landice/test_groups/ensemble_generator.rst @@ -115,13 +115,16 @@ 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 template-selected config, a parameter -array is generated, and the parameters to be varied and over what range are -defined. -The method first loads -``ensemble_templates//spinup/ensemble_generator.cfg`` based on -``[ensemble_generator] ensemble_template``. +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]``. The values for each parameter are passed to the ``EnsembleMember`` constructor to define each run. diff --git a/docs/users_guide/landice/test_groups/ensemble_generator.rst b/docs/users_guide/landice/test_groups/ensemble_generator.rst index ac5164293a..18a9a30774 100644 --- a/docs/users_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/users_guide/landice/test_groups/ensemble_generator.rst @@ -102,7 +102,7 @@ Future improvements may include: Ensemble templates ------------------ -This test group uses an ``ensemble_template``-based configuration workflow. +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 @@ -143,14 +143,14 @@ The shared config option for this test group is: # name of the ensemble template to use # resources are loaded from: # compass.landice.tests.ensemble_generator.ensemble_templates. - ensemble_template = default + ensemble_template = default 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. @@ -204,33 +204,35 @@ The template-specific spinup config options (from # 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 + + [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 + # inputs and forcings + input_file_path = /global/cfs/cdirs/fanssie/MALI_projects/Amery_UQ/Amery_4to20km_from_whole_AIS/Amery.nc - # the value of the friction exponent used for the calculation of muFriction - # in the input file - orig_fric_exp = 0.2 + # the value of the friction exponent used for the calculation of muFriction + # in the input file + orig_fric_exp = 0.2 - # 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 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 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 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 + # 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 - # 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 parameter sampling definitions live in a separate section, ``[ensemble.parameters]``. The order listed sets the sampling dimension ordering, special parameters are unprefixed, and namelist @@ -303,7 +305,11 @@ 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] @@ -342,9 +348,9 @@ Steps for setting up and running an ensemble 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 From c45a120d457ea44cbd858ec92597ab12345dc9d2 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 7 Mar 2026 21:43:41 -0700 Subject: [PATCH 07/13] syntax fixup --- .../tests/ensemble_generator/spinup_ensemble/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index a5dce985ce..f3ae55e8ac 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -76,12 +76,12 @@ def configure(self): section = config['ensemble_generator'] spinup_section_name = 'spinup_ensemble' - if spinup_section_name not in config: + 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 parameter_section_name not in config: + if not config.has_section(parameter_section_name): raise ValueError( f"Missing required config section '{parameter_section_name}'.") param_section = config[parameter_section_name] @@ -125,7 +125,7 @@ def configure(self): if 'meltflux' in spec_by_name: if 'gamma0' not in spec_by_name: sys.exit("ERROR: parameter 'meltflux' requires 'gamma0'.") - if not spinup_section.has_option('iceshelf_area_obs'): + if not config.has_option('spinup_ensemble', 'iceshelf_area_obs'): sys.exit( "ERROR: parameter 'meltflux' requires " "'iceshelf_area_obs' in [spinup_ensemble].") From 8e0fdb2183c4e314d271b52fb15c8f5bb7d3719d Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sun, 8 Mar 2026 08:21:32 -0600 Subject: [PATCH 08/13] Make albany input file optional --- .../branch_ensemble/branch_run.py | 7 +++--- .../ensemble_generator/ensemble_member.py | 23 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) 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 7519a44ddd..360fc1bded 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/branch_run.py +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/branch_run.py @@ -86,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 diff --git a/compass/landice/tests/ensemble_generator/ensemble_member.py b/compass/landice/tests/ensemble_generator/ensemble_member.py index 8659cbbe12..f29008ea90 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_member.py +++ b/compass/landice/tests/ensemble_generator/ensemble_member.py @@ -157,14 +157,14 @@ 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) + has_albany_input = albany_source.is_file() + if has_albany_input: + shutil.copy(str(albany_source), self.work_dir) self.add_model_as_input() @@ -195,13 +195,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 = 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}') From 5bacbd6c6ac70e3ade462d2ffbc1648aa19937dc Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Fri, 13 Mar 2026 21:26:09 -0600 Subject: [PATCH 09/13] Fix trailing whitespace for linting --- .../ensemble_templates/default/spinup/ensemble_generator.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg index 7fd87b9bb4..968179ba74 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg @@ -110,4 +110,3 @@ nl.von_mises_threshold.option_name = \ # 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 - From fba3b5ca3a471855bddfe14900577aaea85b0567 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 14 Mar 2026 10:19:14 -0700 Subject: [PATCH 10/13] Refactor spinup_ensemble/__init__.py to satisfy linter This function was flagged as too complex, so Copilot helped me break it into smaller functions. --- .../spinup_ensemble/__init__.py | 336 ++++++++++++------ 1 file changed, 218 insertions(+), 118 deletions(-) diff --git a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index f3ae55e8ac..780c2e4d52 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -4,16 +4,16 @@ from scipy.stats import qmc from compass.landice.iceshelf_melt import calc_mean_TF -from compass.landice.tests.ensemble_generator.ensemble_template import ( - add_template_file, - get_spinup_template_package, -) from compass.landice.tests.ensemble_generator.ensemble_manager import ( EnsembleManager, ) 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 @@ -62,14 +62,6 @@ 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') @@ -97,115 +89,29 @@ def configure(self): if n_params == 0: sys.exit("ERROR: At least one parameter must be specified.") - 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.") + sampling_method = section.get('sampling_method') + param_unit_values = _sample_parameter_unit_values( + sampling_method=sampling_method, n_params=n_params, + max_samples=max_samples) - # Define parameter vectors - for idx, spec in enumerate(parameter_specs): - print('Including parameter ' + spec['name']) - spec['vec'] = param_unit_values[:, idx] * \ - (spec['max'] - spec['min']) + spec['min'] + parameter_specs = _populate_parameter_vectors( + parameter_specs, param_unit_values) spec_by_name = {spec['name']: spec for spec in parameter_specs} - # melt flux needs to be converted to deltaT - if 'meltflux' in spec_by_name: - 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].") - - # First calculate mean TF for this domain - 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) - - # 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.") - spec_by_name['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 = (spec_by_name['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(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'") - 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): - namelist_option_values = {} - namelist_parameter_values = {} - for spec in parameter_specs: - if spec['type'] == 'namelist': - 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 - - 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) - - self.add_step(EnsembleMember( - test_case=self, 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 + 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 @@ -218,7 +124,18 @@ def configure(self): def _get_parameter_specs(section): - """Parse ordered perturbation definitions from [ensemble.parameters].""" + """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'} @@ -264,13 +181,190 @@ def _get_parameter_specs(section): return specs +def _sample_parameter_unit_values(sampling_method, n_params, max_samples): + """Create an ``(max_samples, n_params)`` array in unit space. + + The returned values are in the range ``[0, 1]`` and are later scaled to + each parameter's configured min/max bounds. + + Returns + ------- + numpy.ndarray + Unit-space samples with shape ``(max_samples, n_params)``. + """ + if sampling_method == 'sobol': + print(f"Generating Sobol sequence for {n_params} parameter(s)") + sampler = qmc.Sobol(d=n_params, scramble=True, seed=4) + return sampler.random(n=max_samples) + + if 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) + return np.tile(samples, (1, n_params)) + + sys.exit("ERROR: Unsupported sampling method specified.") + + +def _populate_parameter_vectors(parameter_specs, param_unit_values): + """Scale unit samples to configured parameter ranges. + + This function updates each ``spec['vec']`` in ``parameter_specs`` and + returns the same list for explicit readability at call site. + + Returns + ------- + list of dict + The same ``parameter_specs`` list with each ``spec['vec']`` populated. + """ + for idx, spec in enumerate(parameter_specs): + print('Including parameter ' + spec['name']) + 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.""" + """Split comma- or whitespace-delimited config lists. + + Returns + ------- + list of str + Non-empty parsed entries. + """ return [entry for entry in raw.replace(',', ' ').split() if entry] def _parse_range(raw, parameter_name): - """Parse parameter min,max bounds from a comma-delimited value.""" + """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( @@ -280,7 +374,13 @@ def _parse_range(raw, parameter_name): def _get_special_value(spec_by_name, name, run_num): - """Get sampled value for a special parameter or None if not active.""" + """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] From 3c3dce38dee0cd16f772ed231146382ec9ba6c7e Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 14 Mar 2026 10:47:36 -0700 Subject: [PATCH 11/13] Add log-uniform sampling method --- .../default/spinup/ensemble_generator.cfg | 19 +++--- .../spinup_ensemble/__init__.py | 66 ++++++++++--------- .../test_groups/ensemble_generator.rst | 1 + .../test_groups/ensemble_generator.rst | 26 +++++--- 4 files changed, 64 insertions(+), 48 deletions(-) diff --git a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg index 968179ba74..72786e6a0c 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg +++ b/compass/landice/tests/ensemble_generator/ensemble_templates/default/spinup/ensemble_generator.cfg @@ -11,23 +11,26 @@ ensemble_template = default # 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 diff --git a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index 780c2e4d52..0ef422b1fe 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -93,12 +93,10 @@ def configure(self): if max_samples < self.end_run: sys.exit("ERROR: max_samples is exceeded by end_run") sampling_method = section.get('sampling_method') - param_unit_values = _sample_parameter_unit_values( - sampling_method=sampling_method, n_params=n_params, - max_samples=max_samples) - parameter_specs = _populate_parameter_vectors( - parameter_specs, param_unit_values) + parameter_specs=parameter_specs, + sampling_method=sampling_method, + max_samples=max_samples) spec_by_name = {spec['name']: spec for spec in parameter_specs} @@ -181,45 +179,51 @@ def _get_parameter_specs(section): return specs -def _sample_parameter_unit_values(sampling_method, n_params, max_samples): - """Create an ``(max_samples, n_params)`` array in unit space. +def _populate_parameter_vectors(parameter_specs, sampling_method, + max_samples): + """Generate and scale samples to each parameter range. - The returned values are in the range ``[0, 1]`` and are later scaled to - each parameter's configured min/max bounds. + 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 ------- - numpy.ndarray - Unit-space samples with shape ``(max_samples, n_params)``. + 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) - return sampler.random(n=max_samples) - - if sampling_method == 'uniform': - print(f"Generating uniform sampling for {n_params} parameter(s)") + 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) - return np.tile(samples, (1, n_params)) - - sys.exit("ERROR: Unsupported sampling method specified.") - + param_unit_values = np.tile(samples, (1, n_params)) + else: + sys.exit("ERROR: Unsupported sampling method specified.") -def _populate_parameter_vectors(parameter_specs, param_unit_values): - """Scale unit samples to configured parameter ranges. + 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']}'.") - This function updates each ``spec['vec']`` in ``parameter_specs`` and - returns the same list for explicit readability at call site. - - Returns - ------- - list of dict - The same ``parameter_specs`` list with each ``spec['vec']`` populated. - """ for idx, spec in enumerate(parameter_specs): print('Including parameter ' + spec['name']) - spec['vec'] = param_unit_values[:, idx] * \ - (spec['max'] - spec['min']) + spec['min'] + 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 diff --git a/docs/developers_guide/landice/test_groups/ensemble_generator.rst b/docs/developers_guide/landice/test_groups/ensemble_generator.rst index 482da68c3f..5362c5b00a 100644 --- a/docs/developers_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/developers_guide/landice/test_groups/ensemble_generator.rst @@ -125,6 +125,7 @@ Spinup run-control options (for example, ``start_run``, ``end_run``, 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. diff --git a/docs/users_guide/landice/test_groups/ensemble_generator.rst b/docs/users_guide/landice/test_groups/ensemble_generator.rst index 18a9a30774..dbc3f1dfd4 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. @@ -157,23 +158,27 @@ The template-specific spinup config options (from # 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 @@ -238,6 +243,9 @@ The parameter sampling definitions live in a separate section, 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] From 66ae3ef812da731c7cecb935c7554d22bf400a81 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 14 Mar 2026 10:57:30 -0700 Subject: [PATCH 12/13] Fix isort error showing up in github action --- .../tests/ensemble_generator/branch_ensemble/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py index a7fb8e1281..07d8c9db62 100644 --- a/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/branch_ensemble/__init__.py @@ -4,16 +4,16 @@ import numpy as np -from compass.landice.tests.ensemble_generator.ensemble_template import ( - add_template_file, - get_branch_template_package, -) from compass.landice.tests.ensemble_generator.branch_ensemble.branch_run import ( # noqa BranchRun, ) 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 From b0e79794fac21c10e4155b7efce7ebf293c90fba Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 14 Mar 2026 11:26:36 -0700 Subject: [PATCH 13/13] Apply Copilot review fixes for ensemble generator Update ensemble generator parsing and docs based on outdated but still relevant Copilot review feedback from PR #940. - Sanitize multiline option parsing in spinup_ensemble._split_entries to remove continuation backslashes before tokenization. - Use importlib.resources.as_file() when handling optional albany_input.yaml in ensemble_member setup. - Clarify in developer docs that albany_input.yaml is copied only when present for Albany-based configurations. - Fix users-guide cfg block indentation and multiline .option_name examples to match ConfigParser behavior. --- .../ensemble_generator/ensemble_member.py | 9 ++++-- .../spinup_ensemble/__init__.py | 8 ++++- .../test_groups/ensemble_generator.rst | 8 ++--- .../test_groups/ensemble_generator.rst | 32 +++++++++---------- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/compass/landice/tests/ensemble_generator/ensemble_member.py b/compass/landice/tests/ensemble_generator/ensemble_member.py index f29008ea90..16554a0355 100644 --- a/compass/landice/tests/ensemble_generator/ensemble_member.py +++ b/compass/landice/tests/ensemble_generator/ensemble_member.py @@ -162,9 +162,12 @@ def setup(self): albany_input_path = os.path.join(self.work_dir, albany_input_name) albany_source = resources.files(resource_module).joinpath( albany_input_name) - has_albany_input = albany_source.is_file() - if has_albany_input: - shutil.copy(str(albany_source), self.work_dir) + # 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() diff --git a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py index 0ef422b1fe..7f4403e7f8 100644 --- a/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py +++ b/compass/landice/tests/ensemble_generator/spinup_ensemble/__init__.py @@ -353,12 +353,18 @@ def _add_member_steps(test_case, parameter_specs, spec_by_name, deltaT_vec, 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. """ - return [entry for entry in raw.replace(',', ' ').split() if entry] + 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): diff --git a/docs/developers_guide/landice/test_groups/ensemble_generator.rst b/docs/developers_guide/landice/test_groups/ensemble_generator.rst index 5362c5b00a..309d7f80e5 100644 --- a/docs/developers_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/developers_guide/landice/test_groups/ensemble_generator.rst @@ -166,10 +166,10 @@ 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.yaml 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 diff --git a/docs/users_guide/landice/test_groups/ensemble_generator.rst b/docs/users_guide/landice/test_groups/ensemble_generator.rst index dbc3f1dfd4..0304e6d3e7 100644 --- a/docs/users_guide/landice/test_groups/ensemble_generator.rst +++ b/docs/users_guide/landice/test_groups/ensemble_generator.rst @@ -144,41 +144,41 @@ The shared config option for this test group is: # name of the ensemble template to use # resources are loaded from: # compass.landice.tests.ensemble_generator.ensemble_templates. - ensemble_template = default + ensemble_template = default The template-specific spinup config options (from ``ensemble_templates//spinup/ensemble_generator.cfg``) are: .. code-block:: cfg - [ensemble_generator] + [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 or log-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 '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 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 or log-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 @@ -259,8 +259,8 @@ positive because sampling is performed in log space. # 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, \ + 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