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

Update diagnostics docs #743

Merged
merged 2 commits into from
Aug 30, 2024
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
4 changes: 2 additions & 2 deletions docs/list_diagnostics.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
diagnostics = [
"Available diagnostics" => "diagnostics/available_diagnostics.md",
"For developers" => "diagnostics/developers_diagnostics.md",
"For users" => "diagnostics/users_diagnostics.md",
"For developers" => "diagnostics/developers_diagnostics.md",
"Available diagnostics" => "diagnostics/available_diagnostics.md",
]
53 changes: 27 additions & 26 deletions docs/src/diagnostics/developers_diagnostics.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
# ClimaLand Diagnostics: why and how

ClimaLand simulations generates variables in the integrator state and cache at each time step.
ClimaLand simulations generates variables in the integrator state (Y) and cache (p) at each time step.
A user will need to use these variables in some form, i.e., access them from a file that contains variables at a given temporal and spatial resolution.
The user will also want to retrieve metadata about those variables, such as name and units.
This is where ClimaLand diagnostics comes in, it writes simulations variables (in a file, such as NetCDF or HDF5, or in Julia Dict), at a specified spatio-temporal reduction
(e.g., hourly averages, monthly max, instantaneous, integrated through soil depth...), along with metadata (e.g., soil temperature short name is t_soil, expressed in "K" units).
(e.g., hourly averages, monthly max, instantaneous, integrated through soil depth...), along with metadata (e.g., soil temperature short name is t\_soil, expressed in "K" units).
We want to provide users with default options, but also the possibility to define their own variables and reductions.

