Skip to content

Commit 0c2a837

Browse files
authored
Check CICE4 restart file dates (#539)
* Add CICE4 restart date checks in access.py driver
1 parent da04782 commit 0c2a837

File tree

5 files changed

+398
-38
lines changed

5 files changed

+398
-38
lines changed

payu/models/access.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ def setup(self):
218218
f90nml.write(cpl_nml, nml_work_path + '~')
219219
shutil.move(nml_work_path + '~', nml_work_path)
220220

221+
if model.model_type == 'cice':
222+
if model.prior_restart_path and not self.expt.repeat_run:
223+
# Set up and check the cice restart files.
224+
model.overwrite_restart_ptr(run_start_date,
225+
previous_runtime,
226+
start_date_fpath)
227+
221228
# Now change the oasis runtime. This needs to be done after the others.
222229
for model in self.expt.models:
223230
if model.model_type == 'oasis':

payu/models/cice.py

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import sys
1818
import shutil
1919
import datetime
20+
import struct
2021
import re
2122
import tarfile
2223

@@ -173,27 +174,7 @@ def setup(self):
173174
setup_nml = self.ice_in['setup_nml']
174175

175176
if self.prior_restart_path:
176-
# Generate ice.restart_file
177-
# TODO: better check of restart filename
178-
iced_restart_file = None
179-
iced_restart_files = [f for f in self.get_prior_restart_files()
180-
if f.startswith('iced.')]
181-
182-
if len(iced_restart_files) > 0:
183-
iced_restart_file = sorted(iced_restart_files)[-1]
184-
185-
if iced_restart_file is None:
186-
raise FileNotFoundError(
187-
f'No iced restart file found in {self.prior_restart_path}')
188-
189-
res_ptr_path = os.path.join(self.work_init_path,
190-
'ice.restart_file')
191-
if os.path.islink(res_ptr_path):
192-
# If we've linked in a previous pointer it should be deleted
193-
os.remove(res_ptr_path)
194-
with open(res_ptr_path, 'w') as res_ptr:
195-
res_dir = self.get_ptr_restart_dir()
196-
print(os.path.join(res_dir, iced_restart_file), file=res_ptr)
177+
self._make_restart_ptr()
197178

198179
# Update input namelist
199180
setup_nml['runtype'] = 'continue'
@@ -394,3 +375,130 @@ def link_restart(self, fpath):
394375
)
395376

