diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_interp_test.f90 b/components/lfric-xios/integration-test/lfric_xios_temporal_interp_test.f90 new file mode 100644 index 000000000..81b0e8868 --- /dev/null +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_interp_test.f90 @@ -0,0 +1,94 @@ +!----------------------------------------------------------------------------- +! (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_temporal_interp_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 lfric_xios_constants_mod, only: lx_second + 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_interp_input", & + xios_id="lfric_xios_interp_input", & + io_mode=FILE_MODE_READ, & + operation=OPERATION_TIMESERIES, & + freq=file_freq, & + cyclic=.true., & + update_freq=20*lx_second, & + fields_in_file=test_db%temporal_fields ) ) + call file_list%insert_item( lfric_xios_file_type( "lfric_xios_interp_output", & + xios_id="lfric_xios_interp_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_temporal_interp_test diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_interp_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_interp_test.py new file mode 100644 index 000000000..06bf075ed --- /dev/null +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_interp_test.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +############################################################################## +# (C) Crown copyright 2024 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. +The tests cover the reading of a piece of non-cyclic temporal data with data +points ranging from 15:01 to 15:10 in 10 1-minute intervals. The model start +time is changed to change how the model interacts with the data. +""" +from testframework import TestEngine, TestFailed +from xiostest import LFRicXiosTest +from pathlib import Path +import sys + +############################################################################### +class LfricXiosFullInterpTest(LFRicXiosTest): # pylint: disable=too-few-public-methods + """ + Tests the LFRic-XIOS temporal reading functionality for a full set of non-cyclic data + """ + + def __init__(self): + super().__init__(command=[sys.argv[1], "cyclic_high_freq.nml"], processes=1) + self.gen_data('temporal_data.cdl', 'lfric_xios_interp_input.nc') + self.gen_data('cyclic_high_freq_kgo.cdl', 'cyclic_high_freq_kgo.nc') + self.gen_config( "cyclic_base.nml", "cyclic_high_freq.nml", + {"dt":10.0, + "timestep_end":'150'} ) + + def test(self, returncode: int, out: str, err: str): + """ + Test the output of the interpolation 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(self.test_working_dir, 'lfric_xios_interp_input.nc'), + Path(self.test_working_dir, 'lfric_xios_interp_output.nc'), + 'temporal_field') + + if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_interp_input.nc'), + Path(self.test_working_dir, 'lfric_xios_interp_output.nc'), + 'temporal_field'): + raise TestFailed("Output data does not match input data for same time values") + + return "Reading and interpolating data okay..." + +class LfricXiosNonSyncInterpTest(LFRicXiosTest): # pylint: disable=too-few-public-methods + """ + Tests the LFRic-XIOS temporal reading functionality for a full set of non-cyclic data + """ + + def __init__(self): + super().__init__(command=[sys.argv[1], "cyclic_high_freq.nml"], processes=1) + self.gen_data('temporal_data.cdl', 'lfric_xios_interp_input.nc') + self.gen_data('cyclic_high_freq_kgo.cdl', 'cyclic_high_freq_kgo.nc') + self.gen_config( "cyclic_base.nml", "cyclic_high_freq.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 interpolation 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(self.test_working_dir, 'lfric_xios_interp_input.nc'), + Path(self.test_working_dir, 'lfric_xios_interp_output.nc'), + 'temporal_field') + + if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_interp_input.nc'), + Path(self.test_working_dir, 'lfric_xios_interp_output.nc'), + 'temporal_field'): + raise TestFailed("Output data does not match input data for same time values") + + return "Reading and interpolating data okay..." + + + +############################################################################## +if __name__ == "__main__": + TestEngine.run(LfricXiosFullInterpTest()) + TestEngine.run(LfricXiosNonSyncInterpTest()) diff --git a/components/lfric-xios/source/lfric_xios_constants_mod.f90 b/components/lfric-xios/source/lfric_xios_constants_mod.f90 index c4933de4d..d9830af05 100644 --- a/components/lfric-xios/source/lfric_xios_constants_mod.f90 +++ b/components/lfric-xios/source/lfric_xios_constants_mod.f90 @@ -19,4 +19,11 @@ module lfric_xios_constants_mod !< The largest integer that can be output by XIOS integer(kind=i_def), parameter :: xios_max_int = huge(0_int16) + !< Enums for configuring XIOS durations within LFRic + !< These must be large-ish prime numbers + integer, public, parameter :: lx_second = 107 + integer, public, parameter :: lx_day = 1049 + integer, public, parameter :: lx_month = 10259 + integer, public, parameter :: lx_year = 100537 + end module lfric_xios_constants_mod \ No newline at end of file diff --git a/components/lfric-xios/source/lfric_xios_file_mod.f90 b/components/lfric-xios/source/lfric_xios_file_mod.f90 index 7527eb3d4..08c78278d 100644 --- a/components/lfric-xios/source/lfric_xios_file_mod.f90 +++ b/components/lfric-xios/source/lfric_xios_file_mod.f90 @@ -95,6 +95,8 @@ module lfric_xios_file_mod logical :: context_init_read = .true. !> Temporal controller for file type(temporal_type) :: temporal + !> Update frequency for temporal control (enum, optional) + integer(i_def) :: update_freq = 0 !> XIOS representations !> Internal XIOS representation of the file @@ -202,11 +204,14 @@ end subroutine register_diagnostics_file !> @param[in] fields_in_file Array of fields contained in the file !> @param[in] is_diag Is it a diagnostics file? !> @param[in] diag_always_on_sampling Is the always-on sampling mode selected? +!> @param[in] file_convention Enum denoting the file convention to use for the file +!> @param[in] update_freq Enum for update frequency to be passed to temporal +!! controller (optional, only relevant for time series files) function lfric_xios_file_constructor( file_name, xios_id, io_mode, freq, & operation, cyclic, field_group_id, & fields_in_file, is_diag, & diag_always_on_sampling, & - file_convention ) result(self) + file_convention, update_freq ) result(self) implicit none @@ -223,7 +228,7 @@ function lfric_xios_file_constructor( file_name, xios_id, io_mode, freq, & logical(l_def), optional, intent(in) :: is_diag logical(l_def), optional, intent(in) :: diag_always_on_sampling integer(i_def), optional, intent(in) :: file_convention - + integer(i_def), optional, intent(in) :: update_freq type(field_collection_iterator_type) :: iter class(field_parent_type), pointer :: fld => null() @@ -241,6 +246,10 @@ function lfric_xios_file_constructor( file_name, xios_id, io_mode, freq, & self%cyclic = cyclic end if + if (present(update_freq)) then + self%update_freq = update_freq + end if + if (present(freq)) then if (freq < 0) then ! we are going to allow freq = 0 (= no_freq) call log_event( "XIOS files cannot have negative frequency", & @@ -424,8 +433,9 @@ subroutine register_with_context(self) ! the temporal object initialiser which will tell XIOS which time entry ! to start reading from call xios_set_attr(self%handle, cyclic=self%cyclic) - call self%temporal%initialise( self%xios_id, self%path, self%fields, & - self%frequency, self%cyclic, record_offset ) + call self%temporal%initialise( self%xios_id, self%path, self%fields, & + self%frequency, self%cyclic, self%update_freq, & + record_offset ) call xios_set_attr(self%handle, record_offset=record_offset) end if diff --git a/components/lfric-xios/source/lfric_xios_temporal_mod.x90 b/components/lfric-xios/source/lfric_xios_temporal_mod.x90 index 74e6a67cd..bb20a4a80 100644 --- a/components/lfric-xios/source/lfric_xios_temporal_mod.x90 +++ b/components/lfric-xios/source/lfric_xios_temporal_mod.x90 @@ -24,7 +24,7 @@ module lfric_xios_temporal_mod use lfric_ncdf_file_mod, only: lfric_ncdf_file_type use lfric_xios_field_mod, only: lfric_xios_field_type use lfric_xios_utils_mod, only: parse_date_as_xios, seconds_from_date, & - read_time_data + read_time_data, duration_from_enum use linked_list_data_mod, only: linked_list_data_type use log_mod, only: log_event, log_scratch_space, & LOG_LEVEL_ERROR, LOG_LEVEL_INFO, & @@ -73,6 +73,9 @@ module lfric_xios_temporal_mod type(xios_duration) :: io_frequency !> Do initial fields need to be read logical(l_def) :: read_initial_fields = .false. + !> How often the model data updated + type(xios_duration) :: update_frequency + logical :: interp_flag = .false. contains @@ -106,9 +109,10 @@ contains !> @param[in,out] file_fields A list of fields within the file !> @param[in] io_frequency How often the file is read !> @param[in] cyclic Is the time axis cyclic or not + !> @param[in] update_frequency How often the model data should be updated (enum) !> @param[out] record_offset Time index for XIOS to start reading data from subroutine initialise( self, file_id, file_path, file_fields, io_frequency, & - cyclic, record_offset ) + cyclic, update_freq, record_offset ) implicit none @@ -118,6 +122,7 @@ contains type(lfric_xios_field_type), intent(inout) :: file_fields(:) type(xios_duration), intent(in) :: io_frequency logical, intent(in) :: cyclic + integer(i_def), intent(in) :: update_freq integer(i_def), intent(out) :: record_offset type(xios_date) :: context_start @@ -130,6 +135,8 @@ contains self%io_frequency = io_frequency + self%update_frequency = duration_from_enum(update_freq) + self%is_cyclic = cyclic if (self%is_cyclic) then self%cycle_period = self%io_frequency * size(self%time_data) @@ -141,9 +148,17 @@ contains ! between the first two time entries call self%align_time_axis(context_start, record_offset) - ! Set window size to 2 if model timesteps not synchonised with time data + ! Add 1 to window_size if model timesteps not synchonised with time data if (.not. context_start == self%time_data(1)) then - self%window_size = 2 + self%window_size = self%window_size + 1 + end if + + ! Add 1 to window size if interpolation is required + if (context_start + self%update_frequency == context_start + xios_duration(0)) then + self%interp_flag = .false. + else + self%interp_flag = .true. + self%window_size = self%window_size + 1 end if ! The lfric_xios_field_type object is used to store the pointers to the model fields @@ -168,10 +183,12 @@ contains call file_fields(i)%set_model_field(self%fields(self%read_index,i)) end do - if (.not. context_start == self%time_data(1)) then + if (.not. context_start == self%time_data(1) .or. self%interp_flag) then ! Set up additional file definition for initialisation call self%setup_initial_read_definition(file_id, file_path, record_offset, n_fields) - call self%shift_read_index(file_fields) + do j = 1, self%window_size - 1 + call self%shift_read_index(file_fields) + end do end if end subroutine initialise @@ -253,18 +270,20 @@ contains type(xios_file) :: initial_read_file type(xios_fieldgroup) :: init_fieldgroup character(str_def) :: init_fields_id + character(len=7) :: init_suffix class(field_parent_type), pointer :: init_field_ptr + call xios_get_timestep(timestep_duration) + call xios_get_handle("file_definition", file_definition) + ! Set up separate file instance for initial read, in the case of time-varying ! inputs that need multiple time entries present on initialisation allocate(self%init_fields(self%window_size-1, n_fields)) do f = 1, self%window_size-1 - call xios_get_timestep(timestep_duration) - - call xios_get_handle("file_definition", file_definition) + write(init_suffix, "(A,I0)") "_init_", f call xios_add_child( file_definition, initial_read_file, & - trim(file_id)//"_init" ) + trim(file_id)//init_suffix ) call xios_set_attr( initial_read_file, name=trim(adjustl(file_path)), & output_freq=timestep_duration, & @@ -278,7 +297,7 @@ contains record_offset = record_offset + 1 ! Add fields to initial file definition - init_fields_id = trim(file_id)//"_init_fields" + init_fields_id = trim(file_id)//init_suffix//"_fields" call xios_add_child(initial_read_file, init_fieldgroup, trim(init_fields_id)) do i = 1, n_fields init_field_ptr => self%fields(f,i) @@ -310,15 +329,17 @@ contains logical :: carryon type(xios_date) :: model_date - integer(i_def) :: next_field_index, i + integer(i_def) :: next_field_index, i, f ! Get current date from XIOS calendar call xios_get_current_date(model_date) ! Perform initial field read if (self%read_initial_fields) then - do i = 1, size(self%init_fields(1,:)) - call self%init_fields(1,i)%recv() + do f = 1, self%window_size - 1 + do i = 1, size(self%init_fields(f,:)) + call self%init_fields(f,i)%recv() + end do end do self%read_initial_fields = .false. call self%update_model_data() @@ -342,7 +363,13 @@ contains if ( self%next_model_update <= model_date ) then call self%update_model_data() - self%next_model_update = self%time_data(2) + ! If the update frequency is 0, the model data is updated at each point + ! corresponding to the frequency of the input data + if (self%interp_flag) then + self%next_model_update = self%next_model_update + self%update_frequency + else + self%next_model_update = self%time_data(2) + end if end if end function advance @@ -376,10 +403,13 @@ contains class(temporal_type), intent(inout) :: self + type(xios_date) :: current_date class(field_parent_type), pointer :: tmp_model_field type(field_type), pointer :: model_field - integer(i_def) :: i, n_fields + integer(i_def) :: i, n_fields, next_field_index + + call xios_get_current_date(current_date) n_fields = size(self%fields, dim=2) @@ -390,14 +420,67 @@ contains type is (field_type) model_field => tmp_model_field end select - call invoke(setval_X(model_field, self%fields(self%current_field_index, i))) + if (self%interp_flag) then + next_field_index = self%current_field_index + 1 + if (next_field_index > self%window_size) next_field_index = 1 + call interp_field_1d( model_field, & + self%fields(self%current_field_index, i), & + self%fields(next_field_index, i), & + [self%time_data(1), self%time_data(2)], & + current_date ) + else + call invoke(setval_X(model_field, self%fields(self%current_field_index, i))) + end if end do nullify(tmp_model_field) nullify(model_field) + end subroutine update_model_data + + !> @brief Populates a model field by interpolating between the data entries of + !> the current time window. + !> @param[in,out] model_field Pointer to field to be populated from data fields + !> @param[in] window_field_1 Field containing data for the first entry of the time window + !> @param[in] window_field_2 Field containing data for the second entry of the time window + !> @param[in] time_window The current time window + !> @param[in] model_date The current model date + subroutine interp_field_1d( model_field, window_field_1, window_field_2, time_window, model_date ) + + implicit none + + type(field_type), intent(inout), pointer :: model_field + type(field_type), intent(in) :: window_field_1 + type(field_type), intent(in) :: window_field_2 + type(xios_date), intent(in) :: time_window(2) + type(xios_date), intent(in) :: model_date + + type(field_type) :: dData, interp_gradient, interp_increment + real(r_def) :: window_dt_sec, window_time_sec, dt_recip + + ! Initialise interpolation variables + call model_field%copy_field_properties(dData) + call model_field%copy_field_properties(interp_gradient) + call model_field%copy_field_properties(interp_increment) + + ! Get time in seconds from XIOS dates + window_dt_sec = seconds_from_date(time_window(2)) - & + seconds_from_date(time_window(1)) + window_time_sec = seconds_from_date(model_date) - & + seconds_from_date(time_window(1)) + + dt_recip = 1.0_r_def/window_dt_sec + + ! Perform linear interpolation and populate model field with interpolated data + call invoke( X_minus_Y( dData, window_field_2, window_field_1 ), & + a_times_X( interp_gradient, dt_recip, dData ), & + a_times_X( interp_increment, window_time_sec, interp_gradient ), & + X_plus_Y( model_field, window_field_1, interp_increment ) ) + + end subroutine interp_field_1d + !----------------------------------------------------------------------------- ! Destructor routines !----------------------------------------------------------------------------- diff --git a/components/lfric-xios/source/lfric_xios_utils_mod.f90 b/components/lfric-xios/source/lfric_xios_utils_mod.f90 index 1e63b9da7..6681cca59 100644 --- a/components/lfric-xios/source/lfric_xios_utils_mod.f90 +++ b/components/lfric-xios/source/lfric_xios_utils_mod.f90 @@ -7,29 +7,30 @@ !> module lfric_xios_utils_mod - use constants_mod, only: i_def, r_def, str_def, str_long - use file_mod, only: FILE_OP_OPEN, FILE_MODE_READ - use lfric_ncdf_dims_mod, only: lfric_ncdf_dims_type - use lfric_ncdf_field_mod, only: lfric_ncdf_field_type - use lfric_ncdf_file_mod, only: lfric_ncdf_file_type - use lfric_mpi_mod, only: global_mpi - use lfric_xios_field_mod, only: lfric_xios_field_type - use log_mod, only: log_event, log_scratch_space, & - LOG_LEVEL_ERROR, LOG_LEVEL_INFO, & - LOG_LEVEL_TRACE - use mesh_mod, only: mesh_type - use xios, only: xios_date, xios_duration, & - xios_get_time_origin, & - xios_get_year_length_in_seconds, & - xios_date_convert_to_seconds, & - operator(<), operator(+) + use constants_mod, only: i_def, r_def, str_def, str_long + use file_mod, only: FILE_OP_OPEN, FILE_MODE_READ + use lfric_ncdf_dims_mod, only: lfric_ncdf_dims_type + use lfric_ncdf_field_mod, only: lfric_ncdf_field_type + use lfric_ncdf_file_mod, only: lfric_ncdf_file_type + use lfric_mpi_mod, only: global_mpi + use lfric_xios_constants_mod, only: lx_year, lx_month, lx_day, lx_second + use lfric_xios_field_mod, only: lfric_xios_field_type + use log_mod, only: log_event, log_scratch_space, & + LOG_LEVEL_ERROR, LOG_LEVEL_INFO, & + LOG_LEVEL_TRACE + use mesh_mod, only: mesh_type + use xios, only: xios_date, xios_duration, & + xios_get_time_origin, & + xios_get_year_length_in_seconds, & + xios_date_convert_to_seconds, & + operator(<), operator(+) implicit none private public :: parse_date_as_xios, seconds_from_date, & set_prime_io_mesh, prime_io_mesh_is, & - read_time_data + read_time_data, duration_from_enum integer(i_def), private, allocatable :: prime_io_mesh_ids(:) @@ -263,4 +264,32 @@ function read_time_data(file_path) result(time_data) end function read_time_data + + !> @brief Construct an XIOS duration object from an integer time enum + !! + !> @param[in] time_enum The integer time enum to be converted to an XIOS duration object + function duration_from_enum(time_enum) result(duration) + + implicit none + + integer(i_def), intent(in) :: time_enum + type(xios_duration) :: duration + + duration = xios_duration(0, 0, 0, 0, 0, 0) + + if (mod(time_enum, lx_year) == 0) then + duration%year = time_enum / lx_year + else if (mod(time_enum, lx_month) == 0) then + duration%month = time_enum / lx_month + else if (mod(time_enum, lx_day) == 0) then + duration%day = time_enum / lx_day + else if (mod(time_enum, lx_second) == 0) then + duration%second = time_enum / lx_second + else + call log_event( "Unable to construct XIOS duration from time enum", & + log_level_error ) + end if + + end function duration_from_enum + end module lfric_xios_utils_mod \ No newline at end of file diff --git a/components/lfric-xios/unit-test/lfric_xios_utils_mod_test.pf b/components/lfric-xios/unit-test/lfric_xios_utils_mod_test.pf index 74b044736..f283767e5 100644 --- a/components/lfric-xios/unit-test/lfric_xios_utils_mod_test.pf +++ b/components/lfric-xios/unit-test/lfric_xios_utils_mod_test.pf @@ -13,7 +13,7 @@ module lfric_xios_utils_mod_test use mesh_collection_mod, only : mesh_collection_type, & mesh_collection use mesh_mod, only : mesh_type, PLANE, PLANE_BI_PERIODIC - use xios, only : xios_date + use xios, only : xios_date, xios_duration use funit @@ -142,4 +142,53 @@ module lfric_xios_utils_mod_test end subroutine prime_io_mesh_test + + @test + subroutine duration_from_enum_test( this ) + + use lfric_xios_constants_mod, only : lx_second, lx_day, lx_month, lx_year + use lfric_xios_utils_mod, only : duration_from_enum + + implicit none + + class(utils_test_type), intent(inout) :: this + + type(xios_duration) :: test_duration + + test_duration = xios_duration(0, 0, 0, 0, 0, 0) + + test_duration = duration_from_enum( 5*lx_day ) + @assertEqual( int(test_duration%year, i_def), 0 ) + @assertEqual( int(test_duration%month, i_def), 0 ) + @assertEqual( int(test_duration%day, i_def), 5 ) + @assertEqual( int(test_duration%hour, i_def), 0 ) + @assertEqual( int(test_duration%minute, i_def), 0 ) + @assertEqual( int(test_duration%second, i_def), 0 ) + + test_duration = duration_from_enum( lx_second ) + @assertEqual( int(test_duration%year, i_def), 0 ) + @assertEqual( int(test_duration%month, i_def), 0 ) + @assertEqual( int(test_duration%day, i_def), 0 ) + @assertEqual( int(test_duration%hour, i_def), 0 ) + @assertEqual( int(test_duration%minute, i_def), 0 ) + @assertEqual( int(test_duration%second, i_def), 1 ) + + test_duration = duration_from_enum( 42*lx_year ) + @assertEqual( int(test_duration%year, i_def), 42 ) + @assertEqual( int(test_duration%month, i_def), 0 ) + @assertEqual( int(test_duration%day, i_def), 0 ) + @assertEqual( int(test_duration%hour, i_def), 0 ) + @assertEqual( int(test_duration%minute, i_def), 0 ) + @assertEqual( int(test_duration%second, i_def), 0 ) + + test_duration = duration_from_enum( 7*lx_month ) + @assertEqual( int(test_duration%year, i_def), 0 ) + @assertEqual( int(test_duration%month, i_def), 7 ) + @assertEqual( int(test_duration%day, i_def), 0 ) + @assertEqual( int(test_duration%hour, i_def), 0 ) + @assertEqual( int(test_duration%minute, i_def), 0 ) + @assertEqual( int(test_duration%second, i_def), 0 ) + + end subroutine duration_from_enum_test + end module lfric_xios_utils_mod_test \ No newline at end of file