Internally, this is done by using the [`ClimaDiagnostics.jl`](https://github.com/CliMA/ClimaDiagnostics.jl) package, that provides the functionality to produce a
[`ClimaLand.Diagnostics`](https://github.com/CliMA/ClimaLand.jl/tree/main/src/Diagnostics/Diagnostics.jl) module in the src/Diagnostics.jl folder. In this folder,
- `Diagnostics.jl` defines the module,
- `diagnostic.jl` defines `ALL_DIAGNOSTICS`, a Dict containing all diagnostics variables defined in `define_diagnostics.jl`, it also defines the function
`add_diagnostic_variable!` which defines a method to add diagnostic variables to ALL_DIAGNOSTICS, finally it contains a function `get_diagnostic_variable` which returns a
`DiagnosticVariable` from its `short_name`, if it exists.
- `diagnostic.jl` defines `ALL_DIAGNOSTICS`, a Dict containing all diagnostics variables defined in `define_diagnostics.jl`, it also defines the function
`add_diagnostic_variable!` which defines a method to add diagnostic variables to ALL\_DIAGNOSTICS, finally it contains a function `get_diagnostic_variable` which returns a
`DiagnosticVariable` from its `short_name`, if it exists.
- `define_diagnostics.jl`, mentioned above, contains a function `define_diagnostics!(land_model)` which contains all default diagnostic variables by calling.
`add_diagnostic_variable!`, and dispatch off the type of land_model to define how to compute a diagnostic (for example, surface temperature is computed in `p.bucket.T_sfc` in the bucket model).
- compute methods are defined in a separate file, for example, `bucket_compute_methods.jl`.
- `standard_diagnostic_frequencies.jl` defines standard functions to schedule diagnostics, for example, hourly average or monthly max, these functions are called on a list of diagnostic variables. As developers, we can add more standard functions that users may want to have access to easily in this file.
`add_diagnostic_variable!`, and dispatch off the type of land\_model to define how to compute a diagnostic (for example, surface temperature is computed in `p.bucket.T_sfc` in the bucket model).
- compute methods are defined in a separate file, for example, `bucket_compute_methods.jl`.
- `standard_diagnostic_frequencies.jl` defines standard functions to schedule diagnostics, for example, hourly average or monthly max, these functions are called on a list of diagnostic variables. As developers, we can add more standard functions that users may want to have access to easily in this file.
- `default_diagnostics.jl` defines default diagnostics functions to use on a model simulation. For example, `default_diagnostics(land_model::BucketModel, t_start; output_writer)`.
will return a `ScheduledDiagnostics` that computes hourly averages for all Bucket variables, along with their metadata, ready to be written on a NetCDF file when running a Bucket simulation.

The following section give more details on these functions, along with examples. As developers, we want to extand these functionality as ClimaLand progresses.

# Compute methods

Each model defines all its compute methods in a file (bucket_compute_methods.jl for the bucket model, for example).
Each model defines all its compute methods in a file (bucket\_compute\_methods.jl for the bucket model, for example).
The structure of a diagnostic variable compute method is, for example:
```
function compute_albedo!(out, Y, p, t, land_model::BucketModel)
```Julia
@with_error function compute_albedo!(out, Y, p, t, land_model::BucketModel)
if isnothing(out)
return copy(p.bucket.α_sfc)
else
Expand All @@ -36,25 +36,21 @@ function compute_albedo!(out, Y, p, t, land_model::BucketModel)
end
```

It defines how to access your diagnostic (here, p.bucket.α_sfc), in your model type (here, ::BucketModel).
Note that, as explained in the [ClimaDiagnostics.jl documentation](https://clima.github.io/ClimaDiagnostics.jl/dev/user_guide/), `out` will probably not be needed in the future.

We also define helper functions returning error messages if a user tries to compute a diagnostic variable that doesn't exist in their model type.
It defines how to access your diagnostic (here, p.bucket.α\_sfc) with the land\_model `BucketModel`.
Note that you can also use the @diagnostic\_compute macro to do the same thing:

```Julia
@diagnostic_compute "albedo" BucketModel p.bucket.α\_sfc
```
error_diagnostic_variable(variable, land_model::T) where {T} =
error("Cannot compute $variable with model = $T")

compute_albedo!(_, _, _, _, land_model) =
error_diagnostic_variable("albedo", land_model)
```
The `@with_error` macro define helper functions returning error messages if a user tries to compute a diagnostic variable that doesn't exist in their model type.

# Define diagnostics

Once the compute functions have been defined, they are added to `define_diagnostics!(land_model)`, which adds diagnostics variables to ALL_DIAGNOSTICS dict,
Once the compute functions have been defined, they are added to `define_diagnostics!(land_model)`, which adds diagnostics variables to ALL\_DIAGNOSTICS dict,
defined in diagnostic.jl. In these functions, you also define a `short_name`, `long_name`, `standard_name`, `units` and `comment`. For example:

```
```Julia
add_diagnostic_variable!(
short_name = "alpha",
long_name = "Albedo",
Expand All @@ -66,9 +62,10 @@ add_diagnostic_variable!(

# Default diagnostics

For each model, we define a function `default_diagnostics` which will define what diagnostic variables to compute by default for a specific model, and
For each model, we define a function `default_diagnostics` which will define what diagnostic variables to compute by default for a specific model, and
on what schedule (for example, hourly average). For example,
```

```Julia
function default_diagnostics(land_model::BucketModel, t_start; output_writer)

define_diagnostics!(land_model)
Expand All @@ -95,13 +92,17 @@ function default_diagnostics(land_model::BucketModel, t_start; output_writer)
end
```

is the default for the BucketModel, it will return hourly averages for the variables listed in `bucket_diagnostics` (which are all variables in the BucketModel).
is the default for the BucketModel, it will return hourly averages for the variables listed in `bucket_diagnostics` (which are all variables in the BucketModel).

For the SoilCanopyModel and the SoilModel, we added two keyword arguments: `output_vars` (can be :long or :short) and `average_period` (can be :hourly, :daily, or :monthly).
If `output_vars = :long` (the default), then `soilcanopy_diagnostics` is an Array of all short\_name, if `output_vars = :short`, then `soilcanopy_diagnostics = ["gpp", "ct", "lai", "swc", "si"]`.
If `average_period = :hourly`, `default_outputs` calls `hourly_averages`, et cetera.

# Standard diagnostic frequencies

We defined some functions of diagnostic schedule that may often be used in `standard_diagnostic_frequencies.jl`, for example

```
```Julia
hourly_averages(short_names...; output_writer, t_start) = common_diagnostics(
60 * 60 * one(t_start),
(+),
Expand Down
149 changes: 95 additions & 54 deletions docs/src/diagnostics/users_diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,46 @@ This is where ClimaLand Diagnostics comes in for users.

In this documentation page, we first explain how to use default diagnostics and what are the defaults, and then explain how to define your own diagnostics for more advanced users.

# Default Diagnostics
## Default Diagnostics

Once you have defined your model and are ready to run a simulation, and after adding ClimaDiagnostics (using ClimaDiagnostics),
you can add default diagnostics to it by doing the following steps:

1. define an output folder
### define an output folder

```
```Julia
output_dir = ClimaUtilities.OutputPathGenerator.generate_output_path("base_output_dir/")
```

2. define a space
### define a space

Your diagnostics will be written in time and space. These may be defined in your model, but usually land model space is a sphere with no vertical dimension.
You may have variables varying with soil depth, and so you will need:

```
```Julia
space = bucket_domain.space.subsurface
```

3. define your writter
### define your writter

Your diagnostics will be written in a Julia Dict or a netcdf file, for example. This is up to you. For a netcdf file, you define your writter like this:

```
```Julia
nc_writer = ClimaDiagnostics.Writers.NetCDFWriter(space, output_dir)
```

providing the space and output_dir defined in steps 1. and 2.

4. make your diagnostics on your model, using your writter, and define a callback
### make your diagnostics on your model, using your writter, and define a callback

Now that you defined your model and your writter, you can create a callback function to be called when solving your model. For example:

```
diags = ClimaLand.default_diagnostics(model, 1.0, reference_date; output_writer = nc_writer)
```Julia
t0 = 0 # the starting time of your simulation

reference_date = DateTime(2024) # reference_date is the DateTime of your starting time

diags = ClimaLand.default_diagnostics(model, t0, reference_date; output_writer = nc_writer)

diagnostic_handler =
ClimaDiagnostics.DiagnosticsHandler(diags, Y, p, t0; dt = Δt)
Expand All @@ -62,62 +66,99 @@ Note that by default, `default_diagnostics` assign two optional kwargs: `output_
`output_vars = :long` will write all available diagnostics, whereas `output_vars = :short` will only write essentials diagnostics.
`average_period` defines the period over which diagnostics are averaged, it can be set to `:hourly`, `:daily` and `:monthly`.

# Custom Diagnostics
## Custom Diagnostics

When defining a custom diagnostic, follow these steps:
- Define how to compute your diagnostic variable from your model state and cache.
For example, let's say you want the bowen ratio (ratio between sensible heat and latent heat) in the Bucket model.
```
function compute_bowen_ratio!(out, Y, p, t, land_model::BucketModel)

### Define how to compute your diagnostic variable from your model state and cache.

For example, let's say you want the bowen ratio (ratio between sensible heat and latent heat) in the Bucket model.

```Julia
function compute_bowen_ratio!(out, Y, p, t, land_model::BucketModel)
if isnothing(out)
return copy(p.bucket.turbulent_fluxes.shf / p.bucket.turbulent_fluxes.lhf)
else
out .= p.bucket.turbulent_fluxes.shf / p.bucket.turbulent_fluxes.lhf
end
end
```
- Add that diagnostic variable to your list of variables
```
```

Or, for convenience, you can use the `@diagnostic_compute` macro which generates the same function.
However, it is better to use that macro only if you are getting a defined variable, such as latent heat flux.
(without an operation like the bowen ratio above). For example,

```Julia
@diagnostic_compute "latent_heat_flux" BucketModel p.bucket.turbulent_fluxes.lhf
```

### Add that diagnostic(s) variable to your list of variables

```Julia
add_diagnostic_variable!(
short_name = "bor",
long_name = "Bowen ratio",
standard_name = "bowen_ratio",
units = "",
compute! = (out, Y, p, t) -> compute_bowen_ratio!(out, Y, p, t, land_model),
)
```
- Define how to schedule your variables. For example, you want the seasonal maximum of your variables, where season is defined as 90 days.
```
seasonal_maxs(short_names...; output_writer, t_start) = common_diagnostics(
short_name = "bor",
long_name = "Bowen ratio",
standard_name = "bowen_ratio",
units = "",
comments = "Ratio of sensible to latent heat flux.",
compute! = (out, Y, p, t) -> compute_bowen_ratio!(out, Y, p, t, land_model),
)

add_diagnostic_variable!(
short_name = "lhf",
long_name = "Latent Heat Flux",
standard_name = "latent_heat_flux",
units = "W m^-2",
comments = "Exchange of energy at the land-atmosphere interface due to water evaporation or sublimation.",
compute! = (out, Y, p, t) ->
compute_latent_heat_flux!(out, Y, p, t, land_model),
)
```

### Define how to schedule your variables. For example, you want the seasonal maximum of your variables, where season is defined as 90 days.

```Julia
seasonal_maxs(short_names...; output_writer, t_start) = common_diagnostics(
90 * 24 * 60 * 60 * one(t_start),
max,
output_writer,
t_start,
short_names...,
)
```
- Define a function to return your `ScheduledDiagnostics`
```
function default_diagnostics(land_model::BucketModel, t_start; output_writer)

define_diagnostics!(land_model)

add_diagnostic_variable!(
short_name = "bor",
long_name = "Bowen ratio",
standard_name = "bowen_ratio",
units = "",
compute! = (out, Y, p, t) -> compute_bowen_ratio!(out, Y, p, t, land_model),
)

my_custom_diagnostics = [
"lhf",
"shf",
"bor",
]

my_custom_outputs =
seasonal_maxs(my_custom_diagnostics...; output_writer, t_start)
return [my_custom_outputs...]
end
```
```

### Define a function to return your `ScheduledDiagnostics`

Now, you can call your schedule with your variables.

```Julia
my_custom_diagnostics = ["lhf", "bor"]

diags = seasonal_maxs(my_custom_diagnostics...; output_writer, t_start)
```

### Analyze your simulation output

Once you've run your simulation and created an output folder (e.g., output\_dir) with diagnostics, you can use [ClimaAnalysis](https://github.com/CliMA/ClimaAnalysis.jl)
to access and analyze your data. For in depth documentation about ClimaAnalysis, see its [documentation](https://clima.github.io/ClimaAnalysis.jl/stable/).

Here is an example of how to plot a variable:

```Julia
import ClimaAnalysis

import ClimaAnalysis.Visualize as viz

import CairoMakie # the plotting package used by ClimaAnalysis

simdir = ClimaAnalysis.SimDir(output_dir) # where output_dir is where you saved your diagnostics.

var = get(simdir; "lhf") # assuming lhf, latent_heat_flux used as an example above, is one of your diagnostics variables.

fig = CairoMakie.Figure() # creates an empty figure object

viz.plot!(fig, var) # creates an axis inside fig, and plot your var in it.

CairoMakie.save(fig) # saves the figure in current working directory
```

16 changes: 16 additions & 0 deletions test/diagnostics/diagnostics_tests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Test
using ClimaLand
using ClimaLand.Diagnostics: @with_error

@test isdefined(ClimaLand.Diagnostics, :compute_albedo!)

Expand All @@ -9,7 +10,22 @@ using ClimaLand
)

# Define some diagnostics for a DummyModel

@test ClimaLand.Diagnostics.ALL_DIAGNOSTICS isa Dict
@test length(ClimaLand.Diagnostics.ALL_DIAGNOSTICS) == 0
struct DummyModel end
ClimaLand.Diagnostics.@diagnostic_compute "albedo" DummyModel p.foo.bar

ClimaLand.Diagnostics.add_diagnostic_variable!(
short_name = "alpha",
long_name = "Albedo",
standard_name = "albedo",
units = "",
compute! = (out, Y, p, t) -> compute_albedo!(out, Y, p, t, land_model),
)

@test length(ClimaLand.Diagnostics.ALL_DIAGNOSTICS) == 1

ClimaLand.Diagnostics.define_diagnostics!(DummyModel())

# Just to trigger the error
Expand Down
Loading