396377
make_symlink(input_path, input_work_path)
378+
379+
def _make_restart_ptr(self):
380+
"""
381+
CICE4 restart pointers are created in the access driver, where
382+
the correct run start dates are available.
383+
"""
384+
pass
385+
386+
def overwrite_restart_ptr(self,
387+
run_start_date,
388+
previous_runtime,
389+
calendar_file):
390+
"""
391+
Generate restart pointer file 'ice.restart_file' pointing to
392+
'iced.YYYYMMDD' with the correct start date.
393+
Additionally check that the `iced.YYYYMNDD` restart file's header
394+
has the correct previous runtime.
395+
Typically called from the access driver, which provides the
396+
the correct date and runtime.
397+
398+
Parameters
399+
----------
400+
run_start_date: datetime.date
401+
Start date of the new simulation
402+
previous_runtime: int
403+
Seconds between experiment initialisation date and start date
404+
calendar_file: str
405+
Calendar restart file used to calculate timing information
406+
"""
407+
# Expected iced restart file name
408+
iced_restart_file = self.find_matching_iced(self.prior_restart_path,
409+
run_start_date)
410+
411+
res_ptr_path = os.path.join(self.work_init_path,
412+
'ice.restart_file')
413+
if os.path.islink(res_ptr_path):
414+
# If we've linked in a previous pointer it should be deleted
415+
os.remove(res_ptr_path)
416+
417+
iced_path = os.path.join(self.prior_restart_path,
418+
iced_restart_file)
419+
420+
# Check binary restart has correct time
421+
self._cice4_check_date_consistency(iced_path,
422+
previous_runtime,
423+
calendar_file)
424+
425+
with open(res_ptr_path, 'w') as res_ptr:
426+
res_dir = self.get_ptr_restart_dir()
427+
res_ptr.write(os.path.join(res_dir, iced_restart_file))
428+
429+
def _cice4_check_date_consistency(self,
430+
iced_path,
431+
previous_runtime,
432+
calendar_file):
433+
"""
434+
Check that the previous runtime in iced restart file header
435+
matches the runtime calculated from the calendar restart file.
436+
437+
Parameters
438+
----------
439+
iced_path: str or Path
440+
Path to iced restart file
441+
previous_runtime: int
442+
Seconds between experiment initialisation date and start date
443+
calendar_file: str or Path
444+
Calendar restart file used to calculate timing information
445+
"""
446+
_, _, cice_iced_runtime, _ = read_binary_iced_header(iced_path)
447+
if previous_runtime != cice_iced_runtime:
448+
msg = (f"Previous runtime from calendar file "
449+
f"{calendar_file}: {previous_runtime} "
450+
"does not match previous runtime in restart"
451+
f"file {iced_path}: {cice_iced_runtime}.")
452+
raise RuntimeError(msg)
453+
454+
def find_matching_iced(self, dir_path, date):
455+
"""
456+
Check a directory for an iced.YYYYMMDD restart file matching a
457+
specified date.
458+
Raises an error if the expected file is not found.
459+
460+
Parameters
461+
----------
462+
dir_path: str or Path
463+
Path to directory containing iced restart files
464+
date: datetime.date
465+
Date for matching iced file names
466+
467+
Returns
468+
-------
469+
iced_file_name: str
470+
Name of iced restart file found in dir_path matching
471+
the specified date
472+
"""
473+
# Expected iced restart file name
474+
date_int = cal.date_to_int(date)
475+
iced_restart_file = f"iced.{date_int:08d}"
476+
477+
dir_files = [f for f in os.listdir(dir_path)
478+
if os.path.isfile(os.path.join(dir_path, f))]
479+
480+
if iced_restart_file not in dir_files:
481+
msg = (f"CICE restart file not found in {dir_path}. Expected "
482+
f"{iced_restart_file} to exist. Is 'dumpfreq' set "
483+
f"in {self.ice_nml_fname} consistently with the run-length?"
484+
)
485+
raise FileNotFoundError(msg)
486+
487+
return iced_restart_file
488+
489+
490+
CICE4_RESTART_HEADER_SIZE = 24
491+
CICE4_RESTART_HEADER_FORMAT = '>iidd'
492+
493+
494+
def read_binary_iced_header(iced_path):
495+
"""
496+
Read header information from a CICE4 binary restart file.
497+
"""
498+
with open(iced_path, 'rb') as iced_file:
499+
header = iced_file.read(CICE4_RESTART_HEADER_SIZE)
500+
bint, istep0, time, time_forc = struct.unpack(
501+
CICE4_RESTART_HEADER_FORMAT,
502+
header)
503+
504+
return (bint, istep0, time, time_forc)

