Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions components/lfric-xios/build/testframework/xiostest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from testframework import MpiTest
import xarray as xr
import matplotlib.pyplot as plt


##############################################################################
Expand Down Expand Up @@ -81,10 +82,41 @@ def nc_data_match(self, in_file: Path, out_file: Path, varname: str):
ds_in_comp = ds_in.sel(time=slice(comparison_window[0], comparison_window[1]))
ds_out_comp = ds_out.sel(time=slice(comparison_window[0], comparison_window[1]))

result = [(ds_in_comp['time'] == ds_out_comp['time']).values.all(),
(ds_in_comp[varname] == ds_out_comp[varname]).values.all()]
if ds_in_comp['time'].size == 0:
return False
else:
result = [(ds_in_comp['time'] == ds_out_comp['time']).values.all(),
(ds_in_comp[varname] == ds_out_comp[varname]).values.all()]
return all(result)

return all(result)
def plot_output(self, in_file: Path, out_file: Path, varname: str):
"""
Visually compare input and output data.
"""

def get_ts_data(file_path, field_id):

ds = xr.open_dataset(file_path, engine='netcdf4', decode_timedelta=False)
ts = ds[field_id].mean(ds[field_id].dims[1::])
time = ds[field_id].coords['time']

return ts, time

input_ts, input_time = get_ts_data(in_file, varname)
output_ts, output_time = get_ts_data(out_file, varname)

plt.rcParams["font.family"] = "serif"
_, ax = plt.subplots(figsize=([10.8, 4.8]))
ax.scatter(output_time, output_ts, c='C0', s=50)
ax.plot(output_time, output_ts, linestyle='--', lw=2, label="Model output data")
ax.scatter(input_time, input_ts, c='C3', marker='s', s=100, label="Input data")

ax.set_xlabel("Date/Time")
ax.set_ylabel("Mean model data")

plt.legend(frameon=False)
plt.savefig(f"{type(self).__name__}.png", bbox_inches="tight")
plt.close()

def post_execution(self, return_code):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
!-----------------------------------------------------------------------------
! (C) Crown copyright 2026 Met Office. All rights reserved.
! The file LICENCE, distributed with this code, contains details of the terms
! under which the code may be used.
!-----------------------------------------------------------------------------

! Tests the LFRic-XIOS temporal reading functionality
!
program lfric_xios_cyclic_temporal_test

use constants_mod, only: i_timestep, r_second
use event_mod, only: event_action
use event_actor_mod, only: event_actor_type
use field_mod, only: field_type, field_proxy_type
use file_mod, only: FILE_MODE_READ, FILE_MODE_WRITE
use io_context_mod, only: callback_clock_arg
use lfric_xios_action_mod, only: advance
use lfric_xios_context_mod, only: lfric_xios_context_type
use lfric_xios_driver_mod, only: lfric_xios_initialise, lfric_xios_finalise
use lfric_xios_file_mod, only: lfric_xios_file_type, OPERATION_TIMESERIES
use linked_list_mod, only: linked_list_type
use log_mod, only: log_event, log_level_info
use test_db_mod, only: test_db_type
use xios, only: xios_date

implicit none

type(test_db_type) :: test_db
type(lfric_xios_context_type), target, allocatable :: io_context

procedure(callback_clock_arg), pointer :: before_close
type(linked_list_type), pointer :: file_list
class(event_actor_type), pointer :: context_actor
procedure(event_action), pointer :: context_advance
type(field_type), pointer :: rfield
type(field_proxy_type) :: rproxy
type(xios_date) :: date
integer(i_timestep) :: file_freq

call test_db%initialise()
call lfric_xios_initialise( "test", test_db%comm, .false. )

! =============================== Start test ================================

allocate(io_context)
call io_context%initialise( "test_io_context", 1, 10 )

! Fixed attribute of input data
file_freq = int(60.0_r_second / test_db%clock%get_seconds_per_step(), i_timestep)

file_list => io_context%get_filelist()
call file_list%insert_item( lfric_xios_file_type( "lfric_xios_cyclic_input", &
xios_id="lfric_xios_cyclic_input", &
io_mode=FILE_MODE_READ, &
operation=OPERATION_TIMESERIES, &
freq=file_freq, &
cyclic=.true., &
fields_in_file=test_db%temporal_fields ) )
call file_list%insert_item( lfric_xios_file_type( "lfric_xios_cyclic_output", &
xios_id="lfric_xios_cyclic_output", &
io_mode=FILE_MODE_WRITE, &
operation=OPERATION_TIMESERIES, &
freq=1, &
fields_in_file=test_db%temporal_fields ) )

