From c974561e22c664df2cec72c0fd6af311e74d6f86 Mon Sep 17 00:00:00 2001 From: Spencer Wong Date: Wed, 21 Aug 2024 09:48:21 +1000 Subject: [PATCH] Update tests, add additional access driver test, modify variable names for clarity --- payu/calendar.py | 2 +- payu/models/access.py | 49 ++++++++++++------ test/models/test_access.py | 101 +++++++++++++++++++++++++++++++++++-- test/test_calendar.py | 44 +++++++++++++--- 4 files changed, 169 insertions(+), 27 deletions(-) diff --git a/payu/calendar.py b/payu/calendar.py index 327cc90d..5ea72f00 100644 --- a/payu/calendar.py +++ b/payu/calendar.py @@ -122,7 +122,7 @@ def seconds_between_dates(start_date, end_date, caltype_int): delta = (date_to_cftime(end_date, calendar_str) - date_to_cftime(start_date, calendar_str)) - return delta.total_seconds() + return int(delta.total_seconds()) def date_to_cftime(date, calendar): diff --git a/payu/models/access.py b/payu/models/access.py index a629bb16..50f03e23 100644 --- a/payu/models/access.py +++ b/payu/models/access.py @@ -56,15 +56,30 @@ def __init__(self, expt, name, config): # Structure of model coupling namelist model.cpl_fname = 'input_ice.nml' model.cpl_group = 'coupling' - model.runtime0_key = 'runtime0' model.start_date_nml_name = "restart_date.nml" + # Experiment initialisation date + model.init_date_key = "init_date" + # Start date for new run + model.inidate_key = "inidate" + # Total time in seconds since initialisation date + model.runtime0_key = 'runtime0' + # Simulation length in seconds for new run + model.runtime_key = "runtime" if model.model_type == 'matm': # Structure of model coupling namelist model.cpl_fname = 'input_atm.nml' model.cpl_group = 'coupling' - model.runtime0_key = 'truntime0' model.start_date_nml_name = "restart_date.nml" + # Experiment initialisation date + model.init_date_key = "init_date" + # Start date for new run + model.inidate_key = "inidate" + # Total time in seconds since initialisation date + model.runtime0_key = 'truntime0' + # Simulation length in seconds for new run + model.runtime_key = "runtime" + def setup(self): if not self.top_level_model: @@ -131,11 +146,13 @@ def setup(self): # Experiment initialisation date init_date = cal.int_to_date( - start_date_nml["init_date"] + start_date_nml[model.init_date_key] ) # Start date of new run - run_start_date = cal.int_to_date(start_date_nml["inidate"]) + run_start_date = cal.int_to_date( + start_date_nml[model.inidate_key] + ) # run_start_date must be after initialisation date if run_start_date < init_date: @@ -144,8 +161,8 @@ def setup(self): f"{model.start_date_nml_name} must not be " "before initialisation date `init_date. " "Values provided: \n" - f"inidate = {start_date_nml['inidate']}\n" - f"init_date = {start_date_nml['init_date']}" + f"inidate={start_date_nml[model.inidate_key]}\n" + f"init_date={start_date_nml[model.init_date_key]}" ) raise ValueError(msg) @@ -160,7 +177,7 @@ def setup(self): else: init_date = cal.int_to_date( - cpl_group['init_date'] + cpl_group[model.init_date_key] ) previous_runtime = 0 run_start_date = init_date @@ -176,14 +193,14 @@ def setup(self): self.expt.runtime.get('seconds', 0), caltype) else: - run_runtime = cpl_group['runtime'] + run_runtime = cpl_group[model.runtime_key] # 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[model.init_date_key] = cal.date_to_int(init_date) + cpl_group[model.inidate_key] = cal.date_to_int(run_start_date) cpl_group[model.runtime0_key] = previous_runtime - cpl_group['runtime'] = int(run_runtime) + cpl_group[model.runtime_key] = int(run_runtime) if model.model_type == 'cice': if self.expt.counter and not self.expt.repeat_run: @@ -257,9 +274,9 @@ def archive(self): work_cpl_grp = work_cpl_nml[model.cpl_group] # Timing information on the completed run. - run_init_date_int = work_cpl_grp["init_date"] - run_start_date_int = work_cpl_grp["inidate"] - run_runtime = work_cpl_grp["runtime"] + exp_init_date_int = work_cpl_grp[model.init_date_key] + run_start_date_int = work_cpl_grp[model.inidate_key] + run_runtime = work_cpl_grp[model.runtime_key] run_caltype = work_cpl_grp["caltype"] # Calculate end date of completed run @@ -271,8 +288,8 @@ def archive(self): end_date_dict = { model.cpl_group: { - "init_date": run_init_date_int, - "inidate": cal.date_to_int(run_end_date) + model.init_date_key: exp_init_date_int, + model.inidate_key: cal.date_to_int(run_end_date) } } diff --git a/test/models/test_access.py b/test/models/test_access.py index 53eef41f..41357e40 100644 --- a/test/models/test_access.py +++ b/test/models/test_access.py @@ -15,6 +15,7 @@ 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 +from payu.calendar import GREGORIAN, NOLEAP import f90nml @@ -23,6 +24,7 @@ INPUT_ICE_FNAME = "input_ice.nml" RESTART_DATE_FNAME = "restart_date.nml" +SEC_PER_DAY = 24*60*60 def setup_module(module): @@ -178,7 +180,7 @@ def initial_start_date_file(restart_dir): # Teardown handled by restart_dir fixture -def test_esm_calendar_cycling_1000( +def test_access_cice_calendar_cycling_500( access_1year_config, ice_control_directory, default_input_ice, @@ -190,8 +192,8 @@ def test_esm_calendar_cycling_1000( over a large number of runs. """ - n_years = 1000 - expected_end_date = 11010101 + n_years = 500 + expected_end_date = 6010101 expected_end_init_date = 10101 # Setup the experiment with cd(ctrldir): @@ -243,3 +245,96 @@ def test_esm_calendar_cycling_1000( assert final_end_date == expected_end_date assert final_init_date == expected_end_init_date + + +@pytest.mark.parametrize( + "start_date_int, caltype, expected_runtime", + [(1010101, GREGORIAN, 365*SEC_PER_DAY), + (1010101, NOLEAP, 365*SEC_PER_DAY), + (1040101, GREGORIAN, 366*SEC_PER_DAY), + (1040101, NOLEAP, 365*SEC_PER_DAY), + (3000101, GREGORIAN, 365*SEC_PER_DAY), + (3000101, NOLEAP, 365*SEC_PER_DAY), + (4000101, GREGORIAN, 366*SEC_PER_DAY), + (4000101, NOLEAP, 365*SEC_PER_DAY)] +) +def test_access_cice_1year_runtimes( + access_1year_config, + ice_control_directory, + fake_cice_in, + restart_dir, + initial_start_date_file, + start_date_int, + caltype, + expected_runtime +): + """ + The large setup/archive cycling test won't pick up situations + where the calculations dyring setup and archive are simultaneously + wrong, e.g. if they both used the wrong calendar. + Hence test seperately that the correct runtimes for cice are + written by the access.setup() step for a range of standard + and generally tricky years. + """ + # Write an input_ice.nml namelist to the control directory + # with the specified calendar type. + ctrl_input_ice_path = ice_control_directory / INPUT_ICE_FNAME + input_ice_nml = { + "coupling": + { + "caltype": caltype + } + } + f90nml.write(input_ice_nml, ctrl_input_ice_path) + + # Reset the start date in the initial_start_date_file + initial_start_nml = f90nml.read(initial_start_date_file) + # Make sure our start date doesn't occur before the init_date + assert start_date_int >= initial_start_nml["coupling"]["init_date"] + + initial_start_nml["coupling"]["inidate"] = start_date_int + f90nml.write(initial_start_nml, initial_start_date_file, force=True) + + # Setup the experiment + with cd(ctrldir): + lab = payu.laboratory.Laboratory(lab_path=str(labdir)) + expt = payu.experiment.Experiment(lab, reproduce=False) + + # For the purposes of the test, use one year runtime + expt.runtime["years"] = 1 + expt.runtime["months"] = 0 + expt.runtime["days"] = 0 + expt.runtime["seconds"] = 0 + + # 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. + cice_model.prior_restart_path = restart_dir + cice_model.restart_path = restart_dir + + # 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(ctrl_input_ice_path, cice_model.work_path) + + access_model.setup() + + # Check that the correct runtime is written to the work directory's + # input ice namelist. + work_input_ice = f90nml.read(cice_model.work_path/INPUT_ICE_FNAME) + written_runtime = work_input_ice["coupling"]["runtime"] + assert written_runtime == expected_runtime diff --git a/test/test_calendar.py b/test/test_calendar.py index 84851b2e..5c626597 100644 --- a/test/test_calendar.py +++ b/test/test_calendar.py @@ -181,9 +181,9 @@ def test_parse_date_offset_no_offset_magnitude(): ), ( datetime.datetime(year=400, month=1, day=1), - datetime.datetime(year=401, month=1, day=1), + datetime.datetime(year=400, month=12, day=31), GREGORIAN, - 366 * SEC_PER_DAY + 365 * SEC_PER_DAY ), ( datetime.datetime(year=12, month=7, day=22), @@ -214,7 +214,7 @@ def test_seconds_between_dates(start_date, end_date, caltype_int, expected): [ (10101, datetime.date(1, 1, 1)), (100321, datetime.date(10, 3, 21)), - (38211130, datetime.date(3821, 11, 30)) + (99991231, datetime.date(9999, 12, 31)) ] ) def test_int_to_date(date_int, expected): @@ -226,19 +226,49 @@ def test_int_to_date(date_int, expected): assert converted_date == expected +@pytest.mark.parametrize( + "bad_date_int", + [0, 100000000, 101, -5, 11119153] +) +def test_int_to_date_failures(bad_date_int): + """ + Check that int_to_date does not allow non existent + or out of range dates. + """ + with pytest.raises(ValueError): + int_to_date(bad_date_int) + + @pytest.mark.parametrize( "start_date, years, months, days, seconds, caltype, expected", [ + # Normal year (datetime.date(101, 1, 1), 1, 0, 0, 0, GREGORIAN, 365*SEC_PER_DAY), + (datetime.date(101, 1, 1), 1, 0, 0, 0, NOLEAP, 365*SEC_PER_DAY), + # Leap year + (datetime.date(4, 1, 1), 1, 0, 0, 0, GREGORIAN, 366*SEC_PER_DAY), + (datetime.date(4, 1, 1), 1, 0, 0, 0, NOLEAP, 365*SEC_PER_DAY), + # Non-leap year due to 100 year rule + (datetime.date(100, 1, 1), 1, 0, 0, 0, GREGORIAN, 365*SEC_PER_DAY), + (datetime.date(100, 1, 1), 1, 0, 0, 0, NOLEAP, 365*SEC_PER_DAY), + # Leap year due to 400 year rule + (datetime.date(400, 1, 1), 1, 0, 0, 0, GREGORIAN, 366*SEC_PER_DAY), + (datetime.date(500, 1, 1), 1, 0, 0, 0, NOLEAP, 365*SEC_PER_DAY), + # Febraury in leap years (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), + # Misc (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) + (datetime.date(1, 1, 1), 9998, 11, 30, 0, + NOLEAP, (9998 * 365 + 364) * SEC_PER_DAY), + (datetime.date(1, 1, 1), 9998, 11, 30, 0, + GREGORIAN, (9998 * 365 + 2424 + 364) * SEC_PER_DAY), + (datetime.date(1, 1, 1), 0, 0, 0, 1, + GREGORIAN, 1), + (datetime.date(1, 1, 1), 0, 0, 0, 1, + NOLEAP, 1), ] ) def test_runtime_from_date(