payu/models/cice5.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,28 @@ def _calc_runtime(self):
8181
the timing information in the cice_in.nml namelist.
8282
"""
8383
pass
84+
85+
def _make_restart_ptr(self):
86+
"""
87+
Generate restart pointer which points to the latest iced.YYYYMMDD
88+
restart file.
89+
"""
90+
iced_restart_file = None
91+
iced_restart_files = [f for f in self.get_prior_restart_files()
92+
if f.startswith('iced.')]
93+
94+
if len(iced_restart_files) > 0:
95+
iced_restart_file = sorted(iced_restart_files)[-1]
96+
97+
if iced_restart_file is None:
98+
raise FileNotFoundError(
99+
f'No iced restart file found in {self.prior_restart_path}')
100+
101+
res_ptr_path = os.path.join(self.work_init_path,
102+
'ice.restart_file')
103+
if os.path.islink(res_ptr_path):
104+
# If we've linked in a previous pointer it should be deleted
105+
os.remove(res_ptr_path)
106+
with open(res_ptr_path, 'w') as res_ptr:
107+
res_dir = self.get_ptr_restart_dir()
108+
res_ptr.write(os.path.join(res_dir, iced_restart_file))

test/models/test_access.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
import pytest
66
import cftime
7+
import f90nml
8+
from unittest.mock import patch
79

810
import payu
911

10-
from test.common import cd
12+
from test.common import cd, expt_workdir
1113
from test.common import tmpdir, ctrldir, labdir, workdir, archive_dir
1214
from test.common import config as config_orig
1315
from test.common import write_config
@@ -16,7 +18,6 @@
1618
from test.common import make_expt_archive_dir, remove_expt_archive_dirs
1719
from test.common import config_path
1820
from payu.calendar import GREGORIAN, NOLEAP
19-
import f90nml
2021

2122

2223
verbose = True
@@ -44,7 +45,6 @@ def setup_module(module):
4445
tmpdir.mkdir()
4546
labdir.mkdir()
4647
ctrldir.mkdir()
47-
workdir.mkdir()
4848
archive_dir.mkdir()
4949
make_all_files()
5050
except Exception as e:
@@ -65,6 +65,23 @@ def teardown_module(module):
6565
print(e)
6666

6767

68+
@pytest.fixture(autouse=True)
69+
def empty_workdir():
70+
"""
71+
Model setup tests require a clean work directory and symlink from
72+
the control directory.
73+
"""
74+
expt_workdir.mkdir(parents=True)
75+
# Symlink must exist for setup to use correct locations
76+
workdir.symlink_to(expt_workdir)
77+
78+
yield expt_workdir
79+
try:
80+
shutil.rmtree(expt_workdir)
81+
except FileNotFoundError:
82+
pass
83+
workdir.unlink()
84+
6885
@pytest.fixture
6986
def access_1year_config():
7087
# Write an access model config file with 1 year runtime
@@ -229,7 +246,15 @@ def test_access_cice_calendar_cycling_500(
229246
# which we are trying to bypass.
230247
shutil.copy(default_input_ice, cice_model.work_path)
231248

232-
access_model.setup()
249+
# Skip writing restart pointer as it requires iced file
250+
# with valid header. Restart pointer functionality is tested
251+
# in test_cice.py.
252+
with patch(
253+
'payu.models.cice.Cice.overwrite_restart_ptr',
254+
return_value=None
255+
):
256+
access_model.setup()
257+
233258
access_model.archive()
234259

235260
end_date_fpath = os.path.join(
@@ -269,7 +294,7 @@ def test_access_cice_1year_runtimes(
269294
expected_runtime
270295
):
271296
"""
272-
The large setup/archive cycling test won't pick up situations
297+
The large setup/archive cycling test won't pick up situations
273298
where the calculations during setup and archive are simultaneously
274299
wrong, e.g. if they both used the wrong calendar.
275300
Hence test seperately that the correct runtimes for cice are
@@ -331,7 +356,14 @@ def test_access_cice_1year_runtimes(
331356
# which we are trying to bypass.
332357
shutil.copy(ctrl_input_ice_path, cice_model.work_path)
333358

334-
access_model.setup()
359+
# Skip writing restart pointer as it requires iced file
360+
# with valid header. Restart pointer functionality is tested
361+
# in test_cice.py
362+
with patch(
363+
'payu.models.cice.Cice.overwrite_restart_ptr',
364+
return_value=None
365+
):
366+
access_model.setup()
335367

336368
# Check that the correct runtime is written to the work directory's
337369
# input ice namelist.

0 commit comments

Comments
 (0)