Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TimeManager module #9

Merged
merged 1 commit into from
Sep 26, 2023
Merged
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
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"coupled simulations" may be more accurate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these functions (e.g. to_datetime) are used in the bucket model in ClimaLSM, which can be run standalone or in a coupled simulation, so I think it makes sense to keep the comment more general

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
```
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,
juliasloan25 marked this conversation as resolved.
Show resolved Hide resolved
)
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