-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit introduces the `ITime` type, a new approach to handling time and dates within CliMA simulations. `ITime` utilizes integer-based representation and operations, offering three advantages: - Eliminates floating-point errors. - Provides a trivial way to go from times to dates. - Provides an abstraction layer over the calendar system, enabling future support for calendars beyond the Gregorian calendar currently used by Dates. `ITime` is defined by a `counter`, a `period`, and an optional `start_date`. The counter tracks the number of elapsed periods since the start date and the period is customizable (1 second by default). `ITime`s also support fractional counters (currently represented by `Rational`s) to account for stages in the timestepper loop. `ITime`s support the arithmetic operations (+, -, *, /, div) while maintaining consistency in the units and the start date. This allows users to just specify `start_date` and `period` for one of their `ITime`s (e.g., `t_start`), and everything will be automatically propagated. Given an `ITime` `t`, one can obtain its time with `seconds(t)` and the date with `date(t)`. I also added `float(t)` to help with the transition.
- Loading branch information
Showing
8 changed files
with
765 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,218 @@ | ||
# 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` defines `ITime`, the time type for CliMA simulations, alongside | ||
with various functions to work with it. | ||
|
||
## TimeManager API | ||
## `ITime` | ||
|
||
```@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 | ||
`ITime` is a type to describe times and dates. The "I" in `ITime` stands for | ||
_integer_: internally, `ITime` uses integers to represent times and dates, | ||
meaning that operations with `ITime` are exact and do not occur in | ||
floating-point errors. | ||
|
||
`ITime` can be thought a combination of three quantities, a `counter`, a | ||
`period`, and (optionally) a `start_date`, with the `counter` counting how many | ||
`period`s have elapsed since `start_date`. In other words, `ITime` counts clock | ||
cycles from an epoch, with each clock cycle lasting a `period`. | ||
|
||
Another useful mental model for `ITime` is that it is a time with some units. It | ||
is useful to keep this abstraction in mind when working with binary operations | ||
that involve `ITime`s because it helps with determining the output type of the | ||
operation (more on this later). | ||
|
||
Let us start getting familiar with `ITime` by exploring some basic operations. | ||
|
||
### First steps with `ITime` | ||
|
||
The first step in using `ITime` is to load it. You can load `ITime` by loading | ||
the `TimeManager` module as in the following: | ||
|
||
```@example example1 | ||
using ClimaUtilities.TimeManager | ||
``` | ||
|
||
This will load `ITime` as well as some other utilities. If you only wish to load | ||
`ITime`, you can change `using` with `import` and explicitly ask for `ITime` | ||
|
||
```@julia example | ||
import ClimaUtilities.TimeManager: ITime | ||
``` | ||
|
||
In these examples, we will stick with `using ClimaUtilites.TimeManager` because | ||
we will call other functions as well. | ||
|
||
By default, `ITime` assumes that the clock cycle is one second: | ||
```@example example1 | ||
ITime(5) | ||
``` | ||
The output that is printed shows the time represented (5 seconds), and its break | ||
down into the integer counter (5) and the duration of each clock cycle (1 | ||
second). | ||
|
||
This `ITime` does not come with any date information attached to it. To add it, | ||
you can pass the `start_date` keyword | ||
```@example example1 | ||
using Dates | ||
ITime(5; start_date = DateTime(2012, 12, 21)) | ||
``` | ||
Now, the output also reports `start_date` and current date (5 seconds after | ||
midnight of the 21st of December 2012). | ||
|
||
The period can also be customized with the `period` keyword. This keyword | ||
accepts any `Dates.FixedPeriod` (fixed periods are intervals of time that have a | ||
fixed duration, such as `Week`, but unlike `Month`), for example | ||
```@example example1 | ||
time1 = ITime(5; period = Hour(20), start_date = DateTime(2012, 12, 21)) | ||
``` | ||
All these quantities are accessible with functions: | ||
```@example example1 | ||
@show "time1" counter(time1) period(time1) start_date(time1) date(time1) | ||
``` | ||
|
||
Now that we know how to create `ITime`s, we can manipulate them. `ITime`s | ||
support most (but not all) arithmetic operations. | ||
|
||
For `ITime`s with a `start_date`, it is only possible to combine `ITime`s that | ||
have the same `start_date`. `ITime`s can be composed to other times and | ||
arithmetic operations propagate `start_date`s. | ||
|
||
This works: | ||
```@example example1 | ||
time1 = ITime(5; start_date = DateTime(2012, 12, 21)) | ||
time2 = ITime(15; start_date = DateTime(2012, 12, 21)) | ||
time1 + time2 | ||
``` | ||
This works too | ||
```@example example1 | ||
time1 = ITime(5; start_date = DateTime(2012, 12, 21)) | ||
time2 = ITime(15) | ||
time1 + time2 | ||
``` | ||
This does not work because the two `ITimes` don't have the same `start_date` | ||
```@example example1 | ||
time1 = ITime(5; start_date = DateTime(2012, 12, 21)) | ||
time2 = ITime(15; start_date = DateTime(2025, 12, 21)) | ||
time1 + time2 | ||
``` | ||
|
||
When dealing with binary operations, it is convenient to think of `ITimes` as | ||
dimensional quantities (quantities with units). In the previous examples, | ||
addition returns a new `ITimes`. Division will behave differently | ||
```@example example1 | ||
time1 = ITime(15) | ||
time2 = ITime(5) | ||
time1 / time2 | ||
``` | ||
In this case, the return value is just a number (essentially, we divided 15 | ||
seconds by 5 seconds, resulting the dimensionless factor of 3). | ||
|
||
Similarly, adding a number to an `ITime` is not allowed (because the number | ||
doesn't have units), but multiplying by is fine. | ||
|
||
In the same spirit, when `ITime`s with different `periods` are combined, units | ||
are transformed to the | ||
```@example example1 | ||
time1 = ITime(5; period = Hour(1)) | ||
time2 = ITime(1; period = Minute(25)) | ||
time1 - time2 | ||
``` | ||
As we can see, the return value of 5 hours - 25 minutes is 275 minutes and it is | ||
represented as 55 clock cycles of a period of 5 minutes. `Minute(5)` was picked | ||
because it the greatest common divisor between `Hour(1)` and `Minute(25)`. | ||
|
||
At any point, we can obtain a date or a number of seconds from an `ITime`: | ||
```@example example1 | ||
time1 = ITime(5; period = Day(10), start_date = DateTime(2012, 12, 21)) | ||
date(time1) | ||
``` | ||
And | ||
```@example example1 | ||
time1 = ITime(5; period = Day(10), start_date = DateTime(2012, 12, 21)) | ||
seconds(time1) | ||
``` | ||
In this, note that the `seconds` function always returns a Float64. | ||
|
||
In this section, we saw that `ITime`s can be used to represent dates and times | ||
and manipulated in a natural way. | ||
|
||
`ITime`s support another feature, fractional times, useful for further | ||
partitioning an interval of time. | ||
|
||
### Fractional times | ||
|
||
Sometimes, one need to work with fractions of a `period`. The primary | ||
application of this is timestepping loops, which are typically divided in stages | ||
which are a fraction of a timestep. | ||
|
||
`ITime`s can represent fractions of a `period` using rational numbers. For | ||
example | ||
```@example example1 | ||
time1 = ITime(5) | ||
time1 / 2 | ||
``` | ||
Here, we can see that the counter is now `5//2` (the fraction 5 divided by 2). | ||
Only integer and rational counters are allowed. | ||
|
||
`ITime`s with fractional counters largely behave as `ITime`s with integer | ||
counters with the exception that it is generally not possible to covert them to | ||
dates without rounding. Given that Julia's Dates follow the UNIX representation | ||
by being encoded with the number of milliseconds from an epoch, we also truncate | ||
fractions to milliseconds when converting to dates. | ||
|
||
!!! note | ||
|
||
The current implementation of fractional times uses the `Rational` module in the | ||
standard library. This might have performance implications. Please, open an | ||
issue if you have any. | ||
|
||
### How to use `ITime`s in packages | ||
|
||
The two key interfaces to work with `ITime`s are [`seconds`](@ref) and | ||
[`date`](@ref), which respectively return the time as Float64 number of seconds | ||
and the associated `DateTime`. | ||
|
||
If you want to support `AbstractFloat`s and `ITime`s, some useful functions are | ||
`float` (which returns the number of seconds as a Float64), `zero`, `one`, and | ||
`oneunit`. The difference between `one` and `oneunit` is that the latter returns | ||
a `ITime` type, while the former returns a number. This is not what happens with | ||
`zero`, which returns an `ITime`. | ||
|
||
You might have the temptation to just sprinkle `float` everywhere to transition | ||
your code to using `ITime`s. Resist to this temptation because it might defy the | ||
purpose of using integer times in the first place. | ||
|
||
For typical codes and simulation, we recommend only setting `t_start` and | ||
`t_end` with a `start_date`: the `start_date` will be propagated naturally to | ||
all the other times involved in the calculations. | ||
|
||
Regarding the question on what `period` to use: if there are natural periods | ||
(e.g., you are dealing with an hourly diagnostic variable), use it, otherwise | ||
you can stick with the default. The `period` can always be changed by setting it | ||
in `t_start` and `t_end`. | ||
|
||
We provide a constructor from floating point numbers to assist you in | ||
transitioning your package to using `ITime`s. This constructor guesses and uses | ||
the largest period that can be used to properly represent the data. | ||
```@example example1 | ||
ITime(60.0) | ||
``` | ||
|
||
## Developer notes | ||
|
||
### Why not using `Dates` directly? | ||
|
||
Periods in Julia's `Dates` are also implemented as integer counters attached to | ||
a type that encode its units. So why not using them directly? | ||
|
||
There are a few reasons why `ITime` is preferred over Julia's `Dates`: | ||
- `Dates` do not support fractional intervals, which are needed for the | ||
timestepping loop; | ||
- `Dates` only support the Gregorian calendar. `ITime` provides an abstraction | ||
layer that will allow us to support other calendars without changing packages; | ||
- `Dates` only allow the given periods, but it often natural to pick the | ||
simulation timestep as `period`; | ||
- Julia's `Dates` are not necessarily faster or better and, being part of the | ||
standard library, means that it is hard to improve them. | ||
|
||
|
||
|
Oops, something went wrong.