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.InputSource — Type
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.
All InputSources implement the following interface:
Terrarium.variables — Method
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.
Terrarium.initialize! — Method
initialize!(fields, _::InputSource, clock)
Initializes the input source. Default implementation does nothing.
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.
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.InputSource — Method
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.
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.InputSource — Method
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.
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.
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 RasterTime-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 variablesUsing 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)
endThe 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:
- Define a
structsubtypingInputSource{NF, name}whereNFis the numeric float type andnameis aSymbolidentifying the variable. - Implement
variables(source::MySource)returning a tuple ofinput(...)variable descriptors. - Implement
initialize!(fields, source::MySource, clock)for any one-time setup. - Implement
update_inputs!(fields, source::MySource, clock::Clock)to update the input field at each time step. - 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