Input sources

Input sources supply both static and time-varying input data for the InputVariables in a Terrarium simulation. Recall that input variables are distinct from model/process parameters: parameters are specified as properties of process/parameterization structs and are spatially invariant, whereas inputs are defined over the model grid; from the perspective of a process implementation, input variables look and behave exactly like any other Field in the state.

The InputSource interface

Terrarium.InputSourceType
abstract type InputSource{NF, name}

Base type for input data sources. Implementations of InputSource are free to load data from any arbitrary backend. They expect an initialize!(fields, ::InputSource) that is called once at model initialization and an update_inputs!(fields, ::InputSource, ::Clock) method that is called at every time step. Both default to doing nothing. Implementations should additionally provide a constructor as a dispatch of InputSource.

The type argument NF corresponds to the numeric type of the input data, name to its name that's also used in its variables definition.

source

All InputSources implement the following interface:

Terrarium.variablesMethod
variables(
    _::InputSource
) -> Tuple{InputVariable{_A, VD, UT, _B, DomainSets.RealLine{Float64}, Nothing} where {_A, VD<:Terrarium.VarDims, UT<:Unitful.Units, _B<:Terrarium.Variable{_A, VD, UT}}}

Returns a tuple of Symbols corresponding to variable names supported by this InputSource.

source
Terrarium.initialize!Method
initialize!(fields, _::InputSource, clock)

Initializes the input source. Default implementation does nothing.

source
Terrarium.update_inputs!Method
update_inputs!(fields, _::InputSource, _::Clock)

Updates the values of input variables stored in fields from the given input source. Default implementation simply returns nothing.

source

Built-in input source types

Static input Fields

A FieldInputSource holds a single Field that is copied into the state once at initialization and is thereafter unchanged. This is the appropriate input source for spatially-varying but time-constant forcings (e.g. maps of soil properties or prescribed climatology).

Terrarium.InputSourceMethod
InputSource(
    grid::Terrarium.AbstractLandGrid{NF},
    field::Oceananigans.Fields.AbstractField{LX, LY, LZ, G, NF} where {LX, LY, LZ, G};
    name,
    units
)

Create a FieldInputSource with the given grid and input variable fields. Use it for static input fields.

source
using Oceananigans: Field

# Existing Field or array on the model grid
albedo_field = Field(grid_2d)
set!(albedo_field, 0.3)

source = InputSource(grid, albedo_field; name = :albedo)

For a ColumnRingGrid, a RingGrids.Field can be passed directly and will be converted automatically:

albedo_ring = RingGrids.Field(albedo_data, global_grid)
source = InputSource(snow_grid, albedo_ring; name = :albedo)

Time-varying Field inputs

A FieldTimeSeriesInputSource wraps an Oceananigans FieldTimeSeries. At each time step the input field is set to the snapshot in the time series that is closest to clock.time (using FieldTimeSeries[Time(t)]).

using Oceananigans.Units: hours

# Allocate and populate a FieldTimeSeries
times = 0.0:3600.0:86400.0 # hourly for one day (seconds)
fts = FieldTimeSeries(grid, XY(), times)
fts.data .= randn(size(fts)) # fill with data
source = InputSource(fts; name = :air_temperature, units = u"°C")
Terrarium.InputSourceMethod
InputSource(
    grid::Terrarium.AbstractLandGrid{NF},
    field::Oceananigans.Fields.AbstractField{LX, LY, LZ, G, NF} where {LX, LY, LZ, G};
    name,
    units
)

Create a FieldInputSource with the given grid and input variable fields. Use it for static input fields.

source

The FieldTimeSeries can also be loaded from a file using the relevant constructors provided by Oceananigans.

Inputs from raster data

Alternatively, Terrarium provides an extension module for Rasters.jl with a RasterInputSource that reads data directly from any format supported by Rasters.jl (NetCDF, GeoTIFF, Zarr, etc.) and supports both static and time-varying inputs.

Note

Load Rasters together with Terrarium to activate the extension:

using Terrarium, Rasters

Static raster input

If the Raster has no time dimension, initialize! copies the data once and update_inputs! is a no-op:

using Rasters

raster = Raster("path/to/temperature.nc"; name = :temperature)
source = InputSource(grid, raster)   # Defaults to using name of Raster

Time-varying raster input

If the Raster has a Ti (time) dimension, values are linearly interpolated between the two nearest time points at each call to update_inputs!. Outside the bounds of the time axis the nearest available snapshot is used (flat extrapolation).

raster = Raster("path/to/forcing_timeseries.nc"; name = :air_temperature)
source = InputSource(grid, raster; reftime = DateTime(2000, 1, 1))

The reftime keyword maps the simulation's numeric clock.time (seconds) to wall-clock DateTime. If reftime is nothing (the default), the first time-axis value is used as the reference point. Pass reftime explicitly when the simulation clock does not start at the first record of the dataset:

# Simulation starts at t=0 s, but data begins on Jan 1 2000
source = InputSource(grid, raster; reftime = DateTime(2000, 1, 1))

Multiple input sources

Multiple InputSource objects are passed to initialize as positional arguments:

integrator = initialize(model, Heun(Δt = 3600.0), source1, source2, source3)

Internally they are collected into an InputSources container, which iterates over each source in order when calling initialize! and update_inputs!.

A standalone InputSources can also be constructed manually for inspection:

sources = InputSources(source1, source2)
variables(sources)   # union of all declared input variables

Using inputs inside process kernels

Input variables are stored in state.inputs and are also accessible through the top-level state shorthand, just like prognostic or auxiliary variables. Inside a kernel function, inputs appear as named fields and are retrieved the same way as any other field:

@propagate_inbounds function compute_snow_flux_tendency(i, j, grid, fields, snow_melt::DegreeDaySnow)
    T = fields.air_temperature[i, j, 1]   # input variable
    P = fields.snow_fall[i, j, 1]         # input variable
    k = snow_melt.k
    T_melt = snow_melt.T_melt
    return ifelse(T > T_melt, P - k * (T - T_melt), P)
end

The minimum set of input fields needed by a process is declared by including input variables in the variables method of the relevant AbstractProcess:

Terrarium.variables(snow::DegreeDaySnow{NF}) where {NF} = (
    input(:air_temperature, XY(), units = u"°C"),
    input(:snow_fall,       XY(), units = u"m/s"),
    prognostic(:snow_storage, XY()),
)

Terrarium's get_fields utility then automatically collects only the fields named in variables when assembling the argument list for kernel launch.

Implementing a custom input source

To add a new input source backend:

  1. Define a struct subtyping InputSource{NF, name} where NF is the numeric float type and name is a Symbol identifying the variable.
  2. Implement variables(source::MySource) returning a tuple of input(...) variable descriptors.
  3. Implement initialize!(fields, source::MySource, clock) for any one-time setup.
  4. Implement update_inputs!(fields, source::MySource, clock::Clock) to update the input field at each time step.
  5. Optionally provide a convenience InputSource(grid, ...; name, units) constructor dispatch so users do not need to reference the concrete type name.
struct MyInputSource{NF} <: InputSource{NF, :my_var}
    data::Vector{NF}
end

Terrarium.variables(::MyInputSource{NF}) where {NF} = (input(:my_var, XY()),)

function Terrarium.update_inputs!(fields, source::MyInputSource, clock::Clock)
    # populate fields.my_var from source.data at clock.time
    ...
end