Some notes about the internals of ClimaDiagnostics
There are multiple moving parts to this package. In this page, we provide some notes about the internal design. This page also aims at clarifying the whys, i.e., explaining why things are the way they are. Learning about that might help you extend this package further.
Schedules
Diagnostics are computed with a callback that is called at the end of each step. The centerpiece of is the orchestrate_diagnostics
function, a master callback that is unconditionally executed at the end of each step. orchestrate_diagnostics
loops over each registered diagnostic, computing and output those for which their trigger condition is met. Conditions are specified with schedule functions, function that take the integrator
as single argument and return a boolean value that determines whether the callback should be executed or note. Most often, schedules_func
are not simple functions, but callable objects (subtypes of AbstractSchedule
). There are two reasons for this:
- Most realistic schedules need to hold additional data (e.g., the last time the function was called)
- We want to attach names to use in the output
Most of the details regarding schedules are described in the user guide. An internal detail that is not there is related to names. We define a method for show
for AbstractSchedules
. This method calls the short_name
function.
Base.show(io::IO, schedule::AbstractSchedule) = print(io, short_name(schedule))
This allows us to set names of ScheduledDiagnostics
with "$schedule_func"
in both the case schedule_func
is a normal function, or an AbstractSchedule
.
Accumulation
Several diagnostics require performing reductions, such as taking the maximum or the average. Since it is not feasible to store all the lists of all the intermediate values, we aggregate the results in specific storage areas (e.g., we take max(max(max(max(t1, t2), t3), t4), t5)
instead of max(t1, t2, t3, t4, t5)
In this, it is convenient to preallocate the space where we want to accumulate the intermediate.
Accumulation is accomplished by the accumulate!
function. All this function 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. However, we have to fill the space with something that does not affect the reduction. This, by definition, is the identity of the operation. The identity of the operation +
is 0
because x + 0 = x
for every x
.
We have to know the identity for every operation we want to support. Of course, users are welcome to define their own by adding new methods to identityofreduction.
For instance, to define the identity of the reduction -
, one would write
function ClimaDiagnostics.Diagnostics.identity_of_reduction(::typeof(-))
return 0
end
(Or add this to the reduction_identities.jl
file.)
On the design of the DiagnosticsHandler
There are two possible choices for accumulation of variables: each scheduled diagnostic can carry its accumulator and counters, or all the accumulators and counters are managed by a single central handler. ClimaDiagnostics
implements this second approach. The author of this package has not decided whether this is a good idea or not. On one side, this allows us to have a concretely typed and well defined DiagnosticsHandler
struct. On the other side, it forces us to initialize all the diagnostics at the very beginning of the simulation (this can also be a positive side, because it allows us to compile all the diagnostics at once). It might be worth exploring the alternative design where the ScheduledDiagnostics
get their storage space the first time they are called.
Given this restriction, the main entry point for ClimaDiagnostics
is the IntegratorWithDiagnostics
function. This function is a little dissatisfying because it creates a new integrator obtained by copying all the fields of the old one and adding the diagnostics (with Accessors
).
Orchestrate diagnostics
One of the design goals for orchestrate_diagnostics
is to keep all the broadcasted expression in the same function scope. This opens a path to optimize the number of GPU kernel launches.