-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
317 additions
and
6 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
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: | ||
|
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
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 |
---|---|---|
@@ -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 |
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,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 |
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