Architecture

The backbone of the climlab architecture are the Process and TimeDependentProcess classes. All model components in climlab are instances of Process. Conceptually, a Process object represents any physical mechanism that can be described in terms of one or more state variables and processes that modify those variables.

As all relevant procedures and events that can be modelled with climlab are expressed in Processes, they build the basic structure of the package.

For example, if you want to model the incoming solar radiation on Earth, climlab implements it as a Process, namely in the Diagnostic Process _Insolation (or one of its specific daughter classes).

Another example: the emitted energy of a surface can be computed through the Boltzmann class which is also a climlab Process and implements the Stefan Boltzmann Law for a grey body. Like that, all events and procedures that climlab can model are organized in Processes.

Note

The implementation of a whole model, for example an Energy Balance Model (EBM), is also an instance of the Process class in climlab.

For more information about models, see the climlab Models chapter.

A Process object contains a subprocess dictionary, which itself can contain an arbitraily complex collection of other Process objects.

A Process that represents a whole model will typically have some subprocesses which represent specific physical components of the model, for example the albedo or the insolation component. More details about subprocesses can be found below.

The state variables of a Process are always defined on a Domain which itself is based on Axes or a single Axis. The following section will give a basic introduction about their role in the package, their dependencies and their implementation.

Process

A Process is an instance of the class Process. Most processes are time-dependent and therefore an instance of the daughter class TimeDependentProcess.

Basic Dictionaries

A climlab.Process object has several iterable dictionaries (dict) of named, gridded variables [1]:

  • process.state

    contains the process’ state variables, which are usually time-dependent and which are major quantities that identify the condition and status of the process. This can be the (surface) temperature of a model for instance.

  • process.input

    contains boundary conditions and other gridded quantities independent of the process. This dictionary is often set by a parent process.

  • process.param

    contains parameter of the Process or model. Basically, this is the same as process.input but with scalar entries.

  • process.tendencies

    is an iterable dictionary of time tendencies \((d/dt)\) for each state variable defined in process.state.

    Note

    A non TimeDependentProcess (but instance of Process) does not have this dictionary.

  • process.diagnostics

    contains any quantity derived from the current state. In an Energy Balance Model this dictionary can have entries like 'ASR', 'OLR', 'icelat', 'net_radiation', 'albedo' or 'insolation'.

  • process.subprocess

    holds subprocesses of the process. More about subprocesses is described below.

The process is fully described by contents of state, input and param dictionaries. tendencies and diagnostics are always computable from the current state.

[1]In the following the small written process refers to an instance of the Process class.

Subprocesses

Subprocesses are representing and modeling certain components of the parent process. A model consists of many subprocesses which are usually defined on the same state variables, domains and axes as the parent process, at least partially.

Example:

The subprocess tree of an EBM may look like this:

model_EBM               #<head process>
   diffusion            #<subprocess>
   LW                   #<subprocess>
   albedo               #<subprocess>
      iceline           #<sub-subprocess>
      cold_albedo       #<sub-subprocess>
      warm_albedo       #<sub-subprocess>
   insolation           #<subprocess>

It can be seen that subprocesses can have subprocesses themselves, like albedo in this case.

A subprocess is similar to its parent process an instance of the Process class. That means a subprocess has dictionaries and attributes with the same names as its parent process. Not necessary all will be the same or have the same entries, but a subprocess has at least the basic dictionaries and attributes created during initialization of the Process instance.

Every subprocess should work independently of its parent process given appropriate input.

Example:

Investigating an individual process (possibly with its own subprocesses) isolated from its parent can be done through:

newproc = climlab.process_like(procname.subprocess['subprocname'])
newproc.compute()

Thereby anything in the input dictionary of 'subprocname' will remain fixed.

Process Integration over time

A TimeDependentProcess can be integrated over time to see how the state variables and other diagnostic variables vary in time.

Time Dependency of a State Variable

For a state variable \(S\) which is dependendet on processes \(P_A\), \(P_B\), … the time dependency can be written as

\[\frac{dS}{dt} = \underbrace{P_A(S)}_{S \textrm{ tendency by }P_A} + \underbrace{P_B(S)}_{S \textrm{ tendency by } P_B} + \ ...\]

When the state variable \(S\) is discretized over time like

\[\frac{dS}{dt} = \frac{\Delta S}{\Delta t} = \frac{S(t_1) - S(t_0)}{t_1 - t_0} = \frac{S_1 - S_0}{\Delta t} ~,\]

the state tendency can be calculated through

\[\Delta S = \big[ P_A(S) + P_B(S) + \ ... \big] \Delta t\]

and the new state of \(S\) after one timestep \(\Delta t\) is then:

\[S_1 = S_0 + \big[ \underbrace{P_A(S)}_{S \textrm{ tendency by }P_A} + \underbrace{P_B(S)}_{S \textrm{ tendency by }P_B} + \ ... \ \big] \Delta t ~.\]

Therefore, the new state of \(S\) is calculated by multiplying the process tendencies of \(S\) with the timestep and adding them up to the previous state of \(S\).

Time Dependency of an Energy Budget

The time dependency of an EBM energy budget is very similar to the above noted equations, just differing in a heat capacity factor \(C\). The state variable is temperature \(T\) in this case, which is altered by subprocesses \(SP_A\), \(SP_B\), …

\[\begin{split}\frac{dE}{dt} = C \frac{dT}{dt} = \underbrace{SP_A(T)}_{\textrm{heating-rate of }SP_A} + \underbrace{SP_B(T)}_{\textrm{ heating-rate of }SP_B} + \ ... \\ \Leftrightarrow \frac{dT}{dt} = \underbrace{\frac{SP_A(T)}{C}}_{T \textrm{ tendency by }SP_A} + \underbrace{\frac{SP_B(T)}{C}}_{T \textrm{ tendency by }SP_B} + \ ...\end{split}\]

