Using ClimaLand Diagnostics when running a simulation

When running a ClimaLand simulations, you have multiple options on how to write the outputs of that simulation. You may want all variables, or just a selected few. You may want instantaneous values, at the highest temporal and spatial resolution, or you may want to get averages at hourly or monthly time scale, and integrate in space (for example soil moisture from 0 to 1 meter depth). You may want to get more specific reductions, such as 10 days maximums, or compute a new variables that is a function of others. You may want to get your outputs in memory in a Julia Dict, or write them in a NetCDF file.

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

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:

define an output folder

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

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:

space = bucket_domain.space.subsurface

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:

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

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

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:

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)

diag_cb = ClimaDiagnostics.DiagnosticsCallback(diagnostic_handler)

sol = SciMLBase.solve(prob, ode_algo; dt = Δt, callback = diag_cb)

Your diagnostics have now been written in netcdf files in your output folder.

Note that by default, default_diagnostics assign two optional kwargs: output_vars = :long and average_period = :daily. 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

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)
    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

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,

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

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

 add_diagnostic_variable!(
    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.

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

Now, you can call your schedule with your variables.

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 to access and analyze your data. For in depth documentation about ClimaAnalysis, see its documentation.

Here is an example of how to plot a variable:

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