diff --git a/payu/models/access.py b/payu/models/access.py index 5d891444..a629bb16 100644 --- a/payu/models/access.py +++ b/payu/models/access.py @@ -180,10 +180,8 @@ def setup(self): # Now write out new run start date and total runtime into the # work directory namelist. - cpl_group["init_date"] = cal.date_to_int( - init_date) - cpl_group['inidate'] = cal.date_to_int( - run_start_date) + cpl_group["init_date"] = cal.date_to_int(init_date) + cpl_group['inidate'] = cal.date_to_int(run_start_date) cpl_group[model.runtime0_key] = previous_runtime cpl_group['runtime'] = int(run_runtime) diff --git a/test/models/test_access.py b/test/models/test_access.py new file mode 100644 index 00000000..f56d6c65 --- /dev/null +++ b/test/models/test_access.py @@ -0,0 +1,242 @@ +import copy +import os +import shutil + +import pytest +import cftime + +import payu + +from test.common import cd +from test.common import tmpdir, ctrldir, labdir, workdir, archive_dir +from test.common import config as config_orig +from test.common import write_config +from test.common import make_all_files +from test.common import list_expt_archive_dirs +from test.common import make_expt_archive_dir, remove_expt_archive_dirs +from test.common import config_path +import f90nml + + +verbose = True + + +INPUT_ICE_FNAME = "input_ice.nml" +RESTART_DATE_FNAME = "restart_date.nml" + +def setup_module(module): + """ + Put any test-wide setup code in here, e.g. creating test files + """ + if verbose: + print("setup_module module:%s" % module.__name__) + + # Should be taken care of by teardown, in case remnants lying around + try: + shutil.rmtree(tmpdir) + except FileNotFoundError: + pass + + try: + tmpdir.mkdir() + labdir.mkdir() + ctrldir.mkdir() + workdir.mkdir() + archive_dir.mkdir() + make_all_files() + except Exception as e: + print(e) + + +def teardown_module(module): + """ + Put any test-wide teardown code in here, e.g. removing test outputs + """ + if verbose: + print("teardown_module module:%s" % module.__name__) + + try: + shutil.rmtree(tmpdir) + print('removing tmp') + except Exception as e: + print(e) + + +@pytest.fixture +def access_1year_config(): + # Write an access model config file with 1 year runtime + + # Create a config.yaml file with the cice submodel and 1 year run length + + # Global config + config = copy.deepcopy(config_orig) + config['model'] = 'access' + config['submodels'] = [{"name": "ice", + "model": "cice"}] + + config["calendar"] = {'start': {'year': 101, 'month': 1, 'days': 1}, + 'runtime': {'years': 1, 'months': 0, 'days': 0}} + write_config(config) + + # Run test + yield + + # Teardown + os.remove(config_path) + +@pytest.fixture +def ice_control_directory(): + # Make a cice control subdirectory + ice_ctrl_dir = ctrldir / "ice" + ice_ctrl_dir.mkdir() + + # Run test + yield ice_ctrl_dir + + # Teardown + shutil.rmtree(ice_ctrl_dir) + + +@pytest.fixture +def default_input_ice(ice_control_directory): + # Create base input_ice.nml namelist + ctrl_input_ice_path = ice_control_directory / INPUT_ICE_FNAME + + # Default timing values from the input_ice.nml namelist that will be + # overwritten by the calendar calculations. + default_input_nml = { + "coupling": + { + "caltype": 1, + "jobnum": 2, + "inidate": "01010101", + "init_date": "00010101", + "runtime0": 3155673600, + "runtime": 86400 + } + } + f90nml.write(default_input_nml, ctrl_input_ice_path) + + # Run test + yield ctrl_input_ice_path + + # Teardown handled by ice_control_directory fixture + + +@pytest.fixture +def fake_cice_in(ice_control_directory): + # Create a fake cice_in.nml file. This is irrelevant for the tests, + # however is required to exist for the experiment initialisation. + fake_cice_in_nml = { + "setup_nml": { + "restart_dir": "", + "history_dir": "" + }, + "grid_nml": { + "grid_file": "", + "kmt_file": "" + } + } + fake_cice_in_path = ice_control_directory / "cice_in.nml" + f90nml.write(fake_cice_in_nml, fake_cice_in_path) + + yield fake_cice_in_path + + # Teardown handled by ice_control_directory fixture + + +@pytest.fixture +def restart_dir(): + # Create restart directory for ice timing tests + restart_path = archive_dir / "restart" + restart_path.mkdir() + + # Run test + yield restart_path + + # Teardown + shutil.rmtree(restart_path) + +@pytest.fixture +def initial_start_date_file(restart_dir): + # Initital start date for testing calendar cycling + initial_res_date = { + "coupling": { + "init_date": 10101, + "inidate": 1010101 + } + } + res_date_path = restart_dir / RESTART_DATE_FNAME + f90nml.write(initial_res_date, res_date_path) + + # Run test + yield res_date_path + + # Teardown handled by restart_dir fixture + + +def test_esm_calendar_cycling_1000( + access_1year_config, + ice_control_directory, + default_input_ice, + fake_cice_in, + restart_dir, + initial_start_date_file): + """ + Test that cice run date calculations remain correct when cycling + over a large number of runs. + """ + + n_years = 1000 + expected_end_date = 11010101 + expected_end_init_date = 10101 + # Setup the experiment + with cd(ctrldir): + lab = payu.laboratory.Laboratory(lab_path=str(labdir)) + expt = payu.experiment.Experiment(lab, reproduce=False) + # Get the models + for model in expt.models: + if model.model_type == "cice": + cice_model = model + # There are two access models within the experiment. The top level + # model, expt.model, and the one under expt.models. The top level + # model's setup and archive steps are the ones that actually run. + access_model = expt.model + + # Overwrite cice model paths created during experiment initialisation. + # It's simpler to just set them here than rely on the ones collected + # from the fake namelist files. + cice_model.work_path = workdir + + # Path to read and write restart dates from. In a real experiment + # The read and write restart dirs would be different and increment + # each run. However we will just use one for the tests to avoid + # creating hundreds of directories. + cice_model.prior_restart_path = restart_dir + cice_model.restart_path = restart_dir + + for i in range(n_years): + if i % 100 == 0: + print(f"Access setup/archive cycle: {i}") + + # Manually copy the input_ice.nml file from the control directory + # to the work directory. This would normally happen in cice.setup() + # which we are trying to bypass. + shutil.copy(default_input_ice, cice_model.work_path) + + access_model.setup() + access_model.archive() + + end_date_fpath = os.path.join( + cice_model.restart_path, + cice_model.start_date_nml_name + ) + + end_date_nml = f90nml.read(end_date_fpath)[ + cice_model.cpl_group] + + final_end_date = end_date_nml["inidate"] + final_init_date = end_date_nml["init_date"] + + assert final_end_date == expected_end_date + assert final_init_date == expected_end_init_date diff --git a/test/test_calendar.py b/test/test_calendar.py index 5be14a28..f35d7784 100644 --- a/test/test_calendar.py +++ b/test/test_calendar.py @@ -3,7 +3,8 @@ import pytest from payu.calendar import parse_date_offset, DatetimeOffset -from payu.calendar import seconds_between_dates +from payu.calendar import seconds_between_dates, int_to_date +from payu.calendar import runtime_from_date from payu.calendar import GREGORIAN, NOLEAP SEC_PER_DAY = 24*60*60 @@ -205,3 +206,56 @@ def test_parse_date_offset_no_offset_magnitude(): ) def test_seconds_between_dates(start_date, end_date, caltype_int, expected): assert seconds_between_dates(start_date, end_date, caltype_int) == expected + + +@pytest.mark.parametrize( + "date_int, expected", + [ + (10101, datetime.date(1, 1, 1)), + (100321, datetime.date(10, 3, 21)), + (38211130, datetime.date(3821, 11, 30)) + ] +) +def test_int_to_date(date_int, expected): + """ + Check that integers typically read in from namelists + are correctly converted to datetime.date objects. + """ + converted_date = int_to_date(date_int) + assert converted_date == expected + + +@pytest.mark.parametrize( + "start_date, years, months, days, seconds, caltype, expected", + [ + (datetime.date(101, 1, 1), 1, 0, 0, 0, GREGORIAN, 365*SEC_PER_DAY), + (datetime.date(40, 2, 8), 0, 1, 0, 0, GREGORIAN, 29*SEC_PER_DAY), + (datetime.date(40, 2, 8), 0, 1, 0, 0, NOLEAP, 28*SEC_PER_DAY), + (datetime.date(153, 8, 21), 2, 7, 510, 5321, + GREGORIAN, 1453*SEC_PER_DAY + 5321), + # Extreme cases, unlikely to ever happen + (datetime.date(1, 1, 1), 9998, 0, 0, 0, + NOLEAP, 9998 * 365 * SEC_PER_DAY), + (datetime.date(1, 1, 1), 9998, 0, 0, 0, + GREGORIAN, (9998 * 365 + 2424) * SEC_PER_DAY) + ] +) +def test_runtime_from_date( + start_date, + years, + months, + days, + seconds, + caltype, + expected): + """ + Test that the number of seconds calculated for run lengths is correct. + """ + runtime = runtime_from_date(start_date, + years, + months, + days, + seconds, + caltype) + + assert runtime == expected