diff --git a/compass/landice/__init__.py b/compass/landice/__init__.py index 6fec487fd7..aafe35fdd6 100644 --- a/compass/landice/__init__.py +++ b/compass/landice/__init__.py @@ -12,6 +12,7 @@ from compass.landice.tests.ismip6_forcing import Ismip6Forcing from compass.landice.tests.ismip6_run import Ismip6Run from compass.landice.tests.ismip7_forcing import Ismip7Forcing +from compass.landice.tests.ismip7_run import Ismip7Run from compass.landice.tests.isunnguata_sermia import IsunnguataSermia from compass.landice.tests.kangerlussuaq import Kangerlussuaq from compass.landice.tests.koge_bugt_s import KogeBugtS @@ -48,6 +49,7 @@ def __init__(self): self.add_test_group(Ismip6Forcing(mpas_core=self)) self.add_test_group(Ismip6Run(mpas_core=self)) self.add_test_group(Ismip7Forcing(mpas_core=self)) + self.add_test_group(Ismip7Run(mpas_core=self)) self.add_test_group(IsunnguataSermia(mpas_core=self)) self.add_test_group(Kangerlussuaq(mpas_core=self)) self.add_test_group(KogeBugtS(mpas_core=self)) diff --git a/compass/landice/tests/ismip7_run/__init__.py b/compass/landice/tests/ismip7_run/__init__.py new file mode 100644 index 0000000000..64eb375d00 --- /dev/null +++ b/compass/landice/tests/ismip7_run/__init__.py @@ -0,0 +1,19 @@ +from compass.landice.tests.ismip7_run.ismip7_ais import Ismip7Ais +from compass.landice.tests.ismip7_run.ismip7_gris import Ismip7Gris +from compass.testgroup import TestGroup + + +class Ismip7Run(TestGroup): + """ + A test group for automated setup of a suite of standardized + ISMIP7 simulations for both AIS and GrIS. + """ + def __init__(self, mpas_core): + """ + mpas_core : compass.landice.Landice + the MPAS core that this test group belongs to + """ + super().__init__(mpas_core=mpas_core, name='ismip7_run') + + self.add_test_case(Ismip7Ais(test_group=self)) + self.add_test_case(Ismip7Gris(test_group=self)) diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/__init__.py b/compass/landice/tests/ismip7_run/ismip7_ais/__init__.py new file mode 100644 index 0000000000..c6e3a41c94 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/__init__.py @@ -0,0 +1,157 @@ +import os + +from compass.landice.tests.ismip7_run.ismip7_ais.create_slm_mapping_files import ( # noqa + CreateSlmMappingFiles, +) +from compass.landice.tests.ismip7_run.ismip7_ais.set_up_experiment import ( + SetUpExperiment, +) +from compass.testcase import TestCase + +# Define the full experiment matrix per the ISMIP7 protocol +EXPERIMENTS = { + 'historical_CESM2-WACCM': { + 'scenario': 'historical', 'model': 'CESM2-WACCM', + 'start_time': '2000-01-01_00:00:00', + 'stop_time': '2015-01-01_00:00:00', + 'is_historical': True}, + 'historical_MRI-ESM2-0': { + 'scenario': 'historical', 'model': 'MRI-ESM2-0', + 'start_time': '2000-01-01_00:00:00', + 'stop_time': '2015-01-01_00:00:00', + 'is_historical': True}, + 'ssp370_CESM2-WACCM': { + 'scenario': 'ssp370', 'model': 'CESM2-WACCM', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2101-01-01_00:00:00', + 'is_historical': False}, + 'ssp370_MRI-ESM2-0': { + 'scenario': 'ssp370', 'model': 'MRI-ESM2-0', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2101-01-01_00:00:00', + 'is_historical': False}, + 'ssp126_CESM2-WACCM': { + 'scenario': 'ssp126', 'model': 'CESM2-WACCM', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ssp126_MRI-ESM2-0': { + 'scenario': 'ssp126', 'model': 'MRI-ESM2-0', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ssp585_CESM2-WACCM': { + 'scenario': 'ssp585', 'model': 'CESM2-WACCM', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ssp585_MRI-ESM2-0': { + 'scenario': 'ssp585', 'model': 'MRI-ESM2-0', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ctrl_CESM2-WACCM': { + 'scenario': 'ctrl', 'model': 'CESM2-WACCM', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ctrl_MRI-ESM2-0': { + 'scenario': 'ctrl', 'model': 'MRI-ESM2-0', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ocx': { + 'scenario': 'ocx', 'model': None, + 'start_time': '1990-01-01_00:00:00', + 'stop_time': '2026-01-01_00:00:00', + 'is_historical': True}, +} + + +class Ismip7Ais(TestCase): + """ + A test case for automated setup of a suite of standardized + ISMIP7 simulations for the Antarctic Ice Sheet. + """ + + def __init__(self, test_group): + """ + Create the test case + + Parameters + ---------- + test_group : compass.landice.tests.ismip7_run.Ismip7Run + The test group that this test case belongs to + """ + name = 'ismip7_ais' + super().__init__(test_group=test_group, name=name, subdir=name) + + def configure(self): + """ + Set up the desired ISMIP7 AIS experiments. + + Read the experiment list from config and add a SetUpExperiment + step for each. + """ + config = self.config + exp_list_str = config.get('ismip7_run_ais', 'exp_list') + + if exp_list_str == 'all': + exp_list = list(EXPERIMENTS.keys()) + elif exp_list_str == 'historical': + exp_list = [k for k, v in EXPERIMENTS.items() + if v['is_historical']] + elif exp_list_str == 'projections': + exp_list = [k for k, v in EXPERIMENTS.items() + if not v['is_historical'] and + v['scenario'] != 'ctrl'] + elif exp_list_str == 'ctrl': + exp_list = [k for k, v in EXPERIMENTS.items() + if v['scenario'] == 'ctrl'] + else: + exp_list = [s.strip() for s in exp_list_str.split(',')] + + for exp in exp_list: + if exp not in EXPERIMENTS: + raise ValueError( + f"Unknown experiment '{exp}'. Valid experiments: " + f"{list(EXPERIMENTS.keys())}") + if os.path.exists(os.path.join(self.work_dir, exp)): + print(f"WARNING: {exp} path already exists; skipping. " + "Remove the directory " + f"{os.path.join(self.work_dir, exp)} and run " + "'compass setup' again to recreate.") + else: + self.add_step( + SetUpExperiment(test_case=self, name=exp, + subdir=exp, exp=exp, + exp_info=EXPERIMENTS[exp])) + + # Do not add experiments to steps_to_run; + # each experiment (step) should be submitted manually + self.steps_to_run = [] + + # Optionally set up sea-level model mapping files + sea_level_model = config.getboolean('ismip7_run_ais', + 'sea_level_model') + if sea_level_model: + subdir = 'mapping_files' + if os.path.exists(os.path.join(self.work_dir, subdir)): + print(f"WARNING: {subdir} path already exists; skipping.") + else: + self.add_step( + CreateSlmMappingFiles(test_case=self, + name='mapping_files', + subdir=subdir)) + self.steps_to_run.append('mapping_files') + + def run(self): + """ + A dummy run method + """ + raise ValueError( + "ERROR: 'compass run' has no functionality at the test case " + "level for this test. Please submit the job script in each " + "experiment's subdirectory manually instead. " + "To create Sea-Level Model mapping files, submit job script " + "or execute 'compass run' from the 'mapping_files' subdirectory.") diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/albany_input.yaml b/compass/landice/tests/ismip7_run/ismip7_ais/albany_input.yaml new file mode 100644 index 0000000000..e1a4dcd7c4 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/albany_input.yaml @@ -0,0 +1,239 @@ +%YAML 1.1 +--- +ANONYMOUS: + Problem: + LandIce Field Norm: + sliding_velocity_basalside: + Regularization Type: Given Value + Regularization Value: 1.0e-4 + LandIce BCs: + BC 0: + Basal Friction Coefficient: + Type: Power Law + Power Exponent: 0.2 + Mu Type: Field + Effective Pressure Type: Constant + Effective Pressure: 1.0 + # Zero Effective Pressure On Floating Ice At Nodes: true + Zero Beta On Floating Ice: true + + Cubature Degree: 8 + +# Discretization Description + Discretization: + #Exodus Output File Name: albany_output.exo + + Piro: +# Nonlinear Solver Information + NOX: + Nonlinear Solver: Line Search Based + Line Search: + Full Step: + Full Step: 1.0e+00 + Method: Backtrack + Solver Options: + Status Test Check Type: Minimal + Status Tests: + Test Type: Combo + Combo Type: OR + Number of Tests: 2 + Test 0: + Test Type: NormF + Norm Type: Two Norm + Scale Type: Scaled + Tolerance: 1.0e-05 + Test 1: + Test Type: MaxIters + Maximum Iterations: 50 + Printing: + Output Precision: 3 + Output Processor: 0 + Output Information: + Error: true + Warning: true + Outer Iteration: true + Parameters: false + Details: false + Linear Solver Details: false + Stepper Iteration: true + Stepper Details: true + Stepper Parameters: true + + Direction: + Method: Newton + Newton: + Forcing Term Method: Constant + Rescue Bad Newton Solve: true + Linear Solver: + Write Linear System: false + Tolerance: 1.0e-8 + + Stratimikos Linear Solver: + Stratimikos: + +# Linear Solver Information + Linear Solver Type: Belos + Linear Solver Types: + Belos: + Solver Type: Block GMRES + Solver Types: + Block GMRES: + Output Frequency: 20 + Output Style: 1 + Verbosity: 33 + Maximum Iterations: 200 + Block Size: 1 + Num Blocks: 200 + Flexible Gmres: false + VerboseObject: + Output File: none + Verbosity Level: low + +# Preconditioner Information + Preconditioner Type: MueLu + Preconditioner Types: + + Ifpack2: + Overlap: 1 + Prec Type: ILUT + + MueLu: + Matrix: + PDE equations: 2 + Factories: + myLineDetectionFact: + factory: LineDetectionFactory + 'linedetection: orientation': coordinates + mySemiCoarsenPFact1: + factory: SemiCoarsenPFactory + 'semicoarsen: coarsen rate': 14 + UncoupledAggregationFact2: + factory: UncoupledAggregationFactory + 'aggregation: ordering': graph + 'aggregation: max selected neighbors': 0 + 'aggregation: min agg size': 3 + 'aggregation: phase3 avoid singletons': true + MyCoarseMap2: + factory: CoarseMapFactory + Aggregates: UncoupledAggregationFact2 + myTentativePFact2: + 'tentative: calculate qr': true + factory: TentativePFactory + Aggregates: UncoupledAggregationFact2 + CoarseMap: MyCoarseMap2 + mySaPFact2: + 'sa: eigenvalue estimate num iterations': 10 + 'sa: damping factor': 1.33333e+00 + factory: SaPFactory + P: myTentativePFact2 + myTransferCoordinatesFact: + factory: CoordinatesTransferFactory + CoarseMap: MyCoarseMap2 + Aggregates: UncoupledAggregationFact2 + myTogglePFact: + factory: TogglePFactory + 'semicoarsen: number of levels': 2 + TransferFactories: + P1: mySemiCoarsenPFact1 + P2: mySaPFact2 + Ptent1: mySemiCoarsenPFact1 + Ptent2: myTentativePFact2 + Nullspace1: mySemiCoarsenPFact1 + Nullspace2: myTentativePFact2 + myRestrictorFact: + factory: TransPFactory + P: myTogglePFact + myToggleTransferCoordinatesFact: + factory: ToggleCoordinatesTransferFactory + Chosen P: myTogglePFact + TransferFactories: + Coordinates1: mySemiCoarsenPFact1 + Coordinates2: myTransferCoordinatesFact + myRAPFact: + factory: RAPFactory + P: myTogglePFact + R: myRestrictorFact + TransferFactories: + For Coordinates: myToggleTransferCoordinatesFact + myRepartitionHeuristicFact: + factory: RepartitionHeuristicFactory + A: myRAPFact + 'repartition: min rows per proc': 3000 + 'repartition: max imbalance': 1.327e+00 + 'repartition: start level': 1 + myZoltanInterface: + factory: ZoltanInterface + A: myRAPFact + Coordinates: myToggleTransferCoordinatesFact + number of partitions: myRepartitionHeuristicFact + myRepartitionFact: + factory: RepartitionFactory + A: myRAPFact + Partition: myZoltanInterface + 'repartition: remap parts': true + number of partitions: myRepartitionHeuristicFact + myRebalanceProlongatorFact: + factory: RebalanceTransferFactory + type: Interpolation + P: myTogglePFact + Coordinates: myToggleTransferCoordinatesFact + Nullspace: myTogglePFact + myRebalanceRestrictionFact: + factory: RebalanceTransferFactory + type: Restriction + R: myRestrictorFact + myRebalanceAFact: + factory: RebalanceAcFactory + A: myRAPFact + TransferFactories: { } + mySmoother1: + factory: TrilinosSmoother + type: LINESMOOTHING_BANDEDRELAXATION + 'smoother: pre or post': both + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 1 + 'relaxation: damping factor': 1.0 + mySmoother3: + factory: TrilinosSmoother + type: RELAXATION + 'smoother: pre or post': both + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 1 + 'relaxation: damping factor': 1.0 + mySmoother4: + factory: TrilinosSmoother + type: RELAXATION + 'smoother: pre or post': pre + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 4 + 'relaxation: damping factor': 1.0 + Hierarchy: + max levels: 7 + 'coarse: max size': 2000 + verbosity: None + Finest: + Smoother: mySmoother1 + CoarseSolver: mySmoother4 + P: myRebalanceProlongatorFact + Nullspace: myRebalanceProlongatorFact + CoarseNumZLayers: myLineDetectionFact + LineDetection_Layers: myLineDetectionFact + LineDetection_VertLineIds: myLineDetectionFact + A: myRebalanceAFact + Coordinates: myRebalanceProlongatorFact + Importer: myRepartitionFact + All: + startLevel: 1 + Smoother: mySmoother4 + CoarseSolver: mySmoother4 + P: myRebalanceProlongatorFact + Nullspace: myRebalanceProlongatorFact + CoarseNumZLayers: myLineDetectionFact + LineDetection_Layers: myLineDetectionFact + LineDetection_VertLineIds: myLineDetectionFact + A: myRebalanceAFact + Coordinates: myRebalanceProlongatorFact + Importer: myRepartitionFact diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/create_slm_mapping_files.py b/compass/landice/tests/ismip7_run/ismip7_ais/create_slm_mapping_files.py new file mode 100644 index 0000000000..108ee7544c --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/create_slm_mapping_files.py @@ -0,0 +1,100 @@ +import shutil + +from mpas_tools.logging import check_call +from mpas_tools.scrip.from_mpas import scrip_from_mpas + +from compass.step import Step + + +class CreateSlmMappingFiles(Step): + """ + A step for creating mapping files for the Sea Level Model + """ + + def __init__(self, test_case, name, subdir): + """ + Initialize step + """ + super().__init__(test_case=test_case, name=name, subdir=subdir) + + def setup(self): + print(" Setting up mapping_file subdirectory") + + def run(self): + """ + Run this step of the test case + """ + config = self.config + logger = self.logger + section = config['ismip7_run_ais'] + sea_level_model = section.getboolean('sea_level_model') + if sea_level_model: + self._build_mapping_files(config, logger) + + def _build_mapping_files(self, config, logger): + """ + Build mapping files between the MALI mesh and the SLM grid. + """ + section = config['ismip7_run_ais'] + init_cond_path = section.get('init_cond_path') + nglv = section.getint('nglv') + section = config['parallel'] + ntasks = section.getint('cores_per_node') + + mali_scripfile = 'mali_scripfile.nc' + slm_scripfile = f'slm_nglv{nglv}scripfile.nc' + mali_meshfile = 'mali_meshfile_sphereLatLon.nc' + + # SLM scripfile + logger.info(f'creating scripfile for the SLM grid with ' + f'{nglv} Gauss-Legendre points in latitude') + + args = ['ncremap', + '-g', slm_scripfile, + '-G', + f'latlon={nglv},{2 * int(nglv)}#lat_typ=gss#lat_drc=n2s'] + + check_call(args, logger=logger) + + # MALI scripfile + shutil.copy(init_cond_path, mali_meshfile) + args = ['set_lat_lon_fields_in_planar_grid', + '--file', mali_meshfile, + '--proj', 'ais-bedmap2-sphere'] + + check_call(args, logger=logger) + + logger.info('creating scrip file for the mali mesh') + scrip_from_mpas(mali_meshfile, mali_scripfile) + + # MALI -> SLM mapping file + logger.info('creating MALI -> SLM grid mapfile with conserve method') + + parallel_executable = config.get("parallel", "parallel_executable") + args = parallel_executable.split(' ') + args.extend(['-n', f'{ntasks}', + 'ESMF_RegridWeightGen', + '-s', mali_scripfile, + '-d', slm_scripfile, + '-w', 'mapfile_mali_to_slm.nc', + '-m', 'conserve', + '-i', '-64bit_offset', '--netcdf4', + '--src_regional']) + + check_call(args, logger) + + # SLM -> MALI mapping file + logger.info('creating SLM -> MALI mesh mapfile with bilinear method') + args = parallel_executable.split(' ') + args.extend(['-n', f'{ntasks}', + 'ESMF_RegridWeightGen', + '-s', slm_scripfile, + '-d', mali_scripfile, + '-w', 'mapfile_slm_to_mali.nc', + '-m', 'bilinear', + '-i', '-64bit_offset', '--netcdf4', + '--dst_regional']) + + check_call(args, logger) + + logger.info('mapping file creation complete') diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/ismip7_ais.cfg b/compass/landice/tests/ismip7_run/ismip7_ais/ismip7_ais.cfg new file mode 100644 index 0000000000..745e4c89f7 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/ismip7_ais.cfg @@ -0,0 +1,82 @@ +[ismip7_run_ais] + +# List of experiments to set up. +# Can be "all", "historical", "projections", "ctrl", or a +# comma-delimited list of experiment names. +# Valid experiment names: +# historical_CESM2-WACCM, historical_MRI-ESM2-0, +# ssp126_CESM2-WACCM, ssp126_MRI-ESM2-0, +# ssp370_CESM2-WACCM, ssp370_MRI-ESM2-0, +# ssp585_CESM2-WACCM, ssp585_MRI-ESM2-0, +# ctrl_CESM2-WACCM, ctrl_MRI-ESM2-0, +# ocx +exp_list = all + +# Number of tasks to use for each run +ntasks = 128 + +# Value to use for config_pio_stride. +# Should be divisible into ntasks +pio_stride = 128 + +# Base path to the pre-processed ISMIP7 forcing files. +# Expected layout: +# {forcing_basepath}/{model}_{scenario}/atmosphere/ +# {forcing_basepath}/{model}_{scenario}/ocean_thermal_forcing/ +# User has to supply. +forcing_basepath = NotAvailable + +# Path to the initial condition file. User has to supply. +init_cond_path = NotAvailable + +# Path to the file for the basal melt parametrization coefficients. +melt_params_path = NotAvailable + +# Path to the reference surface elevation file +reference_surface_path = NotAvailable + +# Path to the region mask file +region_mask_path = NotAvailable + +# Path to the ocean thermal forcing climatology file for CTRL2015 runs. +# This is the constant-climate 30-year mean (2000-2029) thermal forcing. +# User has to supply if running ctrl experiments. +ctrl_tf_climatology_path = NotAvailable + +# Path to the atmosphere climatology files for CTRL2015 runs. +# Directory containing constant-climate SMB, temperature, etc. +# User has to supply if running ctrl experiments. +ctrl_atm_climatology_path = NotAvailable + +# Path to OCX (observationally constrained experiment) forcing. +# User has to supply if running the ocx experiment. +ocx_forcing_path = NotAvailable + +# Calving method to use. Options: restore, von_mises +calving_method = restore + +# Path to the file containing the von Mises parameter fields. +# Only required if calving_method is set to 'von_mises'. +von_mises_parameter_path = NotAvailable + +# True if running coupled MALI-sea level model simulation +sea_level_model = false + +# Path to the directory containing globally defined ice thickness +# field for the sea-level model +slm_input_ice = NotAvailable + +# Path to the directory containing earth model for the sea-level model +slm_input_earth = NotAvailable + +# Earth structure profile filename +slm_earth_structure = prem_512.l60K2C.sum18p6.dum19p2.tz19p4.lm22 + +# Path to the directory containing other SLM input files +slm_input_others = NotAvailable + +# Number of gauss-legendre nodes in latitude (typically multiple of 512) +nglv = 2048 + +[parallel] +parallel_executable = srun --label --cpu-bind=cores diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/ismip7_ais_test.cfg b/compass/landice/tests/ismip7_run/ismip7_ais/ismip7_ais_test.cfg new file mode 100644 index 0000000000..4d9f05318b --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/ismip7_ais_test.cfg @@ -0,0 +1,82 @@ +[ismip7_run_ais] + +# List of experiments to set up. +# Can be "all", "historical", "projections", "ctrl", or a +# comma-delimited list of experiment names. +# Valid experiment names: +# historical_CESM2-WACCM, historical_MRI-ESM2-0, +# ssp126_CESM2-WACCM, ssp126_MRI-ESM2-0, +# ssp370_CESM2-WACCM, ssp370_MRI-ESM2-0, +# ssp585_CESM2-WACCM, ssp585_MRI-ESM2-0, +# ctrl_CESM2-WACCM, ctrl_MRI-ESM2-0, +# ocx +exp_list = historical_CESM2-WACCM,ssp585_CESM2-WACCM + +# Number of tasks to use for each run +ntasks = 512 + +# Value to use for config_pio_stride. +# Should be divisible into ntasks +pio_stride = 128 + +# Base path to the pre-processed ISMIP7 forcing files. +# Expected layout: +# {forcing_basepath}/{model}_{scenario}/atmosphere/ +# {forcing_basepath}/{model}_{scenario}/ocean_thermal_forcing/ +# User has to supply. +forcing_basepath = /global/cfs/cdirs/m4288/users/trhille/ISMIP7/test_processing/AIS/ + +# Path to the initial condition file. User has to supply. +init_cond_path = /global/cfs/cdirs/fanssie/MALI_projects/ISMIP6-2300/initial_conditions/AIS_4to20km_20230105/relaxation_0TGmelt_10yr/relaxed_10yrs_4km.nc + +# Path to the file for the basal melt parametrization coefficients. +melt_params_path = /global/cfs/cdirs/m4288/users/trhille/ISMIP7/test_processing/AIS/climatology/basin_and_coeff_DeltaT_quadratic_non_local_gamma14500.nc + +# Path to the reference surface elevation file +reference_surface_path = /global/cfs/cdirs/m4288/users/trhille/ISMIP7/forcing/AIS/upper_surface.nc + +# Path to the region mask file +region_mask_path = /global/cfs/cdirs/fanssie/MALI_projects/ISMIP6-2300/initial_conditions/AIS_4to20km_20230105/AIS_4to20km_r01_20220907.regionMask_ismip6.nc + +# Path to the ocean thermal forcing climatology file for CTRL2015 runs. +# This is the constant-climate 30-year mean (2000-2029) thermal forcing. +# User has to supply if running ctrl experiments. +ctrl_tf_climatology_path = NotAvailable + +# Path to the atmosphere climatology files for CTRL2015 runs. +# Directory containing constant-climate SMB, temperature, etc. +# User has to supply if running ctrl experiments. +ctrl_atm_climatology_path = NotAvailable + +# Path to OCX (observationally constrained experiment) forcing. +# User has to supply if running the ocx experiment. +ocx_forcing_path = NotAvailable + +# Calving method to use. Options: restore, von_mises +calving_method = restore + +# Path to the file containing the von Mises parameter fields. +# Only required if calving_method is set to 'von_mises'. +von_mises_parameter_path = NotAvailable + +# True if running coupled MALI-sea level model simulation +sea_level_model = false + +# Path to the directory containing globally defined ice thickness +# field for the sea-level model +slm_input_ice = NotAvailable + +# Path to the directory containing earth model for the sea-level model +slm_input_earth = NotAvailable + +# Earth structure profile filename +slm_earth_structure = prem_512.l60K2C.sum18p6.dum19p2.tz19p4.lm22 + +# Path to the directory containing other SLM input files +slm_input_others = NotAvailable + +# Number of gauss-legendre nodes in latitude (typically multiple of 512) +nglv = 2048 + +[parallel] +parallel_executable = srun --label --cpu-bind=cores diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/namelist.landice b/compass/landice/tests/ismip7_run/ismip7_ais/namelist.landice new file mode 100644 index 0000000000..165f9611f6 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/namelist.landice @@ -0,0 +1,87 @@ + config_velocity_solver = 'FO' + config_do_velocity_reconstruction_for_external_dycore = .false. + config_unrealistic_velocity = 00.00159 + config_nonconvergence_error = .false. + config_flowParamA_calculation = 'PB1982' + + config_thickness_advection = 'fo' + config_tracer_advection = 'fo' + config_zero_sfcMassBalApplied_over_bare_land = .true. + config_apply_smb_sat_lapse_rate = .true. + + config_uplift_method = 'none' + config_slm_coupling_interval = 5 + config_MALI_to_SLM_weights_file = 'mapfile_mali_to_slm.nc' + config_SLM_to_MALI_weights_file = 'mapfile_slm_to_mali.nc' + + config_calving = 'none' + config_apply_calving_mask = .false. + config_restore_calving_front_prevent_retreat = .false. + config_calculate_damage = .true. + config_damage_calving_threshold = 0.95 + config_damage_calving_method = 'none' + config_calving_speed_limit = 0.00063492063 + config_restore_calving_front = .true. + config_remove_icebergs = .true. + config_remove_small_islands = .true. + config_distribute_unablatedVolumeDynCell = .true. + config_calving_error_threshold = 100000.0 + + config_thermal_solver = 'temperature' + config_thermal_calculate_bmb = .true. + config_temperature_init = 'file' + config_thermal_thickness = 0.0 + config_surface_air_temperature_source = 'file' + config_basal_heat_flux_source = 'file' + + config_basal_mass_bal_float = 'ismip6' + config_front_mass_bal_grounded = 'ismip6' + config_use_3d_thermal_forcing_for_face_melt = .true. + config_add_ocean_thermal_forcing = 0.0 + + config_ice_density = 910.0 + config_ocean_density = 1028.0 + config_sea_level = 0.0 + config_flowLawExponent = 3.0 + config_dynamic_thickness = 10.0 + + config_dt = '0000-01-00_00:00:00' + config_time_integration = 'forward_euler' + config_adaptive_timestep = .true. + config_adaptive_timestep_calvingCFL_fraction = 0.8 + config_adaptive_timestep_include_calving = .true. + config_min_adaptive_timestep = 60 + config_max_adaptive_timestep = 3.154e7 + config_adaptive_timestep_CFL_fraction = 0.8 + config_adaptive_timestep_include_DCFL = .false. + config_adaptive_timestep_force_interval = '0000-01-00_00:00:00' + config_timeaveraging_interval = '0001-00-00_00:00:00' + config_enable_timeAvgRestarts = .false. + + config_do_restart = .true. + config_restart_timestamp_name = 'restart_timestamp' + config_start_time = 'file' + config_stop_time = '2301-01-01_00:00:00' + config_calendar_type = 'gregorian' + + config_stats_interval = 0 + config_write_stats_on_startup = .false. + config_stats_cell_ID = 1 + config_write_output_on_startup = .true. + + config_always_compute_fem_grid = .true. + + config_ocean_connection_N = .false. + config_SGH = .false. + + config_AM_globalStats_enable = .true. + config_AM_globalStats_compute_interval = 'output_interval' + config_AM_globalStats_stream_name = 'globalStatsOutput' + config_AM_globalStats_compute_on_startup = .true. + config_AM_globalStats_write_on_startup = .true. + + config_AM_regionalStats_enable = .true. + config_AM_regionalStats_compute_interval = 'output_interval' + config_AM_regionalStats_stream_name = 'regionalStatsOutput' + config_AM_regionalStats_compute_on_startup = .true. + config_AM_regionalStats_write_on_startup = .true. diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/namelist.sealevel.template b/compass/landice/tests/ismip7_run/ismip7_ais/namelist.sealevel.template new file mode 100644 index 0000000000..e92a72ecab --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/namelist.sealevel.template @@ -0,0 +1,68 @@ +&time_config + itersl = 1 + starttime = 2000 + dt1 = 5 + +/ +&model_resolution + norder = 512 + nglv = {{ nglv }} + +/ +&io_directory + inputfolder_ice = '{{ slm_input_ice }}' + inputfolder = '{{ slm_input_others }}' + planetfolder = '{{ slm_input_earth }}' + gridfolder = '{{ slm_input_others }}' + outputfolder = 'OUTPUT_SLM/' + outputfolder_ice = 'ICELOAD_SLM/' + folder_coupled = '' + +/ +&file_format + ext ='' + fType_in = 'text' + fType_out = 'both' + +/ +&file_name + planetmodel = '{{ slm_earth_structure }}' + icemodel = 'iceGlobalDomain_zeroField_GL{{ nglv }}_' + icemodel_out = 'iceload_out_' + timearray = 'times' + topomodel = 'etopo2_nglv{{ nglv }}_outside_AIS' + topo_initial = 'etopo2_nglv{{ nglv }}_outside_AIS' + grid_lat = 'GLlat_{{ nglv }}.txt' + grid_lon = 'GLlon_{{ nglv }}.txt' + +/ +&model_config + checkmarine = .false. + tpw = .true. + calcRG = .true. + input_times = .false. + initial_topo = .true. + iceVolume = .true. + coupling = .true. + patch_ice = .true. + +/ +&timewindow_config + L_sim = 300 + dt1 = 5 + dt2 = 10 + dt3 = 10 + dt4 = 10 + Ldt1 = 300 + Ldt2 = 0 + Ldt3 = 0 + Ldt4 = 0 + +/ +&others + whichplanet = 'earth' + + +/ + +! end of the namelist file diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/set_up_experiment.py b/compass/landice/tests/ismip7_run/ismip7_ais/set_up_experiment.py new file mode 100644 index 0000000000..98d59d4c40 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/set_up_experiment.py @@ -0,0 +1,402 @@ +import glob +import os +import sys +from importlib import resources + +from jinja2 import Template + +from compass.job import write_job_script +from compass.load_script import symlink_load_script +from compass.model import make_graph_file, run_model +from compass.step import Step + + +class SetUpExperiment(Step): + """ + A step for setting up an ISMIP7 AIS experiment + """ + + def __init__(self, test_case, name, subdir, exp, exp_info): + """ + Set up a new experiment + + Parameters + ---------- + test_case : compass.testcase.TestCase + The test case this step belongs to + + name : str + The name of this step (same as the experiment name) + + subdir : str + Subdirectory for this step + + exp : str + Experiment identifier (e.g., 'ssp585_CESM2-WACCM') + + exp_info : dict + Dictionary with experiment metadata: + scenario, model, start_time, stop_time, is_historical + """ + self.exp = exp + self.exp_info = exp_info + + super().__init__(test_case=test_case, name=name, subdir=subdir) + + def setup(self): # noqa: C901 + """ + Set up the experiment directory with all needed files. + """ + print(f" Setting up experiment {self.exp}") + + config = self.config + section = config['ismip7_run_ais'] + self.ntasks = section.getint('ntasks') + self.min_tasks = self.ntasks + forcing_basepath = section.get('forcing_basepath') + init_cond_path = section.get('init_cond_path') + init_cond_fname = os.path.split(init_cond_path)[-1] + melt_params_path = section.get('melt_params_path') + melt_params_fname = os.path.split(melt_params_path)[-1] + region_mask_path = section.get('region_mask_path') + region_mask_fname = os.path.split(region_mask_path)[-1] + reference_surface_path = section.get('reference_surface_path') + reference_surface_fname = os.path.split(reference_surface_path)[-1] + calving_method = section.get('calving_method') + sea_level_model = section.getboolean('sea_level_model') + + exp_info = self.exp_info + scenario = exp_info['scenario'] + model = exp_info['model'] + is_historical = exp_info['is_historical'] + start_time = exp_info['start_time'] + stop_time = exp_info['stop_time'] + + # Define where to get templates (in current package) + resource_location = 'compass.landice.tests.ismip7_run.ismip7_ais' + + # Define calving method + use_vM_calving = (calving_method == 'von_mises') + + # --- Determine forcing file paths --- + if scenario == 'ocx': + ocx_forcing_path = section.get('ocx_forcing_path') + forcing_dir = ocx_forcing_path + elif scenario == 'ctrl': + # Control run uses climatology forcing + forcing_dir = None # handled separately below + else: + # Standard ESM-driven experiment + forcing_dir = os.path.join(forcing_basepath, + f"{model}_{scenario}") + + # --- Symlink input files --- + if is_historical: + os.symlink(init_cond_path, + os.path.join(self.work_dir, + os.path.basename(init_cond_path))) + os.symlink(melt_params_path, + os.path.join(self.work_dir, + os.path.basename(melt_params_path))) + os.symlink(region_mask_path, + os.path.join(self.work_dir, + os.path.basename(region_mask_path))) + os.symlink(reference_surface_path, + os.path.join(self.work_dir, + os.path.basename(reference_surface_path))) + + # --- Find and symlink forcing files --- + if scenario == 'ctrl': + # Control run: use climatology files + ctrl_tf_path = section.get('ctrl_tf_climatology_path') + ctrl_atm_path = section.get('ctrl_atm_climatology_path') + tf_fname = os.path.split(ctrl_tf_path)[-1] + os.symlink(ctrl_tf_path, + os.path.join(self.work_dir, tf_fname)) + + # Find atmosphere climatology files + smb_files = glob.glob(os.path.join(ctrl_atm_path, '*SMB*.nc')) + smb_files = [f for f in smb_files if 'gradient' not in f] + if len(smb_files) == 1: + smb_fname = os.path.split(smb_files[0])[-1] + os.symlink(smb_files[0], + os.path.join(self.work_dir, smb_fname)) + else: + sys.exit(f"ERROR: Expected 1 SMB climatology file in " + f"{ctrl_atm_path}, found {len(smb_files)}") + + temp_files = glob.glob( + os.path.join(ctrl_atm_path, '*temperature*.nc')) + temp_files = [f for f in temp_files if 'gradient' not in f] + if len(temp_files) == 1: + temp_fname = os.path.split(temp_files[0])[-1] + os.symlink(temp_files[0], + os.path.join(self.work_dir, temp_fname)) + else: + sys.exit(f"ERROR: Expected 1 temperature climatology file in " + f"{ctrl_atm_path}, found {len(temp_files)}") + + runoff_files = glob.glob( + os.path.join(ctrl_atm_path, '*runoff*.nc')) + if len(runoff_files) == 1: + runoff_fname = os.path.split(runoff_files[0])[-1] + os.symlink(runoff_files[0], + os.path.join(self.work_dir, runoff_fname)) + else: + runoff_fname = '' # runoff may not exist for ctrl + + smb_grad_files = glob.glob( + os.path.join(ctrl_atm_path, '*SMB_gradient*.nc')) + smb_grad_fname = '' + if len(smb_grad_files) == 1: + smb_grad_fname = os.path.split(smb_grad_files[0])[-1] + os.symlink(smb_grad_files[0], + os.path.join(self.work_dir, smb_grad_fname)) + + temp_grad_files = glob.glob( + os.path.join(ctrl_atm_path, '*temperature_gradient*.nc')) + temp_grad_fname = '' + if len(temp_grad_files) == 1: + temp_grad_fname = os.path.split(temp_grad_files[0])[-1] + os.symlink(temp_grad_files[0], + os.path.join(self.work_dir, temp_grad_fname)) + + else: + # Standard or OCX experiment: find forcing in forcing_dir + atm_dir = os.path.join(forcing_dir, 'atmosphere') + ocean_dir = os.path.join(forcing_dir, 'ocean_thermal_forcing') + + # SMB forcing + smb_search = os.path.join(atm_dir, '*SMB_*.nc') + smb_list = glob.glob(smb_search) + smb_list = [f for f in smb_list if 'gradient' not in f] + if len(smb_list) == 1: + smb_fname = os.path.split(smb_list[0])[-1] + os.symlink(smb_list[0], + os.path.join(self.work_dir, smb_fname)) + else: + sys.exit(f"ERROR: Expected 1 SMB file at {smb_search}, " + f"found {len(smb_list)}: {smb_list}") + + # Temperature forcing + temp_search = os.path.join(atm_dir, '*temperature_*.nc') + temp_list = glob.glob(temp_search) + temp_list = [f for f in temp_list if 'gradient' not in f] + if len(temp_list) == 1: + temp_fname = os.path.split(temp_list[0])[-1] + os.symlink(temp_list[0], + os.path.join(self.work_dir, temp_fname)) + else: + sys.exit(f"ERROR: Expected 1 temperature file at " + f"{temp_search}, found {len(temp_list)}") + + # Runoff forcing (optional — may not exist for all experiments) + runoff_search = os.path.join(atm_dir, '*runoff_*.nc') + runoff_list = glob.glob(runoff_search) + runoff_fname = '' + if len(runoff_list) == 1: + runoff_fname = os.path.split(runoff_list[0])[-1] + os.symlink(runoff_list[0], + os.path.join(self.work_dir, runoff_fname)) + + # SMB gradient (lapse rate) + smb_grad_search = os.path.join(atm_dir, '*SMB_gradient_*.nc') + smb_grad_list = glob.glob(smb_grad_search) + smb_grad_fname = '' + if len(smb_grad_list) == 1: + smb_grad_fname = os.path.split(smb_grad_list[0])[-1] + os.symlink(smb_grad_list[0], + os.path.join(self.work_dir, smb_grad_fname)) + + # Temperature gradient (lapse rate) + temp_grad_search = os.path.join(atm_dir, + '*temperature_gradient_*.nc') + temp_grad_list = glob.glob(temp_grad_search) + temp_grad_fname = '' + if len(temp_grad_list) == 1: + temp_grad_fname = os.path.split(temp_grad_list[0])[-1] + os.symlink(temp_grad_list[0], + os.path.join(self.work_dir, temp_grad_fname)) + + # Thermal forcing + tf_search = os.path.join(ocean_dir, '*thermal_forcing_*.nc') + tf_list = glob.glob(tf_search) + if len(tf_list) == 1: + tf_fname = os.path.split(tf_list[0])[-1] + os.symlink(tf_list[0], + os.path.join(self.work_dir, tf_fname)) + else: + sys.exit(f"ERROR: Expected 1 TF file at {tf_search}, " + f"found {len(tf_list)}: {tf_list}") + + # --- Set up streams --- + # Determine forcing interval + if scenario == 'ctrl': + forcing_interval_monthly = 'initial_only' + forcing_interval_annual = 'initial_only' + elif is_historical: + # Historical: read forcing at each interval + forcing_interval_monthly = '0000-01-00_00:00:00' + forcing_interval_annual = '0001-00-00_00:00:00' + else: + # Projections + forcing_interval_monthly = '0000-01-00_00:00:00' + forcing_interval_annual = '0001-00-00_00:00:00' + + stream_replacements = { + 'input_file_init_cond': init_cond_fname if is_historical + else 'USE_RESTART_FILE_INSTEAD', + 'input_file_region_mask': region_mask_fname if is_historical + else 'USE_RESTART_FILE_INSTEAD', + 'input_file_melt_params': melt_params_fname, + 'input_file_reference_surface': reference_surface_fname, + 'input_file_SMB_forcing': smb_fname, + 'input_file_temperature_forcing': temp_fname, + 'input_file_TF_forcing': tf_fname, + 'input_file_runoff_forcing': runoff_fname, + 'input_file_smb_gradient_forcing': smb_grad_fname, + 'input_file_temperature_gradient_forcing': temp_grad_fname, + 'forcing_interval_monthly': forcing_interval_monthly, + 'forcing_interval_annual': forcing_interval_annual, + } + + self.add_streams_file( + resource_location, + 'streams.landice.template', + out_name='streams.landice', + template_replacements=stream_replacements) + + # --- Set up namelist --- + self.add_namelist_file( + resource_location, 'namelist.landice', + out_name='namelist.landice') + + # PIO options + pio_stride = section.getint('pio_stride') + io_tasks = self.ntasks // pio_stride + options = {'config_pio_stride': f'{pio_stride}', + 'config_pio_num_iotasks': f'{io_tasks}'} + self.add_namelist_options(options=options, + out_name='namelist.landice') + + # Historical-specific options + if is_historical: + options = {'config_do_restart': ".false.", + 'config_start_time': f"'{start_time}'", + 'config_stop_time': f"'{stop_time}'"} + self.add_namelist_options(options=options, + out_name='namelist.landice') + else: + options = {'config_stop_time': f"'{stop_time}'"} + self.add_namelist_options(options=options, + out_name='namelist.landice') + + # Calving options + if use_vM_calving: + vM_path = section.get('von_mises_parameter_path') + options = { + 'config_calving': "'von_Mises_stress'", + 'config_restore_calving_front': ".false.", + 'config_floating_von_Mises_threshold_stress_source': "'data'", + 'config_grounded_von_Mises_threshold_stress_source': "'data'"} + self.add_namelist_options(options=options, + out_name='namelist.landice') + vM_stream_replacements = {'input_file_VM_params': vM_path} + self.add_streams_file( + resource_location, 'streams.vM_params', + out_name='streams.landice', + template_replacements=vM_stream_replacements) + + # Sea-level model options + if sea_level_model: + slm_input_ice = section.get('slm_input_ice') + slm_input_earth = section.get('slm_input_earth') + slm_earth_structure = section.get('slm_earth_structure') + slm_input_others = section.get('slm_input_others') + nglv = section.getint('nglv') + + slm_input_ice = os.path.join(slm_input_ice, + f'GL{nglv}/ice_noGrIS_GL{nglv}/') + slm_input_others = os.path.join(slm_input_others, + f'GL{nglv}/') + + options = {'config_uplift_method': "'sealevelmodel'"} + self.add_namelist_options(options=options, + out_name='namelist.landice') + + template = Template(resources.read_text( + resource_location, 'namelist.sealevel.template')) + text = template.render( + nglv=int(nglv), slm_input_ice=slm_input_ice, + slm_input_earth=slm_input_earth, + slm_earth_structure=slm_earth_structure, + slm_input_others=slm_input_others) + + file_slm_nl = os.path.join(self.work_dir, 'namelist.sealevel') + with open(file_slm_nl, 'w') as handle: + handle.write(text) + + os.makedirs(os.path.join(self.work_dir, 'OUTPUT_SLM/'), + exist_ok=True) + os.makedirs(os.path.join(self.work_dir, 'ICELOAD_SLM/'), + exist_ok=True) + + map_dir = os.path.join('..', 'mapping_files') + for map_file in ('mapfile_mali_to_slm.nc', + 'mapfile_slm_to_mali.nc'): + os.symlink(os.path.join(map_dir, map_file), + os.path.join(self.work_dir, map_file)) + + # --- Symlink restart for projections/ctrl --- + if not is_historical: + hist_exp = f"historical_{model}" + os.symlink(f"../{hist_exp}/rst.2015-01-01.nc", + os.path.join(self.work_dir, 'rst.2015-01-01.nc')) + with open(os.path.join(self.work_dir, "restart_timestamp"), + "w") as text_file: + text_file.write("2015-01-01_00:00:00") + + # --- Add albany yaml, graph file, load script, job script --- + self.add_input_file( + filename='albany_input.yaml', + package=resource_location, + copy=True) + + make_graph_file(mesh_filename=init_cond_path, + graph_filename=os.path.join(self.work_dir, + 'graph.info')) + + symlink_load_script(self.work_dir) + + self.config.set('job', 'job_name', self.exp) + machine = self.config.get('deploy', 'machine') + pre_run_cmd = ('LOGDIR=previous_logs_`date +"%Y-%m-%d_%H-%M-%S"`;' + 'mkdir $LOGDIR; cp log* $LOGDIR; date') + post_run_cmd = "date" + write_job_script(self.config, machine, + target_cores=self.ntasks, min_cores=self.min_tasks, + work_dir=self.work_dir, + pre_run_commands=pre_run_cmd, + post_run_commands=post_run_cmd) + + self.add_model_as_input() + + def run(self): + """ + Run this step of the test case + """ + config = self.config + section = config['ismip7_run_ais'] + sea_level_model = section.getboolean('sea_level_model') + if sea_level_model: + map_dir = os.path.join('..', 'mapping_files') + for map_file in ('mapfile_mali_to_slm.nc', + 'mapfile_slm_to_mali.nc'): + if not os.path.isfile(os.path.join(map_dir, map_file)): + sys.exit(f"ERROR: 'mapping_files/{map_file}' " + "does not exist in workdir. " + "Please run the 'mapping_files' step " + "before proceeding.") + + run_model(step=self, namelist='namelist.landice', + streams='streams.landice') diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/streams.landice.template b/compass/landice/tests/ismip7_run/ismip7_ais/streams.landice.template new file mode 100644 index 0000000000..5465926fb8 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/streams.landice.template @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compass/landice/tests/ismip7_run/ismip7_ais/streams.vM_params b/compass/landice/tests/ismip7_run/ismip7_ais/streams.vM_params new file mode 100644 index 0000000000..d9ae1ca3f6 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_ais/streams.vM_params @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/compass/landice/tests/ismip7_run/ismip7_gris/__init__.py b/compass/landice/tests/ismip7_run/ismip7_gris/__init__.py new file mode 100644 index 0000000000..ab883d15e2 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_gris/__init__.py @@ -0,0 +1,134 @@ +import os + +from compass.landice.tests.ismip7_run.ismip7_gris.set_up_experiment import ( + SetUpExperiment, +) +from compass.testcase import TestCase + +# Define the full experiment matrix for GrIS +EXPERIMENTS = { + 'historical_CESM2-WACCM': { + 'scenario': 'historical', 'model': 'CESM2-WACCM', + 'start_time': '2007-01-01_00:00:00', + 'stop_time': '2015-01-01_00:00:00', + 'is_historical': True}, + 'historical_MRI-ESM2-0': { + 'scenario': 'historical', 'model': 'MRI-ESM2-0', + 'start_time': '2007-01-01_00:00:00', + 'stop_time': '2015-01-01_00:00:00', + 'is_historical': True}, + 'ssp370_CESM2-WACCM': { + 'scenario': 'ssp370', 'model': 'CESM2-WACCM', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2101-01-01_00:00:00', + 'is_historical': False}, + 'ssp370_MRI-ESM2-0': { + 'scenario': 'ssp370', 'model': 'MRI-ESM2-0', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2101-01-01_00:00:00', + 'is_historical': False}, + 'ssp126_CESM2-WACCM': { + 'scenario': 'ssp126', 'model': 'CESM2-WACCM', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ssp126_MRI-ESM2-0': { + 'scenario': 'ssp126', 'model': 'MRI-ESM2-0', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ssp585_CESM2-WACCM': { + 'scenario': 'ssp585', 'model': 'CESM2-WACCM', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ssp585_MRI-ESM2-0': { + 'scenario': 'ssp585', 'model': 'MRI-ESM2-0', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ctrl_CESM2-WACCM': { + 'scenario': 'ctrl', 'model': 'CESM2-WACCM', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ctrl_MRI-ESM2-0': { + 'scenario': 'ctrl', 'model': 'MRI-ESM2-0', + 'start_time': '2015-01-01_00:00:00', + 'stop_time': '2301-01-01_00:00:00', + 'is_historical': False}, + 'ocx': { + 'scenario': 'ocx', 'model': None, + 'start_time': '1990-01-01_00:00:00', + 'stop_time': '2026-01-01_00:00:00', + 'is_historical': True}, +} + + +class Ismip7Gris(TestCase): + """ + A test case for automated setup of a suite of standardized + ISMIP7 simulations for the Greenland Ice Sheet. + """ + + def __init__(self, test_group): + """ + Create the test case + + Parameters + ---------- + test_group : compass.landice.tests.ismip7_run.Ismip7Run + The test group that this test case belongs to + """ + name = 'ismip7_gris' + super().__init__(test_group=test_group, name=name, subdir=name) + + def configure(self): + """ + Set up the desired ISMIP7 GrIS experiments. + """ + config = self.config + exp_list_str = config.get('ismip7_run_gris', 'exp_list') + + if exp_list_str == 'all': + exp_list = list(EXPERIMENTS.keys()) + elif exp_list_str == 'historical': + exp_list = [k for k, v in EXPERIMENTS.items() + if v['is_historical']] + elif exp_list_str == 'projections': + exp_list = [k for k, v in EXPERIMENTS.items() + if not v['is_historical'] and + v['scenario'] != 'ctrl'] + elif exp_list_str == 'ctrl': + exp_list = [k for k, v in EXPERIMENTS.items() + if v['scenario'] == 'ctrl'] + else: + exp_list = [s.strip() for s in exp_list_str.split(',')] + + for exp in exp_list: + if exp not in EXPERIMENTS: + raise ValueError( + f"Unknown experiment '{exp}'. Valid experiments: " + f"{list(EXPERIMENTS.keys())}") + if os.path.exists(os.path.join(self.work_dir, exp)): + print(f"WARNING: {exp} path already exists; skipping. " + "Remove the directory " + f"{os.path.join(self.work_dir, exp)} and run " + "'compass setup' again to recreate.") + else: + self.add_step( + SetUpExperiment(test_case=self, name=exp, + subdir=exp, exp=exp, + exp_info=EXPERIMENTS[exp])) + + # Do not add experiments to steps_to_run + self.steps_to_run = [] + + def run(self): + """ + A dummy run method + """ + raise ValueError( + "ERROR: 'compass run' has no functionality at the test case " + "level for this test. Please submit the job script in each " + "experiment's subdirectory manually instead.") diff --git a/compass/landice/tests/ismip7_run/ismip7_gris/albany_input.yaml b/compass/landice/tests/ismip7_run/ismip7_gris/albany_input.yaml new file mode 100644 index 0000000000..bdd3fa7108 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_gris/albany_input.yaml @@ -0,0 +1,238 @@ +%YAML 1.1 +--- +ANONYMOUS: + Problem: + Depth Integrated Model: true + LandIce Field Norm: + sliding_velocity_basalside: + Regularization Type: Given Value + Regularization Value: 1.0e-4 + LandIce BCs: + BC 0: + Basal Friction Coefficient: + Type: Power Law + Power Exponent: 1.0 + Mu Type: Field + Effective Pressure Type: Hydrostatic Computed At Nodes + Zero Effective Pressure On Floating Ice At Nodes: true + Zero Beta On Floating Ice: false + Use Pressurized Bed Above Sea Level: false + +# Discretization Description + Discretization: + #Exodus Output File Name: albany_output.exo + + Piro: +# Nonlinear Solver Information + NOX: + Nonlinear Solver: Line Search Based + Line Search: + Full Step: + Full Step: 1.0e+00 + Method: Backtrack + Solver Options: + Status Test Check Type: Minimal + Status Tests: + Test Type: Combo + Combo Type: OR + Number of Tests: 2 + Test 0: + Test Type: NormF + Norm Type: Two Norm + Scale Type: Scaled + Tolerance: 1.0e-05 + Test 1: + Test Type: MaxIters + Maximum Iterations: 50 + Printing: + Output Precision: 3 + Output Processor: 0 + Output Information: + Error: true + Warning: true + Outer Iteration: true + Parameters: false + Details: false + Linear Solver Details: false + Stepper Iteration: true + Stepper Details: true + Stepper Parameters: true + + Direction: + Method: Newton + Newton: + Forcing Term Method: Constant + Rescue Bad Newton Solve: true + Linear Solver: + Write Linear System: false + Tolerance: 1.0e-8 + + Stratimikos Linear Solver: + Stratimikos: + +# Linear Solver Information + Linear Solver Type: Belos + Linear Solver Types: + Belos: + Solver Type: Block GMRES + Solver Types: + Block GMRES: + Output Frequency: 20 + Output Style: 1 + Verbosity: 33 + Maximum Iterations: 200 + Block Size: 1 + Num Blocks: 200 + Flexible Gmres: false + VerboseObject: + Output File: none + Verbosity Level: low + +# Preconditioner Information + Preconditioner Type: MueLu + Preconditioner Types: + + Ifpack2: + Overlap: 1 + Prec Type: ILUT + + MueLu: + Matrix: + PDE equations: 2 + Factories: + myLineDetectionFact: + factory: LineDetectionFactory + 'linedetection: orientation': coordinates + mySemiCoarsenPFact1: + factory: SemiCoarsenPFactory + 'semicoarsen: coarsen rate': 14 + UncoupledAggregationFact2: + factory: UncoupledAggregationFactory + 'aggregation: ordering': graph + 'aggregation: max selected neighbors': 0 + 'aggregation: min agg size': 3 + 'aggregation: phase3 avoid singletons': true + MyCoarseMap2: + factory: CoarseMapFactory + Aggregates: UncoupledAggregationFact2 + myTentativePFact2: + 'tentative: calculate qr': true + factory: TentativePFactory + Aggregates: UncoupledAggregationFact2 + CoarseMap: MyCoarseMap2 + mySaPFact2: + 'sa: eigenvalue estimate num iterations': 10 + 'sa: damping factor': 1.33333e+00 + factory: SaPFactory + P: myTentativePFact2 + myTransferCoordinatesFact: + factory: CoordinatesTransferFactory + CoarseMap: MyCoarseMap2 + Aggregates: UncoupledAggregationFact2 + myTogglePFact: + factory: TogglePFactory + 'semicoarsen: number of levels': 2 + TransferFactories: + P1: mySemiCoarsenPFact1 + P2: mySaPFact2 + Ptent1: mySemiCoarsenPFact1 + Ptent2: myTentativePFact2 + Nullspace1: mySemiCoarsenPFact1 + Nullspace2: myTentativePFact2 + myRestrictorFact: + factory: TransPFactory + P: myTogglePFact + myToggleTransferCoordinatesFact: + factory: ToggleCoordinatesTransferFactory + Chosen P: myTogglePFact + TransferFactories: + Coordinates1: mySemiCoarsenPFact1 + Coordinates2: myTransferCoordinatesFact + myRAPFact: + factory: RAPFactory + P: myTogglePFact + R: myRestrictorFact + TransferFactories: + For Coordinates: myToggleTransferCoordinatesFact + myRepartitionHeuristicFact: + factory: RepartitionHeuristicFactory + A: myRAPFact + 'repartition: min rows per proc': 3000 + 'repartition: max imbalance': 1.327e+00 + 'repartition: start level': 1 + myZoltanInterface: + factory: ZoltanInterface + A: myRAPFact + Coordinates: myToggleTransferCoordinatesFact + number of partitions: myRepartitionHeuristicFact + myRepartitionFact: + factory: RepartitionFactory + A: myRAPFact + Partition: myZoltanInterface + 'repartition: remap parts': true + number of partitions: myRepartitionHeuristicFact + myRebalanceProlongatorFact: + factory: RebalanceTransferFactory + type: Interpolation + P: myTogglePFact + Coordinates: myToggleTransferCoordinatesFact + Nullspace: myTogglePFact + myRebalanceRestrictionFact: + factory: RebalanceTransferFactory + type: Restriction + R: myRestrictorFact + myRebalanceAFact: + factory: RebalanceAcFactory + A: myRAPFact + TransferFactories: { } + mySmoother1: + factory: TrilinosSmoother + type: LINESMOOTHING_BANDEDRELAXATION + 'smoother: pre or post': both + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 1 + 'relaxation: damping factor': 1.0 + mySmoother3: + factory: TrilinosSmoother + type: RELAXATION + 'smoother: pre or post': both + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 1 + 'relaxation: damping factor': 1.0 + mySmoother4: + factory: TrilinosSmoother + type: RELAXATION + 'smoother: pre or post': pre + ParameterList: + 'relaxation: type': Gauss-Seidel + 'relaxation: sweeps': 4 + 'relaxation: damping factor': 1.0 + Hierarchy: + max levels: 7 + 'coarse: max size': 2000 + verbosity: None + Finest: + Smoother: mySmoother1 + CoarseSolver: mySmoother4 + P: myRebalanceProlongatorFact + Nullspace: myRebalanceProlongatorFact + CoarseNumZLayers: myLineDetectionFact + LineDetection_Layers: myLineDetectionFact + LineDetection_VertLineIds: myLineDetectionFact + A: myRebalanceAFact + Coordinates: myRebalanceProlongatorFact + Importer: myRepartitionFact + All: + startLevel: 1 + Smoother: mySmoother4 + CoarseSolver: mySmoother4 + P: myRebalanceProlongatorFact + Nullspace: myRebalanceProlongatorFact + CoarseNumZLayers: myLineDetectionFact + LineDetection_Layers: myLineDetectionFact + LineDetection_VertLineIds: myLineDetectionFact + A: myRebalanceAFact + Coordinates: myRebalanceProlongatorFact + Importer: myRepartitionFact diff --git a/compass/landice/tests/ismip7_run/ismip7_gris/ismip7_gris.cfg b/compass/landice/tests/ismip7_run/ismip7_gris/ismip7_gris.cfg new file mode 100644 index 0000000000..a3b6a6229a --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_gris/ismip7_gris.cfg @@ -0,0 +1,49 @@ +[ismip7_run_gris] + +# List of experiments to set up. +# Can be "all", "historical", "projections", "ctrl", or a +# comma-delimited list of experiment names. +exp_list = all + +# Number of tasks to use for each run +ntasks = 128 + +# Value to use for config_pio_stride. +pio_stride = 128 + +# Base path to the pre-processed ISMIP7 forcing files. +# Expected layout: +# {forcing_basepath}/{model}_{scenario}/atmosphere/ +# {forcing_basepath}/{model}_{scenario}/ocean_thermal_forcing/ +# User has to supply. +forcing_basepath = NotAvailable + +# Path to the initial condition file. User has to supply. +init_cond_path = NotAvailable + +# Path to the file for the basal melt parametrization coefficients. +melt_params_path = NotAvailable + +# Path to the reference surface elevation file +reference_surface_path = NotAvailable + +# Path to the region mask file +region_mask_path = NotAvailable + +# Path to the ocean thermal forcing climatology file for CTRL2015 runs. +ctrl_tf_climatology_path = NotAvailable + +# Path to the atmosphere climatology files for CTRL2015 runs. +ctrl_atm_climatology_path = NotAvailable + +# Path to OCX forcing. +ocx_forcing_path = NotAvailable + +# Calving method to use. Options: restore, von_mises +calving_method = restore + +# Path to the von Mises parameter fields. +von_mises_parameter_path = NotAvailable + +[parallel] +parallel_executable = srun --label --cpu-bind=cores diff --git a/compass/landice/tests/ismip7_run/ismip7_gris/ismip7_gris_test.cfg b/compass/landice/tests/ismip7_run/ismip7_gris/ismip7_gris_test.cfg new file mode 100644 index 0000000000..9287dc1a1d --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_gris/ismip7_gris_test.cfg @@ -0,0 +1,49 @@ +[ismip7_run_gris] + +# List of experiments to set up. +# Can be "all", "historical", "projections", "ctrl", or a +# comma-delimited list of experiment names. +exp_list = historical_CESM2-WACCM,ssp585_CESM2-WACCM + +# Number of tasks to use for each run +ntasks = 512 + +# Value to use for config_pio_stride. +pio_stride = 128 + +# Base path to the pre-processed ISMIP7 forcing files. +# Expected layout: +# {forcing_basepath}/{model}_{scenario}/atmosphere/ +# {forcing_basepath}/{model}_{scenario}/ocean_thermal_forcing/ +# User has to supply. +forcing_basepath = /global/cfs/cdirs/m4288/users/trhille/ISMIP7/test_processing/GIS + +# Path to the initial condition file. User has to supply. +init_cond_path = /global/cfs/cdirs/fanssie/MALI_input_files/GIS_1to10km_r02/GIS_1to10km_r02_20230202_remove_icebergs.nc + +# Path to the file for the basal melt parametrization coefficients. +melt_params_path = NotAvailable + +# Path to the reference surface elevation file +reference_surface_path = /global/cfs/cdirs/m4288/users/trhille/ISMIP7/forcing/GIS/upper_surface.nc + +# Path to the region mask file +region_mask_path = /global/cfs/cdirs/fanssie/MALI_input_files/GIS_1to10km_r02/GIS_1to10km_r02_20230202_ismip6_regionMasks.nc + +# Path to the ocean thermal forcing climatology file for CTRL2015 runs. +ctrl_tf_climatology_path = NotAvailable + +# Path to the atmosphere climatology files for CTRL2015 runs. +ctrl_atm_climatology_path = NotAvailable + +# Path to OCX forcing. +ocx_forcing_path = NotAvailable + +# Calving method to use. Options: restore, von_mises +calving_method = restore + +# Path to the von Mises parameter fields. +von_mises_parameter_path = NotAvailable + +[parallel] +parallel_executable = srun --label --cpu-bind=cores diff --git a/compass/landice/tests/ismip7_run/ismip7_gris/namelist.landice b/compass/landice/tests/ismip7_run/ismip7_gris/namelist.landice new file mode 100644 index 0000000000..d29250f7b6 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_gris/namelist.landice @@ -0,0 +1,91 @@ + config_velocity_solver = 'FO' + config_do_velocity_reconstruction_for_external_dycore = .false. + config_unrealistic_velocity = 00.00159 + config_nonconvergence_error = .false. + config_flowParamA_calculation = 'PB1982' + + config_thickness_advection = 'fct' + config_tracer_advection = 'fct' + config_horiz_tracer_adv_order = 3 + config_advection_coef_3rd_order = 1.0 + config_zero_sfcMassBalApplied_over_bare_land = .true. + config_apply_smb_sat_lapse_rate = .true. + + config_uplift_method = 'none' + + config_calving = 'crevasse_depth' + config_apply_calving_mask = .false. + config_restore_calving_front_prevent_retreat = .false. + config_calculate_damage = .false. + config_damage_calving_threshold = 0.95 + config_damage_calving_method = 'none' + config_calving_speed_limit = 0.00063492063 + config_restore_calving_front = .false. + config_remove_icebergs = .true. + config_remove_small_islands = .true. + config_distribute_unablatedVolumeDynCell = .true. + config_calving_error_threshold = 100000.0 + config_apply_facemelt_strainrate_enhancement = .true. + config_calving_strainrate_scaling = 1.0 + + + config_thermal_solver = 'temperature' + config_thermal_calculate_bmb = .true. + config_temperature_init = 'file' + config_thermal_thickness = 0.0 + config_surface_air_temperature_source = 'file' + config_basal_heat_flux_source = 'file' + + config_basal_mass_bal_float = 'none' + config_front_mass_bal_grounded = 'ismip6' + config_use_3d_thermal_forcing_for_face_melt = .false. + config_add_ocean_thermal_forcing = 0.0 + + config_ice_density = 910.0 + config_ocean_density = 1028.0 + config_sea_level = 0.0 + config_flowLawExponent = 3.0 + config_dynamic_thickness = 10.0 + + config_dt = '0000-01-00_00:00:00' + config_time_integration = 'runge_kutta' + config_rk_order = 2 + config_rk3_stages = 3 + config_adaptive_timestep = .true. + config_adaptive_timestep_calvingCFL_fraction = 0.8 + config_adaptive_timestep_include_calving = .true. + config_min_adaptive_timestep = 60 + config_max_adaptive_timestep = 3.154e7 + config_adaptive_timestep_CFL_fraction = 0.5 + config_adaptive_timestep_include_DCFL = .false. + config_adaptive_timestep_force_interval = '0000-01-00_00:00:00' + config_timeaveraging_interval = '0001-00-00_00:00:00' + config_enable_timeAvgRestarts = .false. + + config_do_restart = .true. + config_restart_timestamp_name = 'restart_timestamp' + config_start_time = 'file' + config_stop_time = '2301-01-01_00:00:00' + config_calendar_type = 'gregorian' + + config_stats_interval = 0 + config_write_stats_on_startup = .false. + config_stats_cell_ID = 1 + config_write_output_on_startup = .true. + + config_always_compute_fem_grid = .true. + + config_ocean_connection_N = .false. + config_SGH = .false. + + config_AM_globalStats_enable = .true. + config_AM_globalStats_compute_interval = 'output_interval' + config_AM_globalStats_stream_name = 'globalStatsOutput' + config_AM_globalStats_compute_on_startup = .true. + config_AM_globalStats_write_on_startup = .true. + + config_AM_regionalStats_enable = .true. + config_AM_regionalStats_compute_interval = 'output_interval' + config_AM_regionalStats_stream_name = 'regionalStatsOutput' + config_AM_regionalStats_compute_on_startup = .true. + config_AM_regionalStats_write_on_startup = .true. diff --git a/compass/landice/tests/ismip7_run/ismip7_gris/set_up_experiment.py b/compass/landice/tests/ismip7_run/ismip7_gris/set_up_experiment.py new file mode 100644 index 0000000000..4118ce44b5 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_gris/set_up_experiment.py @@ -0,0 +1,323 @@ +import glob +import os +import sys + +from compass.job import write_job_script +from compass.load_script import symlink_load_script +from compass.model import make_graph_file, run_model +from compass.step import Step + + +class SetUpExperiment(Step): + """ + A step for setting up an ISMIP7 GrIS experiment + """ + + def __init__(self, test_case, name, subdir, exp, exp_info): + """ + Set up a new experiment + + Parameters + ---------- + test_case : compass.testcase.TestCase + The test case this step belongs to + + name : str + The name of this step (same as the experiment name) + + subdir : str + Subdirectory for this step + + exp : str + Experiment identifier + + exp_info : dict + Dictionary with experiment metadata + """ + self.exp = exp + self.exp_info = exp_info + + super().__init__(test_case=test_case, name=name, subdir=subdir) + + def setup(self): # noqa: C901 + """ + Set up the experiment directory with all needed files. + """ + print(f" Setting up experiment {self.exp}") + + config = self.config + section = config['ismip7_run_gris'] + self.ntasks = section.getint('ntasks') + self.min_tasks = self.ntasks + forcing_basepath = section.get('forcing_basepath') + init_cond_path = section.get('init_cond_path') + init_cond_fname = os.path.split(init_cond_path)[-1] + melt_params_path = section.get('melt_params_path') + melt_params_fname = os.path.split(melt_params_path)[-1] + region_mask_path = section.get('region_mask_path') + region_mask_fname = os.path.split(region_mask_path)[-1] + reference_surface_path = section.get('reference_surface_path') + reference_surface_fname = os.path.split(reference_surface_path)[-1] + calving_method = section.get('calving_method') + + exp_info = self.exp_info + scenario = exp_info['scenario'] + model = exp_info['model'] + is_historical = exp_info['is_historical'] + start_time = exp_info['start_time'] + stop_time = exp_info['stop_time'] + + resource_location = 'compass.landice.tests.ismip7_run.ismip7_gris' + + use_vM_calving = (calving_method == 'von_mises') + + # --- Determine forcing file paths --- + if scenario == 'ocx': + ocx_forcing_path = section.get('ocx_forcing_path') + forcing_dir = ocx_forcing_path + elif scenario == 'ctrl': + forcing_dir = None + else: + forcing_dir = os.path.join(forcing_basepath, + f"{model}_{scenario}") + + # --- Symlink input files --- + if is_historical: + os.symlink(init_cond_path, + os.path.join(self.work_dir, + os.path.basename(init_cond_path))) + os.symlink(melt_params_path, + os.path.join(self.work_dir, + os.path.basename(melt_params_path))) + os.symlink(region_mask_path, + os.path.join(self.work_dir, + os.path.basename(region_mask_path))) + os.symlink(reference_surface_path, + os.path.join(self.work_dir, + os.path.basename(reference_surface_path))) + + # --- Find and symlink forcing files --- + if scenario == 'ctrl': + ctrl_tf_path = section.get('ctrl_tf_climatology_path') + ctrl_atm_path = section.get('ctrl_atm_climatology_path') + tf_fname = os.path.split(ctrl_tf_path)[-1] + os.symlink(ctrl_tf_path, + os.path.join(self.work_dir, tf_fname)) + + smb_files = glob.glob(os.path.join(ctrl_atm_path, '*SMB*.nc')) + smb_files = [f for f in smb_files if 'gradient' not in f] + if len(smb_files) == 1: + smb_fname = os.path.split(smb_files[0])[-1] + os.symlink(smb_files[0], + os.path.join(self.work_dir, smb_fname)) + else: + sys.exit(f"ERROR: Expected 1 SMB climatology file in " + f"{ctrl_atm_path}, found {len(smb_files)}") + + temp_files = glob.glob( + os.path.join(ctrl_atm_path, '*temperature*.nc')) + temp_files = [f for f in temp_files if 'gradient' not in f] + if len(temp_files) == 1: + temp_fname = os.path.split(temp_files[0])[-1] + os.symlink(temp_files[0], + os.path.join(self.work_dir, temp_fname)) + else: + sys.exit(f"ERROR: Expected 1 temperature climatology file in " + f"{ctrl_atm_path}, found {len(temp_files)}") + + runoff_files = glob.glob( + os.path.join(ctrl_atm_path, '*runoff*.nc')) + runoff_fname = '' + if len(runoff_files) == 1: + runoff_fname = os.path.split(runoff_files[0])[-1] + os.symlink(runoff_files[0], + os.path.join(self.work_dir, runoff_fname)) + + smb_grad_files = glob.glob( + os.path.join(ctrl_atm_path, '*SMB_gradient*.nc')) + smb_grad_fname = '' + if len(smb_grad_files) == 1: + smb_grad_fname = os.path.split(smb_grad_files[0])[-1] + os.symlink(smb_grad_files[0], + os.path.join(self.work_dir, smb_grad_fname)) + + temp_grad_files = glob.glob( + os.path.join(ctrl_atm_path, '*temperature_gradient*.nc')) + temp_grad_fname = '' + if len(temp_grad_files) == 1: + temp_grad_fname = os.path.split(temp_grad_files[0])[-1] + os.symlink(temp_grad_files[0], + os.path.join(self.work_dir, temp_grad_fname)) + + else: + atm_dir = os.path.join(forcing_dir, 'atmosphere') + ocean_dir = os.path.join(forcing_dir, 'ocean_thermal_forcing') + + smb_search = os.path.join(atm_dir, '*SMB_*.nc') + smb_list = glob.glob(smb_search) + smb_list = [f for f in smb_list if 'gradient' not in f] + if len(smb_list) == 1: + smb_fname = os.path.split(smb_list[0])[-1] + os.symlink(smb_list[0], + os.path.join(self.work_dir, smb_fname)) + else: + sys.exit(f"ERROR: Expected 1 SMB file at {smb_search}, " + f"found {len(smb_list)}") + + temp_search = os.path.join(atm_dir, '*temperature_*.nc') + temp_list = glob.glob(temp_search) + temp_list = [f for f in temp_list if 'gradient' not in f] + if len(temp_list) == 1: + temp_fname = os.path.split(temp_list[0])[-1] + os.symlink(temp_list[0], + os.path.join(self.work_dir, temp_fname)) + else: + sys.exit(f"ERROR: Expected 1 temperature file at " + f"{temp_search}, found {len(temp_list)}") + + runoff_search = os.path.join(atm_dir, '*runoff_*.nc') + runoff_list = glob.glob(runoff_search) + runoff_fname = '' + if len(runoff_list) == 1: + runoff_fname = os.path.split(runoff_list[0])[-1] + os.symlink(runoff_list[0], + os.path.join(self.work_dir, runoff_fname)) + + smb_grad_search = os.path.join(atm_dir, '*SMB_gradient_*.nc') + smb_grad_list = glob.glob(smb_grad_search) + smb_grad_fname = '' + if len(smb_grad_list) == 1: + smb_grad_fname = os.path.split(smb_grad_list[0])[-1] + os.symlink(smb_grad_list[0], + os.path.join(self.work_dir, smb_grad_fname)) + + temp_grad_search = os.path.join(atm_dir, + '*temperature_gradient_*.nc') + temp_grad_list = glob.glob(temp_grad_search) + temp_grad_fname = '' + if len(temp_grad_list) == 1: + temp_grad_fname = os.path.split(temp_grad_list[0])[-1] + os.symlink(temp_grad_list[0], + os.path.join(self.work_dir, temp_grad_fname)) + + # GrIS uses 2D thermal forcing + tf_search = os.path.join(ocean_dir, '*thermal_forcing_*.nc') + tf_list = glob.glob(tf_search) + if len(tf_list) == 1: + tf_fname = os.path.split(tf_list[0])[-1] + os.symlink(tf_list[0], + os.path.join(self.work_dir, tf_fname)) + else: + sys.exit(f"ERROR: Expected 1 TF file at {tf_search}, " + f"found {len(tf_list)}") + + # --- Set up streams --- + if scenario == 'ctrl': + forcing_interval_monthly = 'initial_only' + forcing_interval_annual = 'initial_only' + else: + forcing_interval_monthly = '0000-01-00_00:00:00' + forcing_interval_annual = '0001-00-00_00:00:00' + + stream_replacements = { + 'input_file_init_cond': init_cond_fname if is_historical + else 'USE_RESTART_FILE_INSTEAD', + 'input_file_region_mask': region_mask_fname if is_historical + else 'USE_RESTART_FILE_INSTEAD', + 'input_file_melt_params': melt_params_fname, + 'input_file_reference_surface': reference_surface_fname, + 'input_file_SMB_forcing': smb_fname, + 'input_file_temperature_forcing': temp_fname, + 'input_file_TF_forcing': tf_fname, + 'input_file_runoff_forcing': runoff_fname, + 'input_file_smb_gradient_forcing': smb_grad_fname, + 'input_file_temperature_gradient_forcing': temp_grad_fname, + 'forcing_interval_monthly': forcing_interval_monthly, + 'forcing_interval_annual': forcing_interval_annual, + } + + self.add_streams_file( + resource_location, + 'streams.landice.template', + out_name='streams.landice', + template_replacements=stream_replacements) + + # --- Set up namelist --- + self.add_namelist_file( + resource_location, 'namelist.landice', + out_name='namelist.landice') + + # PIO options + pio_stride = section.getint('pio_stride') + io_tasks = self.ntasks // pio_stride + options = {'config_pio_stride': f'{pio_stride}', + 'config_pio_num_iotasks': f'{io_tasks}'} + self.add_namelist_options(options=options, + out_name='namelist.landice') + + if is_historical: + options = {'config_do_restart': ".false.", + 'config_start_time': f"'{start_time}'", + 'config_stop_time': f"'{stop_time}'"} + self.add_namelist_options(options=options, + out_name='namelist.landice') + else: + options = {'config_stop_time': f"'{stop_time}'"} + self.add_namelist_options(options=options, + out_name='namelist.landice') + + if use_vM_calving: + vM_path = section.get('von_mises_parameter_path') + options = { + 'config_calving': "'von_Mises_stress'", + 'config_restore_calving_front': ".false.", + 'config_floating_von_Mises_threshold_stress_source': "'data'", + 'config_grounded_von_Mises_threshold_stress_source': "'data'"} + self.add_namelist_options(options=options, + out_name='namelist.landice') + vM_stream_replacements = {'input_file_VM_params': vM_path} + self.add_streams_file( + resource_location, 'streams.vM_params', + out_name='streams.landice', + template_replacements=vM_stream_replacements) + + # --- Symlink restart for projections/ctrl --- + if not is_historical: + hist_exp = f"historical_{model}" + os.symlink(f"../{hist_exp}/rst.2015-01-01.nc", + os.path.join(self.work_dir, 'rst.2015-01-01.nc')) + with open(os.path.join(self.work_dir, "restart_timestamp"), + "w") as text_file: + text_file.write("2015-01-01_00:00:00") + + # --- Add albany yaml, graph file, load script, job script --- + self.add_input_file( + filename='albany_input.yaml', + package=resource_location, + copy=True) + + make_graph_file(mesh_filename=init_cond_path, + graph_filename=os.path.join(self.work_dir, + 'graph.info')) + + symlink_load_script(self.work_dir) + + self.config.set('job', 'job_name', self.exp) + machine = self.config.get('deploy', 'machine') + pre_run_cmd = ('LOGDIR=previous_logs_`date +"%Y-%m-%d_%H-%M-%S"`;' + 'mkdir $LOGDIR; cp log* $LOGDIR; date') + post_run_cmd = "date" + write_job_script(self.config, machine, + target_cores=self.ntasks, min_cores=self.min_tasks, + work_dir=self.work_dir, + pre_run_commands=pre_run_cmd, + post_run_commands=post_run_cmd) + + self.add_model_as_input() + + def run(self): + """ + Run this step of the test case + """ + run_model(step=self, namelist='namelist.landice', + streams='streams.landice') diff --git a/compass/landice/tests/ismip7_run/ismip7_gris/streams.landice.template b/compass/landice/tests/ismip7_run/ismip7_gris/streams.landice.template new file mode 100644 index 0000000000..26f74dd7c0 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_gris/streams.landice.template @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compass/landice/tests/ismip7_run/ismip7_gris/streams.vM_params b/compass/landice/tests/ismip7_run/ismip7_gris/streams.vM_params new file mode 100644 index 0000000000..d9ae1ca3f6 --- /dev/null +++ b/compass/landice/tests/ismip7_run/ismip7_gris/streams.vM_params @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/docs/developers_guide/landice/test_groups/index.rst b/docs/developers_guide/landice/test_groups/index.rst index 2556037c52..b0871ac2b5 100644 --- a/docs/developers_guide/landice/test_groups/index.rst +++ b/docs/developers_guide/landice/test_groups/index.rst @@ -21,6 +21,7 @@ Test groups ismip6_forcing ismip6_run ismip7_forcing + ismip7_run isunnguata_sermia kangerlussuaq koge_bugt_s diff --git a/docs/developers_guide/landice/test_groups/ismip7_forcing.rst b/docs/developers_guide/landice/test_groups/ismip7_forcing.rst index 72aa8c7411..3191ce9a6b 100644 --- a/docs/developers_guide/landice/test_groups/ismip7_forcing.rst +++ b/docs/developers_guide/landice/test_groups/ismip7_forcing.rst @@ -36,6 +36,12 @@ The module :py:mod:`compass.landice.tests.ismip7_forcing.configure` validates that all required config options in the ``[ismip7]`` section have been set by the user (i.e., are not ``NotAvailable``). +Repository-local example user configs are available at +``compass/landice/tests/ismip7_forcing/ismip7_forcing_test.cfg`` (AIS) and +``compass/landice/tests/ismip7_forcing/ismip7_forcing_test_gis.cfg`` (GrIS). +These are intended for development/testing and include environment-specific +paths. + create_mapfile ~~~~~~~~~~~~~~ diff --git a/docs/developers_guide/landice/test_groups/ismip7_run.rst b/docs/developers_guide/landice/test_groups/ismip7_run.rst new file mode 100644 index 0000000000..46b1c81541 --- /dev/null +++ b/docs/developers_guide/landice/test_groups/ismip7_run.rst @@ -0,0 +1,118 @@ +.. _dev_landice_ismip7_run: + +ismip7_run +========== + +The ``ismip7_run`` test group +(:py:class:`compass.landice.tests.ismip7_run`) sets up experiments from +the ISMIP7 experimental protocol for both the Antarctic Ice Sheet (AIS) +and the Greenland Ice Sheet (GrIS). Optionally, the AIS test case +supports coupled MALI–Sea Level Model (SLM) simulations. +(see :ref:`landice_ismip7_run`). + +framework +--------- + +The ``ismip7_run`` test group +(:py:class:`compass.landice.tests.ismip7_run.Ismip7Run`) registers two +test cases: + +* :py:class:`compass.landice.tests.ismip7_run.ismip7_ais.Ismip7Ais` +* :py:class:`compass.landice.tests.ismip7_run.ismip7_gris.Ismip7Gris` + +There is no shared functionality between the two test cases at present. +Shared functions may be added in the future if the needed functionality +can be generalized. + +ismip7_ais +---------- + +The :py:class:`compass.landice.tests.ismip7_run.ismip7_ais.Ismip7Ais` +test case sets up an ensemble of ISMIP7 Antarctica simulations +(standalone MALI or coupled MALI-SLM). + +The constructor (``__init__``) does nothing other than allow the +``ismip7_ais`` test case to be listed by ``compass list`` without having +all individual experiments listed in a verbose listing. Each individual +experiment is a step rather than a test case to avoid excessive +subdirectories. + +The ``configure`` method parses the ``exp_list`` config option from the +``[ismip7_run_ais]`` section. It supports: + +* ``all`` — all 11 core experiments +* ``historical`` — just the two historical runs +* ``projections`` — the six SSP projection runs +* ``ctrl`` — the two CTRL2015 runs +* A comma-delimited list of specific experiment names + +Each selected experiment is added as a +:py:class:`~compass.landice.tests.ismip7_run.ismip7_ais.set_up_experiment.SetUpExperiment` +step and immediately removed from ``steps_to_run`` (experiments should be +submitted individually, not run through the test case). + +The ``run`` method raises an error instructing the user to submit batch +jobs for each experiment individually. + +set_up_experiment (AIS) +~~~~~~~~~~~~~~~~~~~~~~~ + +The class +:py:class:`compass.landice.tests.ismip7_run.ismip7_ais.set_up_experiment.SetUpExperiment` +defines a step for a single ISMIP7 AIS experiment. + +The ``setup`` method sets up the experiment directory by: + +1. Creating symlinks to forcing files from the conventional path layout + under ``forcing_basepath``. +2. Symlinking the ``reference_surface_path`` file into the run directory, + where it is used by MALI when applying lapse rates. +3. Copying and populating the streams template with the correct forcing + filenames and intervals (monthly for SMB/temperature/runoff, annual + for lapse rates and thermal forcing, ``initial_only`` for melt + parameters). +4. Processing the namelist template for the experiment's time period + and restart frequency. +5. Adding calving-specific streams (face melting, von Mises params) if + configured. +6. Creating a restart symlink for projection experiments pointing to + the corresponding ESM's historical restart + (``../historical_{model}/rst.2015-01-01.nc``). +7. Setting up CTRL2015 experiments with constant-climate forcing + (``initial_only`` intervals). +8. Setting up the OCX experiment with reanalysis-based forcing. +9. If SLM coupling is enabled, adding a ``CreateSlmMappingFiles`` step + and writing the SLM namelist from the Jinja2 template. +10. Generating a ``graph.info`` file and a SLURM job script. +11. Symlinking the compass load script into the run directory. + +The ``run`` method executes MALI for the given experiment. + +create_slm_mapping_files +~~~~~~~~~~~~~~~~~~~~~~~~ + +The class +:py:class:`compass.landice.tests.ismip7_run.ismip7_ais.create_slm_mapping_files.CreateSlmMappingFiles` +creates mapping files between the MALI mesh and the SLM grid. This step +is only added when sea-level model coupling is enabled. + +ismip7_gris +----------- + +The :py:class:`compass.landice.tests.ismip7_run.ismip7_gris.Ismip7Gris` +test case mirrors ``ismip7_ais`` but for the Greenland Ice Sheet. + +Key differences from the AIS test case: + +* Ocean thermal forcing is 2D (depth-averaged) rather than 3D. +* No sea-level model coupling. +* Default calving method is ``crevasse_depth``. +* Config section is ``[ismip7_run_gris]``. + +set_up_experiment (GrIS) +~~~~~~~~~~~~~~~~~~~~~~~~ + +The class +:py:class:`compass.landice.tests.ismip7_run.ismip7_gris.set_up_experiment.SetUpExperiment` +follows the same logic as the AIS version, with the differences noted +above (2D TF stream, no SLM support). diff --git a/docs/users_guide/landice/test_groups/index.rst b/docs/users_guide/landice/test_groups/index.rst index f744061134..6a49090c00 100644 --- a/docs/users_guide/landice/test_groups/index.rst +++ b/docs/users_guide/landice/test_groups/index.rst @@ -26,6 +26,7 @@ physics but that are not run routinely. ismip6_forcing ismip6_run ismip7_forcing + ismip7_run isunnguata_sermia kangerlussuaq koge_bugt_s diff --git a/docs/users_guide/landice/test_groups/ismip7_forcing.rst b/docs/users_guide/landice/test_groups/ismip7_forcing.rst index 1740f95a5f..e26d3f88e9 100644 --- a/docs/users_guide/landice/test_groups/ismip7_forcing.rst +++ b/docs/users_guide/landice/test_groups/ismip7_forcing.rst @@ -43,6 +43,21 @@ To use this test group, users need to: 5. Run the ``ocean_thermal`` test case for each model and scenario combination. +Example user config files are provided in the source tree for local testing: + +* ``compass/landice/tests/ismip7_forcing/ismip7_forcing_test.cfg`` + (AIS-focused example) +* ``compass/landice/tests/ismip7_forcing/ismip7_forcing_test_gis.cfg`` + (GrIS-focused example) + +These files contain machine-specific absolute paths and are intended as +templates, not portable defaults. Copy one and edit paths and options for +your environment before using it with ``compass setup ... -f USER.cfg``. + +The AIS example enables both ocean processing modes and uses a 2015-2300 +processing window for both atmosphere and ocean thermal scenario forcing. +The GrIS example enables scenario ocean processing only and uses 1980-2015. + .. _landice_ismip7_forcing_input_data: Input Data diff --git a/docs/users_guide/landice/test_groups/ismip7_run.rst b/docs/users_guide/landice/test_groups/ismip7_run.rst new file mode 100644 index 0000000000..1d418eb849 --- /dev/null +++ b/docs/users_guide/landice/test_groups/ismip7_run.rst @@ -0,0 +1,229 @@ +.. _landice_ismip7_run: + +ismip7_run +========== + +The ``landice/ismip7_run`` test group sets up one or more experiments from the +`ISMIP7 protocol `_ for both the +Antarctic Ice Sheet (AIS) and the Greenland Ice Sheet (GrIS). + +This functionality assumes the forcing files have already been generated using +the :ref:`landice_ismip7_forcing` test group and organized into the expected +directory layout. It creates a consistent set of run directories for the +requested experiments. Each experiment directory is self-contained with +namelists, streams, forcing symlinks, and a job script ready for submission. + +The test group includes two test cases: + +* ``ismip7_ais`` — Antarctic Ice Sheet experiments +* ``ismip7_gris`` — Greenland Ice Sheet experiments + +.. note:: + + This test group is not meant for automated running of experiments. + Expert knowledge is recommended for conducting the actual simulations. + Each experiment (step) should be submitted manually via its job script. + +.. _landice_ismip7_run_experiments: + +Experiment Matrix +----------------- + +The ISMIP7 core protocol defines 11 experiments per ice sheet: + +.. list-table:: + :header-rows: 1 + + * - Experiment + - Scenario + - Start + - End + - ESM + * - ``historical_CESM2-WACCM`` + - Historical + - ≥1850 + - 2014 + - CESM2-WACCM + * - ``historical_MRI-ESM2-0`` + - Historical + - ≥1850 + - 2014 + - MRI-ESM2-0 + * - ``ssp370_CESM2-WACCM`` + - SSP370 + - 2015 + - 2100 + - CESM2-WACCM + * - ``ssp370_MRI-ESM2-0`` + - SSP370 + - 2015 + - 2100 + - MRI-ESM2-0 + * - ``ssp126_CESM2-WACCM`` + - SSP126 + - 2015 + - 2300 + - CESM2-WACCM + * - ``ssp126_MRI-ESM2-0`` + - SSP126 + - 2015 + - 2300 + - MRI-ESM2-0 + * - ``ssp585_CESM2-WACCM`` + - SSP585 + - 2015 + - 2300 + - CESM2-WACCM + * - ``ssp585_MRI-ESM2-0`` + - SSP585 + - 2015 + - 2300 + - MRI-ESM2-0 + * - ``ctrl_CESM2-WACCM`` + - CTRL2015 + - 2015 + - 2300 + - CESM2-WACCM + * - ``ctrl_MRI-ESM2-0`` + - CTRL2015 + - 2015 + - 2300 + - MRI-ESM2-0 + * - ``ocx`` + - OCX + - 1990 + - 2025 + - (reanalysis) + +Unlike ISMIP6, ISMIP7 requires a **separate historical simulation per ESM**. +Projection experiments automatically symlink their restart file from the +corresponding ESM's historical run (e.g., +``ssp585_CESM2-WACCM`` → ``../historical_CESM2-WACCM/rst.2015-01-01.nc``). + +.. _landice_ismip7_run_usage: + +Usage +----- + +1. Process forcing data using :ref:`landice_ismip7_forcing`. + +2. Organize output into the expected directory layout:: + + {forcing_basepath}/ + ├── CESM2-WACCM_historical/ + │ ├── atmosphere/ + │ │ └── {mesh}_smb_CESM2-WACCM_historical_*.nc + │ └── ocean_thermal_forcing/ + │ └── {mesh}_thermal_forcing_CESM2-WACCM_historical_*.nc + ├── CESM2-WACCM_ssp585/ + │ ├── atmosphere/ + │ └── ocean_thermal_forcing/ + └── ... + +3. Create a user config file overriding the ``NotAvailable`` paths. + +4. Set up and run:: + + compass setup landice/ismip7_run/ismip7_ais -f my_ismip7_ais.cfg + # Then submit job scripts from individual experiment directories + +.. _landice_ismip7_run_config: + +config options +-------------- + +All config options should be reviewed and altered as needed. + +**AIS config** (``[ismip7_run_ais]``): + +.. code-block:: cfg + + [ismip7_run_ais] + + # Experiment list: "all", "historical", "projections", "ctrl", + # or comma-delimited experiment names + exp_list = all + + # Number of MPI tasks + ntasks = 128 + pio_stride = 128 + + # Base path to pre-processed forcing + forcing_basepath = NotAvailable + + # Initial condition and parameter files + init_cond_path = NotAvailable + melt_params_path = NotAvailable + reference_surface_path = NotAvailable + region_mask_path = NotAvailable + + # Climatology files for CTRL2015 experiments + ctrl_tf_climatology_path = NotAvailable + ctrl_atm_climatology_path = NotAvailable + + # OCX forcing path + ocx_forcing_path = NotAvailable + + # Calving: restore or von_mises + calving_method = restore + von_mises_parameter_path = NotAvailable + + # Face melting + use_face_melting = false + + # Sea-level model coupling + sea_level_model = false + slm_input_ice = NotAvailable + slm_input_earth = NotAvailable + slm_earth_structure = prem_512.l60K2C.sum18p6.dum19p2.tz19p4.lm22 + slm_input_others = NotAvailable + nglv = 2048 + +**GrIS config** (``[ismip7_run_gris]``) is similar (including +``reference_surface_path``) but without sea-level model options + +.. _landice_ismip7_run_forcing_streams: + +Forcing Streams +--------------- + +ISMIP7 uses more forcing fields than ISMIP6, at mixed temporal resolutions: + +**Monthly forcing** (``input_interval = 0000-01-00_00:00:00``): + +* ``sfcMassBal`` — surface mass balance +* ``surfaceAirTemperature`` — surface air temperature +* ``ismip6Runoff`` — ice sheet runoff +* ``ismip6_2dThermalForcing`` (GrIS) — ocean thermal forcing + +**Annual forcing** (``input_interval = 0001-00-00_00:00:00``): + +* ``sfcMassBalLapseRate`` — SMB elevation lapse rate +* ``surfaceAirTemperatureLapseRate`` — temperature lapse rate +* ``ismip6shelfMelt_3dThermalForcing`` (AIS) + +**Static** (``input_interval = initial_only``): + +* ``ismip6shelfMelt_zOcean`` — ocean depth coordinates (AIS only) +* ``ismip6shelfMelt_deltaT``, ``ismip6shelfMelt_basin``, + ``ismip6shelfMelt_gamma0`` — melt parameterization coefficients + +For CTRL2015 experiments, all forcing intervals are set to +``initial_only`` (constant climate). + +.. _landice_ismip7_run_ais: + +ismip7_ais +---------- + +``landice/ismip7_run/ismip7_ais`` sets up AIS experiments with 3D ocean +thermal forcing (30 vertical layers) and optional sea-level model coupling. + +.. _landice_ismip7_run_gris: + +ismip7_gris +----------- + +``landice/ismip7_run/ismip7_gris`` sets up GrIS experiments with 2D +(depth-averaged) ocean thermal forcing. Sea-level model coupling is not +currently supported for GrIS. Crevasse-depth calving is the default.