diff --git a/README.md b/README.md index b8786a2d..dc224d2d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ ClimaUtilities.jl to process input data and remap it onto the simulation grid. - [`OutputPathGenerator`](https://clima.github.io/ClimaUtilities.jl/dev/outputpathgenerator/) to prepare the output directory structure of a simulation. -- [`TimeManager`](https://clima.github.io/ClimaUtilities.jl/dev/timemanager/) to +- [`CallbackManager`](https://clima.github.io/ClimaUtilities.jl/dev/callbackmanager/) to handle dates. ## ClimaUtilities.jl Developer Guidelines @@ -178,6 +178,5 @@ two commits when the second just fixes the first). The `Space` and `TimeVaryingInputs` modules were initially developed in the context of [`ClimaLand`](https://github.com/CliMA/ClimaLand.jl), the -`TempestRegridder` and `TimeManager` ones were initially developed in +`TempestRegridder` and `CallbackManager` ones were initially developed in [`ClimaCoupler`](https://github.com/CliMA/ClimaCoupler.jl). - diff --git a/docs/make.jl b/docs/make.jl index d0770fde..2de001e4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -23,7 +23,7 @@ pages = [ "DataHandling" => "datahandling.md", "Regridders" => "regridders.md", "OutputPathGenerator" => "outputpathgenerator.md", - "TimeManager" => "timemanager.md", + "CallbackManager" => "callbackmanager.md", "Frequently Asked Questions" => "faqs.md", ] diff --git a/docs/src/callbackmanager.md b/docs/src/callbackmanager.md new file mode 100644 index 00000000..3859c36d --- /dev/null +++ b/docs/src/callbackmanager.md @@ -0,0 +1,18 @@ +# CallbackManager + +This module contains functions that handle dates and times +in simulations. The functions in this module often call +functions from Julia's [Dates](https://docs.julialang.org/en/v1/stdlib/Dates/) module. + +## CallbackManager API + +```@docs +ClimaUtilities.CallbackManager.HourlyCallback +ClimaUtilities.CallbackManager.MonthlyCallback +ClimaUtilities.CallbackManager.Monthly +ClimaUtilities.CallbackManager.EveryTimestep +ClimaUtilities.CallbackManager.to_datetime +ClimaUtilities.CallbackManager.strdate_to_datetime +ClimaUtilities.CallbackManager.datetime_to_strdate +ClimaUtilities.CallbackManager.trigger_callback! +``` diff --git a/docs/src/timemanager.md b/docs/src/timemanager.md deleted file mode 100644 index f69eac74..00000000 --- a/docs/src/timemanager.md +++ /dev/null @@ -1,16 +0,0 @@ -# TimeManager - -This module contains functions that handle dates and times -in simulations. The functions in this module often call -functions from Julia's [Dates](https://docs.julialang.org/en/v1/stdlib/Dates/) module. - -## TimeManager API - -```@docs -ClimaUtilities.TimeManager.to_datetime -ClimaUtilities.TimeManager.strdate_to_datetime -ClimaUtilities.TimeManager.datetime_to_strdate -ClimaUtilities.TimeManager.trigger_callback -ClimaUtilities.TimeManager.Monthly -ClimaUtilities.TimeManager.EveryTimestep -``` diff --git a/src/CallbackManager.jl b/src/CallbackManager.jl new file mode 100644 index 00000000..3afde8e1 --- /dev/null +++ b/src/CallbackManager.jl @@ -0,0 +1,157 @@ +""" + CallbackManager + +This module facilitates calendar functions and temporal interpolations +of data. +""" +module CallbackManager + +import Dates + +export HourlyCallback, + MonthlyCallback, + Monthly, + EveryTimestep, + trigger_callback!, + to_datetime, + strdate_to_datetime, + datetime_to_strdate + +""" + AbstractCallback +""" +abstract type AbstractCallback end + +""" + HourlyCallback{FT} + +This is a callback type that triggers at intervals of 1h or multiple hours. +""" +@kwdef struct HourlyCallback{FT} <: AbstractCallback + """ Time interval at which the callback is triggered. """ + dt::FT = FT(1) # hours + """ Function to be called at each trigger. """ + func::Function = do_nothing + """ Reference date when the callback should be called. """ + ref_date::Array = [Dates.DateTime(0)] + """ Whether the callback is active. """ + active::Bool = false + """ Data to be passed to the callback function. """ + data::Array = [] +end + +""" + MonthlyCallback{FT} + +This is a callback type that triggers at intervals of 1 month or multiple months. +""" +@kwdef struct MonthlyCallback{FT} <: AbstractCallback + """ Time interval at which the callback is triggered. """ + dt::FT = FT(1) # months + """ Function to be called at each trigger. """ + func::Function = do_nothing + """ Reference date for the callback. """ + ref_date::Array = [Dates.DateTime(0)] + """ Whether the callback is active. """ + active::Bool = false + """ Data to be passed to the callback function. """ + data::Array = [] +end + +""" + dt_cb(cb::HourlyCallback) + dt_cb(cb::MonthlyCallback) + +This function returns the time interval for the callback. +""" +dt_cb(cb::HourlyCallback) = Dates.Hour(cb.dt) +dt_cb(cb::MonthlyCallback) = Dates.Month(cb.dt) + + +""" + AbstractFrequency + +This is an abstract type for the frequency of a callback function. +""" +abstract type AbstractFrequency end +struct Monthly <: AbstractFrequency end +struct EveryTimestep <: AbstractFrequency end + +""" + trigger_callback!(callback, date_current) + +If the callback is active and the current date is equal to or later than the +"next call" reference date/time, call the callback function and increment the +next call date based on the callback frequency. Otherwise, do nothing and leave +the next call date unchanged. + +Note that the collection of data in `callback.data` must match the types, number, +and orderof arguments expected by `callback.func`. +""" +function trigger_callback!(callback::HourlyCallback, date_current) + if callback.active && date_current >= callback.ref_date[1] + callback.func(callback.data...) + callback.ref_date[1] += Dates.Hour(1) + end +end + +function trigger_callback!(callback::MonthlyCallback, date_current) + if callback.active && date_current >= callback.ref_date[1] + callback.func(callback.data...) + callback.ref_date[1] += Dates.Month(1) + end +end + +""" + to_datetime(date) + +Convert a `DateTime`-like object (e.g. `DateTimeNoLeap`) to a `DateTime`. +We need this since some data files we use contain +`DateTimeNoLeap` objects for dates, which can't be used for math with `DateTime`s. +The `DateTimeNoLeap` type uses the Gregorian calendar without leap years, while +the `DateTime` type uses Gregorian calendar with leap years. + +For consistency, all input data files should have dates converted to `DateTime` +before being used in a simulation. + +This function is similar to `reinterpret` in CFTime.jl. + +# Arguments +- `date`: `DateTime`-like object to be converted to `DateTime` +""" +function to_datetime(date) + return Dates.DateTime( + Dates.year(date), + Dates.month(date), + Dates.day(date), + Dates.hour(date), + Dates.minute(date), + Dates.second(date), + Dates.millisecond(date), + ) +end + +""" + strdate_to_datetime(strdate::String) + +Convert from String ("YYYYMMDD") to Date format, +required by the official AMIP input files. +""" +strdate_to_datetime(strdate::String) = Dates.DateTime( + parse(Int, strdate[1:4]), + parse(Int, strdate[5:6]), + parse(Int, strdate[7:8]), +) + +""" + datetime_to_strdate(datetime::Dates.DateTime) + +Convert from DateTime to String ("YYYYMMDD") format. +""" +datetime_to_strdate(datetime::Dates.DateTime) = + string(lpad(Dates.year(datetime), 4, "0")) * + string(string(lpad(Dates.month(datetime), 2, "0"))) * + string(lpad(Dates.day(datetime), 2, "0")) + + +end # module CallbackManager diff --git a/src/ClimaUtilities.jl b/src/ClimaUtilities.jl index 5063558a..6278db57 100644 --- a/src/ClimaUtilities.jl +++ b/src/ClimaUtilities.jl @@ -2,7 +2,7 @@ module ClimaUtilities include("Utils.jl") include("MPIUtils.jl") -include("TimeManager.jl") +include("CallbackManager.jl") include("DataStructures.jl") include("FileReaders.jl") include("Regridders.jl") diff --git a/src/TimeManager.jl b/src/TimeManager.jl deleted file mode 100644 index 7f129c01..00000000 --- a/src/TimeManager.jl +++ /dev/null @@ -1,108 +0,0 @@ -""" - TimeManager - -This module facilitates calendar functions and temporal interpolations -of data. -""" -module TimeManager - -import Dates - -export to_datetime, - strdate_to_datetime, - datetime_to_strdate, - trigger_callback, - Monthly, - EveryTimestep - -""" - to_datetime(date) - -Convert a `DateTime`-like object (e.g. `DateTimeNoLeap`) to a `DateTime`. -We need this since some data files we use contain -`DateTimeNoLeap` objects for dates, which can't be used for math with `DateTime`s. -The `DateTimeNoLeap` type uses the Gregorian calendar without leap years, while -the `DateTime` type uses Gregorian calendar with leap years. - -For consistency, all input data files should have dates converted to `DateTime` -before being used in a simulation. - -This function is similar to `reinterpret` in CFTime.jl. - -# Arguments -- `date`: `DateTime`-like object to be converted to `DateTime` -""" -function to_datetime(date) - return Dates.DateTime( - Dates.year(date), - Dates.month(date), - Dates.day(date), - Dates.hour(date), - Dates.minute(date), - Dates.second(date), - Dates.millisecond(date), - ) -end - -""" - strdate_to_datetime(strdate::String) - -Convert from String ("YYYYMMDD") to Date format, -required by the official AMIP input files. -""" -strdate_to_datetime(strdate::String) = Dates.DateTime( - parse(Int, strdate[1:4]), - parse(Int, strdate[5:6]), - parse(Int, strdate[7:8]), -) - -""" - datetime_to_strdate(datetime::Dates.DateTime) - -Convert from DateTime to String ("YYYYMMDD") format. -""" -datetime_to_strdate(datetime::Dates.DateTime) = - string(lpad(Dates.year(datetime), 4, "0")) * - string(string(lpad(Dates.month(datetime), 2, "0"))) * - string(lpad(Dates.day(datetime), 2, "0")) - -abstract type AbstractFrequency end -struct Monthly <: AbstractFrequency end -struct EveryTimestep <: AbstractFrequency end - -""" - trigger_callback(date_nextcall::Dates.DateTime, - date_current::Dates.DateTime, - ::Monthly, - func::Function,) - -If the current date is equal to or later than the "next call" date at time -00:00:00, call the callback function and increment the next call date by one -month. Otherwise, do nothing and leave the next call date unchanged. - -The tuple of arguments `func_args` must match the types, number, and order -of arguments expected by `func`. - -# Arguments -- `date_nextcall::DateTime` the next date to call the callback function at or after -- `date_current::DateTime` the current date of the simulation -- `save_freq::AbstractFrequency` frequency with which to trigger callback -- `func::Function` function to be triggered if date is at or past the next call date -- `func_args::Tuple` a tuple of arguments to be passed into the callback function -""" -function trigger_callback( - date_nextcall::Dates.DateTime, - date_current::Dates.DateTime, - ::Monthly, - func::Function, - func_args::Tuple, -) - if date_current >= date_nextcall - func(func_args...) - return date_nextcall + Dates.Month(1) - else - return date_nextcall - end -end - -end # module TimeManager diff --git a/test/timemanager.jl b/test/callbackmanager.jl similarity index 73% rename from test/timemanager.jl rename to test/callbackmanager.jl index da5fba1a..34d3e403 100644 --- a/test/timemanager.jl +++ b/test/callbackmanager.jl @@ -1,4 +1,4 @@ -import ClimaUtilities: TimeManager +import ClimaUtilities: CallbackManager import Dates import CFTime using Test @@ -9,9 +9,9 @@ for FT in (Float32, Float64) year = 2001 dt_noleap = CFTime.DateTimeNoLeap(year) dt = Dates.DateTime(year) - @test TimeManager.to_datetime(dt_noleap) == dt + @test CallbackManager.to_datetime(dt_noleap) == dt # In non-leap year, DateTime and DateTimeNoLeap are the same - @test TimeManager.to_datetime(dt_noleap + Dates.Day(365)) == + @test CallbackManager.to_datetime(dt_noleap + Dates.Day(365)) == dt + Dates.Day(365) # Test leap year behavior @@ -19,26 +19,26 @@ for FT in (Float32, Float64) dt_noleap_ly = CFTime.DateTimeNoLeap(leap_year) dt_ly = Dates.DateTime(leap_year) # DateTime includes leap days, DateTimeNoLeap does not, so DateTime has one extra day in leap year - @test TimeManager.to_datetime(dt_noleap_ly + Dates.Day(365)) == + @test CallbackManager.to_datetime(dt_noleap_ly + Dates.Day(365)) == dt_ly + Dates.Day(366) end @testset "test strdate_to_datetime for FT=$FT" begin - @test TimeManager.strdate_to_datetime("19000101") == + @test CallbackManager.strdate_to_datetime("19000101") == Dates.DateTime(1900, 1, 1) - @test TimeManager.strdate_to_datetime("00000101") == + @test CallbackManager.strdate_to_datetime("00000101") == Dates.DateTime(0, 1, 1) end @testset "test datetime_to_strdate for FT=$FT" begin - @test TimeManager.datetime_to_strdate(Dates.DateTime(1900, 1, 1)) == + @test CallbackManager.datetime_to_strdate(Dates.DateTime(1900, 1, 1)) == "19000101" - @test TimeManager.datetime_to_strdate(Dates.DateTime(0, 1, 1)) == + @test CallbackManager.datetime_to_strdate(Dates.DateTime(0, 1, 1)) == "00000101" end - @testset "test trigger_callback for FT=$FT" begin + @testset "test trigger_callback! for FT=$FT" begin # Define callback function func! = (val) -> val[1] += 1 # Case 1: date_current == date_nextcall @@ -47,10 +47,10 @@ for FT in (Float32, Float64) arg_copy = copy(arg) date_current = date_nextcall = date_nextcall_copy = Dates.DateTime(1979, 3, 21) - date_nextcall = TimeManager.trigger_callback( + date_nextcall = CallbackManager.trigger_callback!( date_nextcall, date_current, - TimeManager.Monthly(), + CallbackManager.Monthly(), func!, (arg,), ) @@ -61,10 +61,10 @@ for FT in (Float32, Float64) # Case 2: date_current > date_nextcall date_nextcall = date_nextcall_copy = Dates.DateTime(1979, 3, 21) date_current = date_nextcall + Dates.Day(1) - date_nextcall = TimeManager.trigger_callback( + date_nextcall = CallbackManager.trigger_callback!( date_nextcall, date_current, - TimeManager.Monthly(), + CallbackManager.Monthly(), func!, (arg,), ) @@ -75,10 +75,10 @@ for FT in (Float32, Float64) # Case 3: date_current < date_nextcall date_nextcall = date_nextcall_copy = Dates.DateTime(1979, 3, 21) date_current = date_nextcall - Dates.Day(1) - date_nextcall = TimeManager.trigger_callback( + date_nextcall = CallbackManager.trigger_callback!( date_nextcall, date_current, - TimeManager.Monthly(), + CallbackManager.Monthly(), func!, (arg,), ) diff --git a/test/runtests.jl b/test/runtests.jl index beeb92bc..1b983489 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -28,8 +28,8 @@ end include("output_path_generator.jl") end -@safetestset "TimeManager tests" begin - include("timemanager.jl") +@safetestset "CallbackManager tests" begin + include("callbackmanager.jl") end @safetestset "DataStructures tests" begin