Skip to content

Commit

Permalink
Add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Sbozzolo committed Apr 3, 2024
1 parent c53d282 commit e0735df
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/Documentation.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Documentation

on:
pull_request:
push:
branches:
- main
pull_request:
tags: '*'

concurrency:
Expand Down
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ ClimaDiagnostics = "1ecacbb8-0713-4841-9a07-eb5aa8a2d53f"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"

[compat]
ClimaDiagnostics = "0.1"
ClimaDiagnostics = "0.0.1"
Documenter = "0.27"
4 changes: 3 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Documenter
using ClimaDiagnostics

pages = ["Overview" => "index.md"]
pages = ["Overview" => "index.md",
"How to add ClimaDiagnostics to a package" => "developer_guide.md",
"Internals" => "internals.md"]

mathengine = MathJax(
Dict(
Expand Down
306 changes: 306 additions & 0 deletions docs/src/developer_guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
# I am a developer, how do I add `ClimaDiagnostics.jl` to my package?

Before reading this section, make sure you are familiar with the terminology.
You know to know what a [`DiagnosticVariable`](@ref) and a
[`ScheduledDiagnostic`](@ref) are.

There are three components needed to add support for `ClimaDiagnostics.jl` in your package.

1. A way to convert users' intentions to a list of [`ScheduledDiagnostic`](@ref)
2. A call to [`DiagnosticsHandler`](@ref) after the initial state `Y` and cache
`p` are prepared. This initializes the diagnostics and creates an object
`DiagnosticsHandler`.
3. Adding `DiagnosticsCallback(DiagnosticsHandler)` to the `SciML` integrator.

## An example for steps 2. and 3.

Let us assume that `scheduled_diagnostics` is the list of `ScheduledDiagnostic`s
obtained from step 1. (more on this later), `Y` is the simulation initial state,
`p`, the cache, `t0` the initial time, and `dt` the timestep.

Schematically, what we need to do is
```julia
import ClimaDiagnostics: DiagnosticsHandler, DiagnosticsCallback

# Initialize the diagnostics, can be expensive
diagnostic_handler = ClimaDiagnostics.DiagnosticsHandler(
scheduled_diagnostics,
Y,
p,
t0;
dt,
)

# Prepare the SciML callback
diag_cb = ClimaDiagnostics.DiagnosticsCallback(diagnostic_handler)

SciMLBase.init(args...; kwargs..., callback = diag_cb)
```
with `args` and `kwargs` the argument and keyword argument needed to set up the
target simulation.

In `DiagnosticsHandler`, `dt` is used exclusively for consistency checks.
Suppose your timestep is `50s` and you request a variable to be output every
`70s`, if you pass `dt`, `DiagnosticsHandler` will catch this and error out. If
you don't pass `dt`, `DiagnosticsHandler` will warn you about that.

Creating a `DiagnosticsHandler` results in calling all the diagnostics once.
Therefore, the compile and runtime of this function can be significant if you
have a large number of diagnostics.

You can learn about what is happening under the hook in the [Internals](@ref)
page.

This is pretty much all that you need to know about steps 2 and 3!

## Step 1

Step 1 in the recipe to bring `ClimaDiagnostics` to your package strongly
depends on you.

In this section, I will present a tower of interfaces that you can put in place
to make it more convenient for your users. Keep in mind that each layer trades
more convenience for less flexibility. So, as you set up your interfaces, I
recommend you keep them exposed so that your users can access lower-level
functions if they need to.

### Level 0: do nothing

At the zero-th level, you let your users work directly with `ClimaDiagnostics`.
This means that they will have to define their own `DiagnosticVariable`s and
`ScheduledDiagnostic`s. This also requires that your simulation is executed as a
julia script.

It is a good idea for your users to be aware of this possibility because it
brings enormous power. `ScheduledDiagnostic`s can be triggered on arbitrary
conditions, and your users could be creative with that. For example, users might
want to compute and output a variable `var1` when they find that the maximum of
variable `var2` is greater than a threshold (e.g., for debugging).

Let us see the simplest example to accomplish this
```julia
import ClimaDiagnostics: DiagnosticVariable, ScheduledDiagnostic
import ClimaDiagnostics.Writers: DictWriter

myvar = DiagnosticVariable(; compute! = (out, u, p, t) -> u.var1)

myschedule = (integrator) -> maximum(integrator.u.var2) > 10.0

diag = ScheduledDiagnostic(variable = myvar,
compute_schedule_func = myschedule,
output_schedule_func = myschedule,
output_writer = DictWriter())
```

Now we can go to step 2 and 3 in the previous list and pass `[diag]` to the
`DiagnosticsHandler`.

Point your users to the documentation of this package for them to learn how to
use it in its full power.

### Level 1: provide a database of `DiagnosticVariable`s

As a package developer, you know that there is a large collection of variables
that several users will be interested in. For example, if you are running an
atmospheric simulation, your users will want to be able to look at the air
temperature. For this reason, it is a very good (and user-friendly) idea to
provide a collection of `DiagnosticVariable`s ready to be used. In this section,
I sketch how you could go about and implement this.

Your `DiagnosticVariable`s database can be represented as a dictionary
`ALL_DIAGNOSTICS` indexed over the short name of the variable. Then, you could
provide adders and accessors.

This might look like the following:
```julia
module Diagnostics
import ClimaDiagnostics: DiagnosticVariable

const ALL_DIAGNOSTICS = Dict{String, DiagnosticVariable}()

"""
add_diagnostic_variable!(; short_name,
long_name,
standard_name,
units,
description,
compute!)
Add a new variable to the `ALL_DIAGNOSTICS` dictionary (this function mutates the state of
`ALL_DIAGNOSTICS`).
If possible, please follow the naming scheme outline in
https://airtable.com/appYNLuWqAgzLbhSq/shrKcLEdssxb8Yvcp/tblL7dJkC3vl5zQLb
Keyword arguments
=================
- `short_name`: Name used to identify the variable in the output files and in the file
names. Short but descriptive. Diagnostics are identified by the short name.
- `long_name`: Name used to identify the variable in the output files.
- `standard_name`: Standard name, as in
http://cfconventions.org/Data/cf-standard-names/71/build/cf-standard-name-table.html
- `units`: Physical units of the variable.
- `comments`: More verbose explanation of what the variable is, or comments related to how
it is defined or computed.
- `compute!`: Function that compute the diagnostic variable from the state. It has to take
two arguments: the `integrator`, and a pre-allocated area of memory where to
write the result of the computation. It the no pre-allocated area is
available, a new one will be allocated. To avoid extra allocations, this
function should perform the calculation in-place (i.e., using `.=`).
"""
function add_diagnostic_variable!(;
short_name,
long_name,
standard_name = "",
units,
comments = "",
compute!,
)
haskey(ALL_DIAGNOSTICS, short_name) && @warn(
"overwriting diagnostic `$short_name` entry containing fields\n" *
"$(map(
field -> "$(getfield(ALL_DIAGNOSTICS[short_name], field))",
# We cannot really compare functions...
filter(field -> field != :compute!, fieldnames(DiagnosticVariable)),
))"
)

ALL_DIAGNOSTICS[short_name] = DiagnosticVariable(;
short_name,
long_name,
standard_name,
units,
comments,
compute!,
)

"""
get_diagnostic_variable!(short_name)
Return a `DiagnosticVariable` from its `short_name`, if it exists.
"""
function get_diagnostic_variable(short_name)
haskey(ALL_DIAGNOSTICS, short_name) ||
error("diagnostic $short_name does not exist")

return ALL_DIAGNOSTICS[short_name]
end

end
```
Of course, you should have the fields and comments that are relevant to your package.

Next, as a developer, you will use `add_diagnostic_variable!` to populate your
database. You can also expose your users to this function so that they can
extend their personal database in their simulations.

A simple example of a new variable might look like
```julia
###
# Density (3d)
###
add_diagnostic_variable!(
short_name = "rhoa",
long_name = "Air Density",
standard_name = "air_density",
units = "kg m^-3",
compute! = (out, state, cache, time) -> begin
if isnothing(out)
return copy(state.c.ρ)
else
out .= state.c.ρ
end
end,
)
```

It is a good idea to put safeguards in place to ensure that your users will not
be allowed to call diagnostics that do not make sense for the simulation they
are running. If your package has a notion of `Model` that is stored in `p`, you
can dispatch over that and return an error. A simple example might be
```julia
###
# Specific Humidity
###
compute_hus!(out, state, cache, time) =
compute_hus!(out, state, cache, time, cache.atmos.moisture_model)

compute_hus!(out, state, cache, time) =
compute_hus!(out, state, cache, time, cache.model.moisture_model)
compute_hus!(_, _, _, _, model::T) where {T} =
error("Cannot compute hus with $model")

function compute_hus!(
out,
state,
cache,
time,
moisture_model::T,
) where {T <: Union{EquilMoistModel, NonEquilMoistModel}}
if isnothing(out)
return state.c.ρq_tot ./ state.c.ρ
else
out .= state.c.ρq_tot ./ state.c.ρ
end
end

add_diagnostic_variable!(
short_name = "hus",
long_name = "Specific Humidity",
standard_name = "specific_humidity",
units = "kg kg^-1",
comments = "Mass of all water phases per mass of air",
compute! = compute_hus!,
)
```
This relies on dispatching over `moisture_model`. If `model` is not in
`Union{EquilMoistModel, NonEquilMoistModel}`, the code returns an informative error.

If you provide a database, users can create their `ScheduledDiagnostic`s
directly from the `DiagnosticVariable`s you provided.

For instance to output the specific humidity every 5 iterations:
```julia
import ClimaDiagnostics: ScheduledDiagnostic
import ClimaDiagnostics.Callbacks: DivisorSchedule
import ClimaDiagnostics.Writers: DictWriter

diag = ScheduledDiagnostic(variable = get_diagnostic_variable!("hus"),
output_schedule_func = DivisorSchedule(5),
output_writer = DictWriter())
```

Alongside with providing the `DiagnosticVariable`s, you can also provide
convenience functions for standard operations.

For example, you could provide
```julia
using ClimaDiagnostics.Callbacks: EveryStepSchedule, EveryDtSchedule

function monthly_average(short_name; output_writer, t_start)
period = 30 * 24 * 60 * 60 * one(t_start)
return ScheduledDiagnostic(
variable = get_diagnostic_variable(short_name),
compute_schedule_func = EveryStepSchedule(),
output_schedule_func = EveryDtSchedule(period; t_start),
reduction_time_func = (+),
output_writer = output_writer,
pre_output_hook! = average_pre_output_hook!,
)
end
```
Allowing users to just call `monthly_average("hus", writer, t_start)`.

> Note: `ClimaDiagnostics` will probably provided these schedules natively at
> some point in the future.
### Level 2: provide a database of `DiagnosticVariable`s
6 changes: 5 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# `ClimaDiagnostics.jl`

`ClimaDiagnostics.jl`
`ClimaDiagnostics.jl` is a module that adds diagnostics to your
[`CliMA`](https://github.com/CliMA) simulations.

## Features

- Trigger diagnostics on arbitrary conditions

## Quick guide
3 changes: 1 addition & 2 deletions docs/src/internals.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Internals

## Accumulation

Expand All @@ -13,8 +14,6 @@ does is applying the binary `reduction_time_func` to the previous accumulated
value and the newly computed one and store the output to the accumulator.




After an accumulated variable is output, the accumulator is reset to its natural
state. This is achieved with the `reset_accumulator!` function.

Expand Down

0 comments on commit e0735df

Please sign in to comment.