before_close => null()
call io_context%initialise_xios_context( test_db%comm, &
test_db%chi, test_db%panel_id, &
test_db%clock, test_db%calendar, &
before_close )


context_advance => advance
context_actor => io_context
call test_db%clock%add_event( context_advance, context_actor )
call io_context%set_active(.true.)

do while (test_db%clock%tick())
call test_db%temporal_fields%get_field("temporal_field", rfield)
rproxy = rfield%get_proxy()
call log_event("Valid data for this TS:", log_level_info)
print*,rproxy%data(1)
end do

deallocate(io_context)

! ============================== Finish test =================================

call lfric_xios_finalise()
call test_db%finalise()

end program lfric_xios_cyclic_temporal_test
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
##############################################################################
# (C) Crown copyright 2026 Met Office. All rights reserved.
# The file LICENCE, distributed with this code, contains details of the terms
# under which the code may be used.
##############################################################################
"""
A set of tests which exercise the temporal reading functionality provided by
the LFRic-XIOS component.
"""
from testframework import TestEngine, TestFailed
from xiostest import LFRicXiosTest
from pathlib import Path
import sys

###############################################################################
class LfricXiosFullCyclicTest(LFRicXiosTest): # pylint: disable=too-few-public-methods
"""
Tests the LFRic-XIOS temporal reading functionality for a full set of cyclic data
"""

def __init__(self):
super().__init__(command=[sys.argv[1], "resources/configs/cyclic_full.nml"], processes=1)
test_data_dir = Path(Path.cwd(), 'resources/data')
Path('lfric_xios_cyclic_input.nc').unlink(missing_ok=True)
self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_cyclic_input.nc'))
self.gen_config( Path("resources/configs/cyclic_base.nml"),
Path("resources/configs/cyclic_full.nml"), {} )

def test(self, returncode: int, out: str, err: str):
"""
Test the output of the context test
"""

if returncode != 0:
print(out)
raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" +
f"stderr:\n" +
f"{err}")

