Skip to content

Commit

Permalink
Add ITime, an integer time type
Browse files Browse the repository at this point in the history
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
Sbozzolo committed Nov 6, 2024
1 parent 58d8317 commit 000b68b
Show file tree
Hide file tree
Showing 8 changed files with 765 additions and 15 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ ClimaUtilities.jl

</h1>

`ClimaUtilities` is collection of general-purpose tools for CliMA packages.
`ClimaUtilities` is collection of general-purpose tools and types for CliMA packages.

`ClimaUtilities.jl` contains:
- [`ClimaArtifacts`](https://clima.github.io/ClimaUtilities.jl/dev/climaartifacts/),
a module that provides an MPI-safe way to lazily download artifacts and to tag
artifacts that are being accessed in a given simulation.
- [`TimeManager`](https://clima.github.io/ClimaUtilities.jl/dev/timemanager/) to
handle time and dates. `TimeManager` implements `ITime`, the time type used in
CliMA simulations.
- [`SpaceVaryingInputs` and
`TimeVaryingInputs`](https://clima.github.io/ClimaUtilities.jl/dev/inputs/) to
work with external input data.
Expand All @@ -26,8 +29,6 @@ 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
handle dates.

## ClimaUtilities.jl Developer Guidelines

Expand Down
224 changes: 213 additions & 11 deletions docs/src/timemanager.md
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.



Loading

0 comments on commit 000b68b

Please sign in to comment.