Therefore, the new state of \(T\) after one timestep \(\Delta t\) can be written as:

\[T_1 = \underbrace{T_0 + \underbrace{ \left[ \frac{SP_A(T)}{C} + \frac{SP_B(T)}{C} + \ ... \right]}_{\textrm{compute()}} \Delta t }_{\textrm{step\_forward()}}\]

The integration procedure is implemented in multiple nested function calls. The top functions for model integration are explained here, for details about computation of subprocess tendencies see Classification of Subprocess Types below.

  • compute() is a method that computes tendencies \(d/dt\) for all state variables
    • it returns a dictionary of tendencies for all state variables

      Temperature tendencies are \(\frac{SP_A(T)}{C}\), \(\frac{SP_B(T)}{C}\), … in this case, which are summed up like:

      \[\textrm{tendencies}(T) = \frac{SP_A(T)}{C} + \frac{SP_B(T)}{C} + ...\]
    • the keys for this dictionary are the same as keys of state dictionary

      As temperature \(T\) is the only state variable in this energy budget, the tendencies dictionary also just has the one key, representing the state variable \(T\).

    • the tendency dictionary holds the total tendencies for each state including all subprocesses

      In case subprocess \(SP_A\) itself has subprocesses, their \(T\) tendencies get included in tendency computation by compute().

    • the method only computes \(d/dt\) but does not apply changes (which is done by step_forward())

    • therefore, the method is relatively independent of the numerical scheme

    • method will update variables in proc.diagnostic dictionary. Therefore, it will also gather all diagnostics from the subprocesses

  • step_forward() updates the state variables
    • it calls compute() to get current tendencies
    • the method multiplies state tendencies with the timestep and adds them up to the state variables
  • integrate_years() etc will automate time-stepping by calling the step_forward method multiple times. It also does the computation of time-average diagnostics.

  • integrate_converge() calls integrate_years() as long as the state variables keep changing over time.

Example:

Integration of a climlab EBM model over time can look like this:

import climlab
model = climlab.EBM()

# integrate the model for one year
model.integrate_years(1)

Classification of Subprocess Types

Processes can be classified in types: explicit, implicit, diagnostic and adjustment. This makes sense as subprocesses may have different impact on state variable tendencies (diagnostic processes don’t have a direct influence for instance) or the way their tendencies are computed differ (explixit and implicit).

Therefore, the compute() method handles them seperately as well as in specific order. It calls private _compute() methods that are specified in daugther classes of Process namely DiagnosticProcess, EnergyBudget (which are explicit processes) or ImplicitProcess.

The description of compute() reveals the details how the different process types are handeled:

The function first computes all diagnostic processes. They don’t produce any tendencies directly but they may effect the other processes (such as change in solar distribution). Subsequently, all tendencies and diagnostics for all explicit processes are computed.

Tendencies due to implicit and adjustment processes need to be calculated from a state that is already adjusted after explicit alteration. For that reason the explicit tendencies are applied to the states temporarily. Now all tendencies from implicit processes are calculated by matrix inversions and similar to the explicit tendencies, the implicit ones are applied to the states temporarily. Subsequently, all instantaneous adjustments are computed.

Then the changes that were made to the states from explicit and implicit processes are removed again as this compute() function is supposed to calculate only tendencies and not apply them to the states.

Finally, all calculated tendencies from all processes are collected for each state, summed up and stored in the dictionary self.tendencies, which is an attribute of the time-dependent-process object, for which the compute() method has been called.

Domain

A Domain defines an area or spatial base for a climlab Process object. It consists of axes which are Axis objects that define the dimensions of the Domain.

In a Domain the heat capacity of grid points, bounds or cells/boxes is specified.

There are daughter classes Atmosphere and Ocean of the private _Domain class implemented which themselves have daughter classes SlabAtmosphere and SlabOcean.

Every Process needs to be defined on a Domain. If none is given during initialization but latitude lat is specified, a default Domain is created.

Several methods are implemented that create Domains with special specifications. These are

Axis

An Axis is an object where information of a _Domain’s spacial dimension are specified.

These include the type of the axis, the number of points, location of points and bounds on the spatial dimension, magnitude of bounds differences delta as well as their unit.

The axes of a _Domain are stored in the dictionary axes, so they can be accessed through dom.axes if dom is an instance of _Domain.

Accessibility

For convenience with interactive work, each subprocess 'name' should be accessible as proc.subprocess.name as well as the regular way through the subprocess dictionary proc.subprocess['name']. Note that proc is an instance of the Process class here.

Example:
import climlab
model = climlab.EBM()

# quick access
longwave_subp = model.subprocess.LW

# regular path
longwave_subp = model.subprocess['LW']

climlab will remain (as much as possible) agnostic about the data formats. Variables within the dictionaries will behave as numpy.ndarray objects.

Grid information and other domain details are accessible as attributes of each process. These attributes are lat, lat_bounds, lon, lon_bounds, lev, lev_bounds, depth and depth_bounds.

Example:

the latitude points of a process object that is describing an EBM model

import climlab
model = climlab.EBM()

# quick access
lat_points = model.lat

# regular path
lat_points = model.domains['Ts'].axes['lat'].points

Shortcuts like proc.lat will work where these are unambiguous, which means there is only a single axis of that type in the process.

Many variables will be accessible as process attributes proc.name. This restricts to unique field names in the above dictionaries.

Warning

There may be other dictionaries that do have name conflicts: e.g. dictionary of tendencies proc.tendencies, with same keys as proc.state.

These will not be accessible as proc.name, but will be accessible as proc.dict_name.name (as well as regular dictionary interface proc.dict_name['name']).