self.plot_output(Path('lfric_xios_cyclic_input.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field')

if not self.nc_data_match(Path('lfric_xios_cyclic_input.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field'):
raise TestFailed("Output data does not match input data for same time values")

return "Reading full set of cyclic data okay..."


class LfricXiosFutureCyclicTest(LFRicXiosTest): # pylint: disable=too-few-public-methods
"""
Tests the LFRic-XIOS temporal reading functionality when data is in the future
"""

def __init__(self):
super().__init__(command=[sys.argv[1], "resources/configs/cyclic_future.nml"], processes=1)
test_data_dir = Path(Path.cwd(), 'resources/data')
Path('lfric_xios_cyclic_input.nc').unlink(missing_ok=True)
self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_cyclic_input.nc'))
self.gen_config( Path("resources/configs/cyclic_base.nml"),
Path("resources/configs/cyclic_future.nml"),
{"calendar_start":"'2024-01-01 14:55:00'"} )

def test(self, returncode: int, out: str, err: str):
"""
Test the output of the context test
"""

expected_error_code = "ERROR: I/O context must start after data time " \
"window when reading cyclic temporal data"

self.plot_output(Path('lfric_xios_cyclic_input.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field')

if returncode == 1:
errorcode = err.split("\n")[0].split("0:")[1]
if not errorcode == expected_error_code:
raise TestFailed("Incorrect error handling of cyclic future data")
else:
raise TestFailed("Unexpected non-failure of test executable")

return "Expected error for future cyclic data reading..."


class LfricXiosPastCyclicTest(LFRicXiosTest): # pylint: disable=too-few-public-methods
"""
Tests the LFRic-XIOS temporal reading functionality when data is in the future
"""

def __init__(self):
super().__init__(command=[sys.argv[1], "resources/configs/cyclic_future.nml"], processes=1)
test_data_dir = Path(Path.cwd(), 'resources/data')
Path('lfric_xios_cyclic_input.nc').unlink(missing_ok=True)
self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_cyclic_input.nc'))
self.gen_data(Path(test_data_dir, 'cyclic_past_kgo.cdl'), Path('cyclic_past_kgo.nc'))
self.gen_config( Path("resources/configs/cyclic_base.nml"),
Path("resources/configs/cyclic_future.nml"),
{"calendar_start":"'2025-01-01 14:55:00'"} )

def test(self, returncode: int, out: str, err: str):
"""
Test the output of the context test
"""

if returncode != 0:
print(out)
raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" +
f"stderr:\n" +
f"{err}")
if not self.nc_data_match(Path('cyclic_past_kgo.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field'):
raise TestFailed("Output data does not match expected values")

return "Reading full set of cyclic data from the past okay..."


class LfricXiosCyclicHighFreqTest(LFRicXiosTest): # pylint: disable=too-few-public-methods
"""
Tests the LFRic-XIOS temporal reading functionality when data is in the future
"""

def __init__(self):
super().__init__(command=[sys.argv[1], "resources/configs/cyclic_future.nml"], processes=1)
test_data_dir = Path(Path.cwd(), 'resources/data')
Path('lfric_xios_cyclic_input.nc').unlink(missing_ok=True)
self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_cyclic_input.nc'))
self.gen_data(Path(test_data_dir, 'cyclic_past_kgo.cdl'), Path('cyclic_past_kgo.nc'))
self.gen_config( Path("resources/configs/cyclic_base.nml"),
Path("resources/configs/cyclic_future.nml"),
{"dt":"10.0",
"timestep_end":"'150'"} )

def test(self, returncode: int, out: str, err: str):
"""
Test the output of the context test
"""

self.plot_output(Path('lfric_xios_cyclic_input.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field')

if returncode != 0:
print(out)
raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" +
f"stderr:\n" +
f"{err}")
if not self.nc_data_match(Path('cyclic_past_kgo.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field'):
raise TestFailed("Output data does not match expected values")

return "Reading full set of cyclic data from the past okay..."


class LfricXiosCyclicHighFreqTest(LFRicXiosTest): # pylint: disable=too-few-public-methods
"""
Tests the LFRic-XIOS temporal reading functionality when data is in the future
"""

def __init__(self):
super().__init__(command=[sys.argv[1], "resources/configs/cyclic_high_freq.nml"], processes=1)
test_data_dir = Path(Path.cwd(), 'resources/data')
Path('lfric_xios_cyclic_input.nc').unlink(missing_ok=True)
self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_cyclic_input.nc'))
self.gen_data(Path(test_data_dir, 'cyclic_high_freq_kgo.cdl'), Path('cyclic_high_freq_kgo.nc'))
self.gen_config( Path("resources/configs/cyclic_base.nml"),
Path("resources/configs/cyclic_high_freq.nml"),
{"dt":"10.0",
"timestep_end":"'150'"} )

def test(self, returncode: int, out: str, err: str):
"""
Test the output of the context test
"""

self.plot_output(Path('lfric_xios_cyclic_input.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field')

if returncode != 0:
print(out)
raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" +
f"stderr:\n" +
f"{err}")
if not self.nc_data_match(Path('cyclic_high_freq_kgo.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field'):
raise TestFailed("Output data does not match expected values")

return "Reading full set of cyclic data from the past okay..."


class LfricXiosCyclicNonSyncTest(LFRicXiosTest): # pylint: disable=too-few-public-methods
"""
Tests the LFRic-XIOS temporal reading functionality when model timesteps do not match data timesteps
"""

def __init__(self):
super().__init__(command=[sys.argv[1], "resources/configs/cyclic_non_sync.nml"], processes=1)
test_data_dir = Path(Path.cwd(), 'resources/data')
Path('lfric_xios_cyclic_input.nc').unlink(missing_ok=True)
self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_cyclic_input.nc'))
self.gen_data(Path(test_data_dir, 'non_sync_kgo.cdl'), Path('non_sync_kgo.nc'))
self.gen_config( Path("resources/configs/cyclic_base.nml"),
Path("resources/configs/cyclic_non_sync.nml"),
{"dt":"10.0",
"calendar_start":"'2024-01-01 15:03:20'",
"timestep_end":"'30'"} )

def test(self, returncode: int, out: str, err: str):
"""
Test the output of the context test
"""

if returncode != 0:
print(out)
raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" +
f"stderr:\n" +
f"{err}")
if not self.nc_data_match(Path('non_sync_kgo.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field'):
raise TestFailed("Output data does not match expected values")

self.plot_output(Path('lfric_xios_cyclic_input.nc'),
Path('lfric_xios_cyclic_output.nc'),
'temporal_field')

return "Reading non-synchronised cyclic data okay..."



##############################################################################
if __name__ == "__main__":
TestEngine.run(LfricXiosFullCyclicTest())
TestEngine.run(LfricXiosFutureCyclicTest())
TestEngine.run(LfricXiosPastCyclicTest())
TestEngine.run(LfricXiosCyclicHighFreqTest())
TestEngine.run(LfricXiosCyclicNonSyncTest())
Loading