Skip to content

Commit

Permalink
TimeManager module implemented, tested
Browse files Browse the repository at this point in the history
  • Loading branch information
juliasloan25 committed Sep 20, 2023
1 parent 3afad31 commit 8574bbb
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 3 deletions.
5 changes: 5 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ uuid = "b3f4f4ca-9299-4f7f-bd9b-81e1242a7513"
authors = ["Julia Sloan <jsloan@caltech.edu>"]
version = "0.1.1"

[deps]
CFTime = "179af706-886a-5703-950a-314cd64e0468"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"

[compat]
CFTime = "0.1"
julia = "1.8"
7 changes: 7 additions & 0 deletions docs/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ version = "0.0.1"
[[deps.Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[deps.CFTime]]
deps = ["Dates", "Printf"]
git-tree-sha1 = "ed2e76c1c3c43fd9d0cb9248674620b29d71f2d1"
uuid = "179af706-886a-5703-950a-314cd64e0468"
version = "0.1.2"

[[deps.ClimaUtilities]]
deps = ["CFTime", "Dates"]
path = ".."
uuid = "b3f4f4ca-9299-4f7f-bd9b-81e1242a7513"
version = "0.1.1"
Expand Down
3 changes: 1 addition & 2 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using Documenter, Example, Literate
using ClimaUtilities

# TODO fill in once we have doc pages
pages = Any[]
pages = ["timemanager.md"]

mathengine = MathJax(
Dict(
Expand Down
16 changes: 16 additions & 0 deletions docs/src/timemanager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 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
ClimaCoupler.TimeManager.to_datetime
ClimaCoupler.TimeManager.strdate_to_datetime
ClimaCoupler.TimeManager.datetime_to_strdate
ClimaCoupler.TimeManager.trigger_callback
ClimaCoupler.TimeManager.Monthly
ClimaCoupler.TimeManager.EveryTimestep
```
2 changes: 1 addition & 1 deletion src/ClimaUtilities.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module ClimaUtilities

greet() = print("Hello World!")
include("TimeManager.jl")

end # module ClimaUtilities
97 changes: 97 additions & 0 deletions src/TimeManager.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
TimeManager
This module facilitates calendar functions and temporal interpolations
of data.
"""
module TimeManager

import Dates
import CFTime

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`,
using CFTime.jl. 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.
# Arguments
- `date`: `DateTime`-like object to be converted to `DateTime`
"""
to_datetime(date) = CFTime.reinterpret(Dates.DateTime, date)

"""
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
2 changes: 2 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[deps]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
CFTime = "179af706-886a-5703-950a-314cd64e0468"
ClimaUtilities = "b3f4f4ca-9299-4f7f-bd9b-81e1242a7513"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

Expand Down
5 changes: 5 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ using SafeTestsets
@safetestset "Aqua tests" begin
include("aqua.jl")
end

# Unit tests
@safetestset "TimeManager tests" begin
include("timemanager.jl")
end
89 changes: 89 additions & 0 deletions test/timemanager.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import ClimaUtilities: TimeManager
import Dates
import CFTime
using Test

for FT in (Float32, Float64)
@testset "test to_datetime for FT=$FT" begin
# Test non-leap year behavior
year = 2001
dt_noleap = CFTime.DateTimeNoLeap(year)
dt = Dates.DateTime(year)
@test TimeManager.to_datetime(dt_noleap) == dt
# In non-leap year, DateTime and DateTimeNoLeap are the same
@test TimeManager.to_datetime(dt_noleap + Dates.Day(365)) ==
dt + Dates.Day(365)

# Test leap year behavior
leap_year = 2000
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)) ==
dt_ly + Dates.Day(366)

end

@testset "test strdate_to_datetime for FT=$FT" begin
@test TimeManager.strdate_to_datetime("19000101") ==
Dates.DateTime(1900, 1, 1)
@test TimeManager.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)) ==
"19000101"
@test TimeManager.datetime_to_strdate(Dates.DateTime(0, 1, 1)) ==
"00000101"
end

@testset "test trigger_callback for FT=$FT" begin
# Define callback function
func! = (val) -> val[1] += 1
# Case 1: date_current == date_nextcall
# Define list for arg so we can mutate it in `func!`
arg = [FT(0)]
arg_copy = copy(arg)
date_current =
date_nextcall = date_nextcall_copy = Dates.DateTime(1979, 3, 21)
date_nextcall = TimeManager.trigger_callback(
date_nextcall,
date_current,
TimeManager.Monthly(),
func!,
(arg,),
)
# Test that cutoff date was updated and `func!` got called
@test date_nextcall == date_nextcall_copy + Dates.Month(1)
@test arg[1] == func!(arg_copy)

# 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,
date_current,
TimeManager.Monthly(),
func!,
(arg,),
)
# Test that cutoff date was updated and `func!` got called
@test date_nextcall == date_nextcall_copy + Dates.Month(1)
@test arg[1] == func!(arg_copy)

# 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,
date_current,
TimeManager.Monthly(),
func!,
(arg,),
)
# Test that cutoff date is unchanged and `func!` did not get called
@test date_nextcall == date_nextcall_copy
@test arg[1] == arg_copy[1]
end
end

0 comments on commit 8574bbb

Please sign in to comment.