Sindbad.DataLoaders Module
DataLoadersThe DataLoaders module provides tools for ingesting and preprocessing SINDBAD input data. It supports reading, cleaning, masking, and managing forcing/observation data with an emphasis on spatial and temporal dimensions.
Purpose
Streamline the ingestion and preprocessing of input data for SINDBAD experiments.
Dependencies
Related (SINDBAD ecosystem)
OmniTools: Utility functions for handling NamedTuples, printing, and shared helpers.
External (third-party)
AxisKeys: Labeled multidimensional arrays (KeyedArray).DimensionalData: Dimension-aware indexing/slicing.FillArrays: Efficient filled-array representations.Interpolations: Interpolation methods ofresampleCubewhen forcing or parameters are put on the space and time of the run.YAXArrays,YAXArrayBase: Multidimensional array/cube abstractions used for IO and spatial data.YAXArrays.open_datasetreads both netcdf and zarr sources uniformly.NetCDF,Zarr: not called directly for I/O; merely having them loaded activates YAXArrayBase'sNetCDFExt/ZarrExtbackends thatopen_datasetdispatches to.NetCDFis also used directly by some example scripts viaDataLoaders.NetCDF.open.
Internal (within Sindbad)
Sindbad.SetupSindbad.TypesSindbadTEM
Included Files
utilsDataLoaders.jl: Utility functions for data preprocessing (cleaning, masking, bounds checks).spatialSubset.jl: Spatial operations (extracting subsets based on spatial dimensions).resampleCube.jl: Interpolation methods that put a cube on target dimensions, withresampleValues!to extend for new methods, andresampleForcingCubesandresampleCubeToTarget, which put the forcing and parameters on the target resolution of the run.getForcing.jl: Extracting and processing forcing data (environmental drivers), with the settings of each variable merged and its dimensions renamed.getParameters.jl: Loading and checking the parameters per location from a json or zarr/nc parameters input.getObservation.jl: Reading and processing observational data for evaluation/validation.handleModelObsData.jl: Helpers for aligning forcing/observation/model outputs for cost evaluation.
Notes
The module uses
YAXArraysdirectly; it does not re-export it.Designed to handle large datasets efficiently, leveraging chunked and compressed data formats like NetCDF and Zarr.
Ensures compatibility with SINDBAD's experimental framework by integrating spatial and temporal data management tools.
Functions
applyQCBound
Sindbad.DataLoaders.applyQCBound Function
applyQCBound(_data, data_qc, bounds_qc, _data_fill)Apply quality control bounds to data values.
Arguments
_data: Input data array to be quality controlleddata_qc: Quality control flags associated with the databounds_qc: Bounds/thresholds for quality control checks_data_fill: Fill value to use for data points that fail QC
Returns
The quality controlled data array with values outside bounds replaced by fill value
sourceCode
function applyQCBound(_data, data_qc, bounds_qc, _data_fill)
_data_out = _data
if data_qc < first(bounds_qc) || data_qc > last(bounds_qc)
_data_out = _data_fill
end
return _data_out
endapplyUnitConversion
Sindbad.DataLoaders.applyUnitConversion Function
applyUnitConversion(_data, conversion, isadditive=false)Applies a simple factor to the input, either additively or multiplicatively depending on isadditive flag
Arguments
_data: Input data to be convertedconversion: Conversion factor or function to be appliedisadditive: Boolean flag indicating whether the conversion is additive (default: false) or multiplicative
Returns
Converted data with the applied unit transformation
sourceCode
function applyUnitConversion(_data, conversion, isadditive=false)
if isadditive
_data_out = _data + conversion
else
_data_out = _data * conversion
end
return _data_out
endcleanData
Sindbad.DataLoaders.cleanData Function
cleanData(_data, _data_info, ::Val{T}) where {T}Applies the unit conversion and the bounds to a data point. An invalid data point, i.e., nothing, missing, NaN or Inf, is not filled with another value: it is returned as NaN, without unit conversion or clamping, so that it stays invalid.
Arguments
_data: The raw data to be cleaned_data_info: Information about the data structure and cleaning requirements::Val{T}: Value type parameter for dispatch
Returns
Cleaned data according to the specified type parameter T
sourceCode
function cleanData(_data, _data_info, ::Val{T}) where {T}
is_invalid_number(_data) && return T(NaN)::T
_data = applyUnitConversion(_data, _data_info.source_to_sindbad_unit,
_data_info.additive_unit_conversion)
bounds = _data_info.bounds
# no bounds, given as null or an empty list, means no clamping
if !isnothing(bounds) && !isempty(bounds)
_data = clamp(_data, first(bounds), last(bounds))
end
# the assertion keeps the cleaned data concretely typed when the bounds are an
# untyped empty list, for which the type of the clamp cannot be inferred
return T(_data)::T
endgetData
Sindbad.DataLoaders.getData Function
getData(model_output::LandWrapper, observations, cost_option)
getData(model_output::NamedTuple, observations, cost_option)
getData(model_output::AbstractArray, observations, cost_option)Arguments:
model_output: a collection of SINDBAD model output time series as a time series of stacked land NT or as a preallocated array.observations: a NT or a vector of arrays of observations, their uncertainties, and mask to use for calculation of performance metric/losscost_option: information for a observation constraint on how it should be used to calculate the loss/metric of model performance
Notes:
- The observations are taken from
cost_option.obs_yandcost_option.obs_yσ, whichprepCostOptionsprepares fromobservationsat the resolution of the cost.
Code
function getData end
function getData(model_output::LandWrapper, observations, cost_option)
mod_field = cost_option.mod_field
mod_subfield = cost_option.mod_subfield
ŷField = getproperty(model_output, mod_field)
ŷ = getproperty(ŷField, mod_subfield)
y = cost_option.obs_y
yσ = cost_option.obs_yσ
if size(ŷ, 2) == 1
ŷ = getModelOutputView(ŷ)
y = y[:]
yσ = yσ[:]
end
# ymask = observations[obs_ind + 2]
y, yσ, ŷ = getHarmonizedData(y, yσ, ŷ, cost_option)
return (y, yσ, ŷ)
end
function getData(model_output::LandWrapper, observations, cost_option)
mod_field = cost_option.mod_field
mod_subfield = cost_option.mod_subfield
ŷField = getproperty(model_output, mod_field)
ŷ = getproperty(ŷField, mod_subfield)
y = cost_option.obs_y
yσ = cost_option.obs_yσ
if size(ŷ, 2) == 1
ŷ = getModelOutputView(ŷ)
y = y[:]
yσ = yσ[:]
end
# ymask = observations[obs_ind + 2]
y, yσ, ŷ = getHarmonizedData(y, yσ, ŷ, cost_option)
return (y, yσ, ŷ)
end
function getData(model_output::NamedTuple, observations, cost_option)
mod_field = cost_option.mod_field
mod_subfield = cost_option.mod_subfield
ŷ = model_output
sf_name = mod_subfield
if !hasproperty(model_output, sf_name)
sf_name = Symbol(String(mod_field) * "__" * String(mod_subfield))
end
ŷ = getproperty(model_output, sf_name)
y = cost_option.obs_y
yσ = cost_option.obs_yσ
if size(ŷ, 2) == 1
ŷ = getModelOutputView(ŷ)
end
# ymask = observations[obs_ind + 2]
y, yσ, ŷ = getHarmonizedData(y, yσ, ŷ, cost_option)
return (y, yσ, ŷ)
end
function getData(model_output::AbstractArray, observations, cost_option)
ŷ = model_output[cost_option.mod_ind]
if size(ŷ, 2) == 1
ŷ = getModelOutputView(ŷ)
end
y = cost_option.obs_y
yσ = cost_option.obs_yσ
y, yσ, ŷ = getHarmonizedData(y, yσ, ŷ, cost_option)
return (y, yσ, ŷ)
endgetForcing
Sindbad.DataLoaders.getForcing Function
getForcing(info::NamedTuple)Reads forcing data from the data_path specified in the experiment configuration and returns a NamedTuple with the forcing data.
Arguments:
info: A SINDBAD NamedTuple containing all information needed for setup and execution of an experiment.
Returns:
- A NamedTuple
forcingcontaining:data: The processed input cubes.dims: The dimensions of the forcing data.variables: The names of the forcing variables.f_types: The types of the forcing data (e.g.,ForcingWithTimeorForcingWithoutTime).helpers: Helper information for the forcing data.parameters: The parameters per location set by a json or zarr/ncparametersinput, ornothing. They are kept apart fromdata, seegetParameters.
Notes:
Reads forcing data from the specified data path and processes it using the SINDBAD framework.
Handles spatiotemporal and spatial-only forcing data.
Applies masks and subsets to the forcing data if specified in the configuration.
Each variable can name its own source dimensions in
dimension.space, as a vector or a single name such as"site", anddimension.time. They are renamed to the names indata_dimensionbefore subsetting.When
target_resolution.space.variableortarget_resolution.time.variablenames a forcing variable and that axis hasapplyset totrue, all cubes are resampled to its space grid or time axis with the methods ininterpolation_method.spaceandinterpolation_method.time. A method of"None", which is the default, leaves that variable on its own grid. SeeresampleForcingCubes.
Code
function getForcingVariableInfo(default_info, var_settings, forcing_data_settings)
vinfo = merge_namedtuple_prefer_nonempty(default_info, var_settings)
dims_fallback = (; space=forcing_data_settings.data_dimension.space, time=forcing_data_settings.data_dimension.time)
var_dims = mergeForcingGroup(dims_fallback, :dimension, default_info, var_settings)
method_fallback = (; space="None", time="None")
var_methods = mergeForcingGroup(method_fallback, :interpolation_method, default_info, var_settings)
vinfo = set_namedtuple_field(vinfo, (:dimension, var_dims))
vinfo = set_namedtuple_field(vinfo, (:interpolation_method, var_methods))
return vinfo
end
function getForcing(info::NamedTuple)
nc_default = nothing
forcing_data_settings = info.experiment.data_settings.forcing
# forcing_data_settings = info.experiment.data_settings.forcing
data_path = forcing_data_settings.default_forcing.data_path
if !isnothing(data_path)
data_path = getAbsDataPath(info, data_path)
print_info(getForcing, @__FILE__, @__LINE__, "default_data_path: `$(data_path)`")
nc_default = YAXArrays.open_dataset(data_path)
end
forcing_mask = nothing
if :sel_mask ∈ keys(forcing_data_settings)
if !isnothing(forcing_data_settings.forcing_mask.data_path)
mask_path = getAbsDataPath(info, forcing_data_settings.forcing_mask.data_path)
_, forcing_mask = getYaxFromSource(nothing, mask_path, nothing, forcing_data_settings.forcing_mask.source_variable)
forcing_mask = positive_mask(forcing_mask)
end
end
default_info = info.experiment.data_settings.forcing.default_forcing
forcing_vars = keys(forcing_data_settings.variables)
tar_dims = getTargetDimensionOrder(info)
print_info(getForcing, @__FILE__, @__LINE__, "getting forcing variables. Units given in forcing settings are not strictly enforced but shown for reference. Bounds are applied after unit conversion...", n_m=1)
num_type = Val{info.helpers.numbers.num_type}()
loaded_vars = map(forcing_vars) do k
nc = nc_default
vinfo = getForcingVariableInfo(default_info, forcing_data_settings.variables[k], forcing_data_settings)
data_path_v = getAbsDataPath(info, getfield(vinfo, :data_path))
nc, yax = getYaxFromSource(nc, data_path, data_path_v, vinfo.source_variable)
yax = renameToCanonicalDims(yax, vinfo.dimension, forcing_data_settings)
incube = subsetAndProcessCube(yax, forcing_mask, tar_dims, vinfo, info, num_type)
v_op = vinfo.additive_unit_conversion ? " + " : " * "
v_op = v_op * "$(vinfo.source_to_sindbad_unit)"
v_string = "`$(k)` ($(vinfo.sindbad_unit), $(vinfo.bounds)) = <$(vinfo.space_time_type)> `$(vinfo.source_variable)` ($(vinfo.source_unit)) $(v_op)"
print_info(nothing, @__FILE__, @__LINE__, v_string, n_m=4)
(k => incube, vinfo)
end
incubes = first.(loaded_vars)
vinfos = last.(loaded_vars)
interpincubes = resampleForcingCubes(incubes, vinfos, forcing_data_settings, num_type)
# The sizes are taken after resampling so that they match the target resolution.
f_sizes = nothing
f_dimension = nothing
i_spatiotemporal = findfirst(vinfo -> vinfo.space_time_type == "spatiotemporal", vinfos)
if !isnothing(i_spatiotemporal)
f_sizes = collectForcingSizes(info, interpincubes[i_spatiotemporal])
f_dimension = getSindbadDims(interpincubes[i_spatiotemporal])
end
parameters = getParameters(info, f_dimension, NamedTuple{Tuple(forcing_vars)}(Tuple(interpincubes)))
return createForcingNamedTuple(interpincubes, f_sizes, f_dimension, info; parameters=parameters)
endgetInterpolationMethod
Sindbad.DataLoaders.getInterpolationMethod Function
getInterpolationMethod(setting, axis)Returns the interpolation method of a forcing or parameter variable from its setting in interpolation_method.space or interpolation_method.time.
Arguments:
setting: the name of the method, e.g."linear","Linear"or"fritsch_carlson_monotonic", or a group with the name inmethodand options of the method, e.g.{"method": "cubic", "extrapolation": "periodic"}. A method is returned as it is.axis::spaceor:time, the axis that the method is set for
Returns:
- An instance of the subtype of
SpaceInterpolationMethodorTimeInterpolationMethodnamedSpaceInterporTimeInterpand the name in camel case, e.g.TimeInterpFritschCarlsonMonotonic, with its options set.
Notes:
The methods of space and time are separate types, so a name is only looked up among the methods of
axis.The options of a method are the fields of its type. A name without options takes their defaults.
The name and options are checked here, so a wrong one stops the run before the data is resampled.
Code
function getInterpolationMethod end
function getInterpolationMethod(method::InterpolationMethod, axis::Symbol)
method isa getInterpolationAxis(axis).method_type || error("`$(nameof(typeof(method)))` is not a $(axis) interpolation method")
return method
end
function getInterpolationMethod(method::InterpolationMethod, axis::Symbol)
method isa getInterpolationAxis(axis).method_type || error("`$(nameof(typeof(method)))` is not a $(axis) interpolation method")
return method
end
function getInterpolationMethod(setting::NamedTuple, axis::Symbol)
hasproperty(setting, :method) || error("an interpolation_method group needs the name of the method in `method`, got $(setting)")
method_name = string(setting.method)
method_type = getInterpolationType(method_name, axis)
if isnothing(method_type)
other_axis = axis == :space ? :time : :space
other_note = isnothing(getInterpolationType(method_name, other_axis)) ? "" : " It is a $(other_axis) method."
error("unknown $(axis) interpolation_method `$(method_name)`.$(other_note) Valid $(axis) methods are: $(join(getInterpolationMethodNames(axis), ", ")).")
end
options = Base.structdiff(setting, NamedTuple{(:method,)})
unknown = setdiff(keys(options), fieldnames(method_type))
if !isempty(unknown)
valid_options = isempty(fieldnames(method_type)) ? "none" : join(fieldnames(method_type), ", ")
error("$(axis) interpolation_method `$(method_name)` has no option $(join(unknown, ", ")). Its options are: $(valid_options).")
end
method = method_type(; options...)
checkInterpolationMethod(method)
return method
end
function getInterpolationMethodNames(axis::Symbol)
axis_info = getInterpolationAxis(axis)
concreteSubtypes(T) = isconcretetype(T) ? [T] : reduce(vcat, map(concreteSubtypes, subtypes(T)); init=Type[])
type_names = map(T -> replace(string(nameof(T)), Regex("^" * axis_info.prefix) => ""), concreteSubtypes(axis_info.method_type))
return sort(map(n -> lowercase(replace(n, r"(?<=[a-z0-9])(?=[A-Z])" => "_")), type_names))
endgetModelOutputView
Sindbad.DataLoaders.getModelOutputView Function
getModelOutputView(_dat::AbstractArray{<:Any,N}) where NCode
function getModelOutputView(_dat::AbstractArray{<:Any,N}) where N
# the second dimension is dropped and every other one is kept whole. ntuple with a
# static N builds the indices without the counter that map needed, which was
# captured and boxed by its closure and allocated at every call.
inds = ntuple(i -> i == 2 ? 1 : Colon(), Val(N))
@view _dat[inds...]
endgetNumberOfTimeSteps
Sindbad.DataLoaders.getNumberOfTimeSteps Function
getNumberOfTimeSteps(incubes, time_name)Returns the number of time steps in the input data cubes.
Arguments
incubes: Input data cubes containing temporal informationtime_name: Name of the time dimension/variable
Returns
Integer representing the total number of time steps in the data
sourceCode
function getNumberOfTimeSteps(incubes, time_name)
i1 = findfirst(c -> YAXArrays.Axes.findAxis(time_name, c) !== nothing, incubes)
return length(getAxis(time_name, incubes[i1]).values)
endgetObservation
Sindbad.DataLoaders.getObservation Function
getObservation(info::NamedTuple, forcing_helpers::NamedTuple)Processes observation data and returns a NamedTuple containing the observation data, dimensions, and variables.
Arguments:
info: A SINDBAD NamedTuple containing all information needed for setup and execution of an experiment.forcing_helpers: A SINDBAD NamedTuple containing helper information for forcing data.
Returns:
- A NamedTuple with the following fields:
data: The processed observation data as an input array.dims: The dimensions of the observation data.variables: A tuple of variable names for the observation data.
Notes:
- Reads observation data from the path specified in the experiment configuration.
Examples
julia> using Sindbad
julia> # Load observation data from experiment configuration
julia> # observations = getObservation(info, forcing_helpers)Handles quality flags, uncertainty, spatial weights, and selection masks for each observation variable.
Subsets and harmonizes the observation data based on the target dimensions and masks.
Code
function getObservation(info::NamedTuple, forcing_helpers::NamedTuple)
observation_data_settings = info.experiment.data_settings.optimization
forcing_data_settings = info.experiment.data_settings.forcing
data_path = observation_data_settings.observations.default_observation.data_path
default_info = observation_data_settings.observations.default_observation
tar_dims = getTargetDimensionOrder(info)
nc_default = nothing
if !isnothing(data_path)
data_path = getAbsDataPath(info, data_path)
print_info(getObservation, @__FILE__, @__LINE__, "default_observation_data_path: `$(data_path)`")
nc_default = YAXArrays.open_dataset(data_path)
end
varnames = Symbol.(observation_data_settings.observational_constraints)
yax_mask = nothing
if :one_sel_mask ∈ keys(observation_data_settings)
if !isnothing(observation_data_settings.one_sel_mask)
mask_path = getAbsDataPath(info, observation_data_settings.one_sel_mask)
_, yax_mask = getYaxFromSource(nothing, mask_path, nothing, "mask")
yax_mask = positive_mask(yax_mask)
end
end
obscubes = []
num_type = Val{info.helpers.numbers.num_type}()
num_type_bool = Val{Bool}()
print_info(getObservation, @__FILE__, @__LINE__, "getting observation variables. Units given in optimization settings are not strictly enforced but shown for reference. Bounds are applied after unit conversion...")
map(varnames) do k
vinfo = getproperty(observation_data_settings.observations.variables, k)
print_info(nothing, @__FILE__, @__LINE__, "constraint: `$k`", n_m=4)
src_var = vinfo.data.source_variable
nc = nc_default
nc, yax, vinfo_data, bounds_data = getAllConstraintData(nc, data_path, default_info, vinfo, :data, info)
# get quality flags data and use it later to mask observations. Set to value of 1 when :qflag field is not given for a data stream or all are turned off by setting optimizatio.optimization.observations.use_quality_flag to false
nc_qc, yax_qc, vinfo_qc, bounds_qc = getAllConstraintData(nc, data_path, default_info, vinfo, :qflag, info; yax=yax, use_data_sub=observation_data_settings.observations.use_quality_flag)
# get uncertainty data and add to observations. Set to value of 1 when :unc field is not given for a data stream or all are turned off by setting observation_data_settings.use_uncertainty to false
nc_unc, yax_unc, vinfo_unc, bounds_unc = getAllConstraintData(nc, data_path, default_info, vinfo, :unc, info; yax=yax, use_data_sub=observation_data_settings.observations.use_uncertainty)
nc_wgt, yax_wgt, vinfo_wgt, bounds_wgt = getAllConstraintData(nc, data_path, default_info, vinfo, :weight, info; yax=yax, use_data_sub=observation_data_settings.observations.use_spatial_weight)
_, yax_mask_v, vinfo_mask, bounds_mask = getAllConstraintData(nc, data_path, default_info, vinfo, :sel_mask, info; yax=yax)
yax_mask_v = positive_mask(yax_mask_v)
if !isnothing(yax_mask)
yax_mask_v .= yax_mask .* yax_mask_v
end
print_info(nothing, @__FILE__, @__LINE__, "harmonize/subset...", n_m=6)
@debug " qflag"
cyax_qc = subsetAndProcessCube(yax_qc, yax_mask_v, tar_dims, vinfo_qc, info, num_type; clean_data=false)
@debug " data"
cyax = subsetAndProcessCube(yax, yax_mask, tar_dims, vinfo_data, info, num_type; yax_qc=cyax_qc, bounds_qc=bounds_qc)
@debug " unc"
cyax_unc = subsetAndProcessCube(yax_unc, yax_mask, tar_dims, vinfo_unc, info, num_type; yax_qc=cyax_qc, bounds_qc=bounds_qc)
@debug " weight"
cyax_wgt = subsetAndProcessCube(yax_wgt, yax_mask, tar_dims, vinfo_wgt, info, num_type; yax_qc=cyax_qc, bounds_qc=bounds_qc)
@debug " mask"
yax_mask_v = subsetAndProcessCube(yax_mask_v, yax_mask_v, tar_dims, vinfo_mask, info, num_type_bool; clean_data=false)
push!(obscubes, cyax)
push!(obscubes, cyax_unc)
push!(obscubes, yax_mask_v)
push!(obscubes, cyax_wgt)
end
print_info(getObservation, @__FILE__, @__LINE__, "getting observation helpers...", n_m=2)
@debug "getObservation: getting observation dimensions..."
indims = getDataDims.(obscubes, Ref(forcing_data_settings.data_dimension.space))
@debug "getObservation: getting number of time steps..."
nts = forcing_helpers.sizes
@debug "getObservation: getting variable name..."
varnames_all = []
for v ∈ varnames
push!(varnames_all, v)
push!(varnames_all, Symbol(string(v) * "_σ"))
push!(varnames_all, Symbol(string(v) * "_mask"))
push!(varnames_all, Symbol(string(v) * "_weight"))
end
print_info_separator()
return (; data=getInputArrayOfType(obscubes, info.helpers.run.input_array_type), dims=indims, variables=Tuple(varnames_all))
endgetParameters
Sindbad.DataLoaders.getParameters Function
getParameters(info::NamedTuple, forcing_axes, forcing_cubes=nothing)Loads the parameters per location from the parameter input, and makes their values ready to use in the same way getForcing does for the forcing.
Arguments:
info: A SINDBAD NamedTuple containing all information needed for setup and execution of an experiment.forcing_axes: the dimension name and coordinate pairs of the forcing, after its subsetforcing_cubes: a NamedTuple of the forcing cubes by variable name, which gives the class of each location for alookup_parameter_map
Returns:
nothingwhen there is no json or zarr/nc parameters input.Otherwise a NamedTuple with:
variables: the names of the parameter variablesdata: one YAXArray per variable on the spatial dimensions of the forcing, in the units of the run, without NaN, and within the bounds of the parameter. A variable that varies in time has the time dimension of the forcing first. It is lazy in a lazy run, so a large forcing is only read where the run needs it, and in memory otherwise.parameter_table: a Table with one row per variable with the resolved settings:model,name,name_full,model_approach,units,timescale_run,unit_factor,lower,upper,default,source_variable,data_path,space_time_type,time_dimension,time_method,n_nanandn_clampedf_types:ForcingWithTime()orForcingWithoutTime()per variable, as for the forcingdims: the dimensions of each variable besides the spatial ones,(time,)or(), asforcing.dimslookup_variable: thelookup_variableof a class map whose lookup variable has time, elsenothing
Notes:
The settings of a variable come from its entry in the json first, then from
default_parameter_map, then from the attributes of the variable in the file.When the input has no
variables, which is the case for a zarr/nc file given directly, every variable of the file withmodelandnameattributes is used.Every parameter must exist in the selected model structure, checked against
info.models.parameter_table. Only scalar parameters are supported.The values are selected at the coordinates of the forcing, in the order of the forcing, so the same spatial index selects the same location in the forcing and in the parameters. A file may have more locations than the forcing, in any order.
A location map on another grid than the forcing is resampled to the forcing grid with the
spaceofinterpolation_method,nearestby default, and its spatial dimensions may have other names, set in thespaceofdimension. SeeputOnForcingGrid.Values,
lower,upperanddefaultare converted to the time step of the run with thetimescale_runof the input, the same waysetInputParametersconverts a csv.NaN values are replaced by the
default, and values outside[lower, upper]are clamped. Both are counted and reported once per parameter.A
lookup_parameter_mapindefault_parameter_map, e.g.,{"lookup_variable": "f_pft", "default": 4}, makes the file a class map: its variables have one dimension named after the forcing variablelookup_variable, with one value per class. Each location takes the value of its class in that forcing variable, else of thedefaultclass, else thedefaultof the parameter. The result is a location map like any other, seegetParameterValuesByClass.A variable with
space_time_typespatiotemporalvaries in time like a forcing variable, seegetParameterValuesByLocationAndTime. A class map varies in time when its lookup variable does, e.g., a yearly map of PFT, and its variables then stayspatial. The run updates the models when the values change, seetimeLoopTEM!.
Code
function getParameters(info::NamedTuple, forcing_axes, forcing_cubes=nothing)
parameter_input = get(info.experiment.data_settings, :parameters, nothing)
isnothing(parameter_input) && return nothing
print_info(getParameters, @__FILE__, @__LINE__, "getting parameters per location from the parameters input...", n_m=1)
if isnothing(forcing_axes)
error("A json or zarr/nc parameters input needs a forcing with a spatiotemporal variable to set the locations of the parameters.")
end
space_dims = Symbol.(info.experiment.data_settings.forcing.data_dimension.space)
time_name = Symbol(info.experiment.data_settings.forcing.data_dimension.time)
num_type = info.helpers.numbers.num_type
axes_values = Dict(Symbol(first(f_axis)) => last(f_axis) for f_axis ∈ forcing_axes)
forcing_space_dims = Tuple(DD.Dim{s_dim}(axes_values[s_dim]) for s_dim ∈ space_dims)
forcing_time_dim = haskey(axes_values, time_name) ? DD.Dim{time_name}(axes_values[time_name]) : nothing
open_files = Dict{String,Any}()
var_names, var_sources = listParameterVariables(parameter_input, open_files)
lookup = getParameterLookup(parameter_input)
lookup_cube = isnothing(lookup) ? nothing : getLookupCube(lookup, forcing_cubes, space_dims, time_name)
lookup_has_time = !isnothing(lookup_cube) && hasdim(lookup_cube, time_name)
# the lazy cube of each parameter is computed once for an eager run, which reads
# it per location
keep_lazy = info.helpers.run.run_lazy isa DoRunLazy
table_rows = []
map_classes = []
data = map(var_names, var_sources) do var_name, var_source
dataset = openParameterFile!(open_files, var_source.data_path)
attributes = readVariableAttributes(dataset, var_source.source_variable, var_source.data_path)
p_row = resolveParameterVariable(var_name, var_source, parameter_input, attributes, info)
is_spatiotemporal = p_row.space_time_type == "spatiotemporal"
p_values = if !isnothing(lookup)
if is_spatiotemporal
error("The parameter variable `$(var_name)` of the class map by `$(lookup.lookup_variable)` is spatiotemporal. A class map varies in time through its lookup variable, so its variables are spatial.")
end
by_class = getParameterValuesByClass(dataset[var_source.source_variable], lookup, lookup_cube, p_row, num_type, var_name)
push!(map_classes, by_class.classes)
by_class
elseif is_spatiotemporal
isnothing(forcing_time_dim) && error("The parameter variable `$(var_name)` is spatiotemporal, but the forcing has no `$(time_name)` dimension.")
getParameterValuesByLocationAndTime(dataset[var_source.source_variable], forcing_axes, forcing_space_dims, forcing_time_dim, p_row, num_type, var_name)
else
getParameterValuesByLocation(dataset[var_source.source_variable], forcing_axes, forcing_space_dims, p_row, num_type, var_name)
end
push!(table_rows, (; p_row..., n_nan=p_values.n_nan, n_clamped=p_values.n_clamped))
print_info(nothing, @__FILE__, @__LINE__, "`$(var_name)` = <$(p_row.space_time_type)> `$(p_row.name_full)` ($(p_row.units)) from `$(var_source.source_variable)` in `$(var_source.data_path)` * $(p_row.unit_factor)", n_m=4)
# kept as a cube on the dimensions and coordinates of the forcing, so a lazy
# run can map over it together with the forcing
p_dims = hasdim(p_values.data, time_name) ? (forcing_time_dim, forcing_space_dims...) : forcing_space_dims
YAXArray(p_dims, keep_lazy ? p_values.data.data : Array(p_values.data.data))
end
if !isnothing(lookup)
allequal(map_classes) || error("The variables of the class map by `$(lookup.lookup_variable)` have different classes. All need the same `$(lookup.lookup_variable)` coordinates.")
if lookup_has_time
# counting the classes would read the whole lookup variable, which is far
# too much for a global forcing at every time step
print_info(nothing, @__FILE__, @__LINE__, "the class map by `$(lookup.lookup_variable)` varies in time, so the classes that are not in the map are not counted", n_m=4)
else
warnClassesNotInMap(lookup, findClassesNotInMap(lookup_cube, first(map_classes)))
end
end
parameter_table = Table([table_rows...])
full_names = parameter_table.name_full
duplicates = unique(filter(n -> count(==(n), full_names) > 1, full_names))
if !isempty(duplicates)
error("The parameters input sets $(duplicates) more than once. Keep one variable per parameter.")
end
has_time = map(p_data -> hasdim(p_data, time_name), Tuple(data))
f_types = map(t -> t ? ForcingWithTime() : ForcingWithoutTime(), has_time)
p_dims = map(t -> t ? (time_name,) : (), has_time)
lookup_variable = lookup_has_time ? lookup.lookup_variable : nothing
return (; variables=Tuple(var_names), data=Tuple(data), parameter_table=parameter_table, f_types=f_types, dims=p_dims, lookup_variable=lookup_variable)
endgetSpatialSubset
Sindbad.DataLoaders.getSpatialSubset Function
getSpatialSubset(ss, v)Extracts a spatial subset of data based on specified spatial subsetting type/strategy.
Arguments
ss: Spatial subset parameters or geometry defining the region of interestv: Data to be spatially subset
Returns
Spatially subset data according to the specified parameters
Note
The function assumes input data and spatial parameters are in compatible formats
Examples
julia> using Sindbad
julia> # Get spatial subset from configuration
julia> # subset_data = getSpatialSubset(spatial_subset_config, data_cube)Code
function getSpatialSubset(ss, v)
if isa(ss, Dict)
ss = dict_to_namedtuple(ss)
end
if !isnothing(ss)
ssname = propertynames(ss)
for ssn ∈ ssname
ss_r = getproperty(ss, ssn)
# a variable without the dimension, e.g., a purely temporal one, is kept
# as it is
if !isnothing(ss_r) && hasdim(v, ssn)
ss_range = collect(ss_r)
checkSubsetValues(v, ssn, ss_range)
ss_typeName = Symbol("Space" * string(ssn))
v = spatialSubset(v, ss_range, getfield(Types, ss_typeName)())
end
end
end
return v
endmapCleanData
Sindbad.DataLoaders.mapCleanData Function
Maps and cleans data based on quality control parameters.
Arguments
_data: Raw input data to be cleaned_data_qc: Quality control data corresponding to input databounds_qc: Quality control bounds/thresholds_data_info: Additional information about the data::Val{T}: Value type parameter for dispatch
Returns
Cleaned and mapped data. Data points that fail the QC and invalid data points are NaN.
Note
This function performs quality control checks and data cleaning based on the provided bounds. Invalid data points are not filled, see cleanData. The exact behavior depends on the value type T.
Code
function mapCleanData(_data, _data_qc, bounds_qc, _data_info, ::Val{T}) where {T}
if !isnothing(bounds_qc) && !isnothing(_data_qc)
_data = map((da, dq) -> applyQCBound(da, dq, bounds_qc, NaN), _data, _data_qc)
end
vT = Val{T}()
_data = map(data_point -> cleanData(data_point, _data_info, vT), _data)
return _data
endprepCostOptions
Sindbad.DataLoaders.prepCostOptions Function
prepCostOptions(observations, cost_options, ::CostMethod)Prepares cost options for optimization by filtering variables with insufficient data points and setting up the required configurations.
Arguments:
observations: A NamedTuple or a vector of arrays containing observation data, uncertainties, and masks used for calculating performance metrics or loss.cost_options: A table listing each observation constraint and its configuration for calculating the loss or performance metric.::CostMethod: A type indicating the cost function method.
Returns:
- A filtered table of
cost_optionscontaining only valid variables with sufficient data points.
cost methods:
CostMethod
Abstract type for cost calculation methods in SINDBAD
Available methods/subtypes:
CostModelObs: cost calculation between model output and observationsCostModelObsLandTS: cost calculation between land model output and time series observationsCostModelObsMT: multi-threaded cost calculation between model output and observationsCostModelObsPriors: cost calculation between model output, observations, and priors. NOTE THAT THIS METHOD IS JUST A PLACEHOLDER AND DOES NOT CALCULATE PRIOR COST PROPERLY YET
Extended help
Examples
julia> using Sindbad
julia> # Prepare cost options for optimization
julia> # filtered_cost_options = prepCostOptions(observations, cost_options, CostModelObs())Notes:
The function iterates through the observation variables and checks if the number of valid data points meets the minimum threshold specified in
cost_options.min_data_points.Variables with insufficient data points are excluded from the returned
cost_options.The function modifies the
cost_optionstable by adding:obs_mask: The valid points of the observation and its uncertainty at their native resolution. Withaggr_obs, the model output is masked by it before it is aggregated.obs_yandobs_yσ: The observation and its uncertainty at the resolution of the cost. They are aggregated here once, and not in every cost evaluation.valids: Indices of valid data points for each variable at the resolution of the cost.valid_indices: The positions of the valid data points invalids, found once here. A view with a Bool mask finds them again at every call, which allocated an index vector three times per variable in every cost evaluation.is_valid: A boolean flag indicating whether the variable meets the minimum data point requirement.
Unnecessary fields such as
min_data_points,temporal_data_aggr, andaggr_funcare removed from the finalcost_options.
Code
function prepCostOptions end
function prepCostOptions(observations, cost_options)
return prepCostOptions(observations, cost_options, CostModelObs())
end
function prepCostOptions(observations, cost_options)
return prepCostOptions(observations, cost_options, CostModelObs())
end
function prepCostOptions(observations, cost_options, ::CostModelObsPriors)
return prepCostOptions(observations, cost_options, CostModelObs())
end
function prepCostOptions(observations, cost_options, ::CostModelObs)
obs_masks = []
obs_ys = []
obs_yσs = []
valids = []
valid_indices = []
is_valid = []
for cost_option in Table(cost_options)
y = observations[cost_option.obs_ind]
yσ = observations[cost_option.obs_ind+1]
obs_mask = Array(.!is_invalid_number.(y .* yσ))
y, yσ = aggregateObsData(y, yσ, obs_mask, cost_option, cost_option.aggr_obs)
idxs = Array(.!is_invalid_number.(y .* yσ))
push!(is_valid, sum(idxs) >= cost_option.min_data_points)
push!(obs_masks, obs_mask)
push!(obs_ys, y)
push!(obs_yσs, yσ)
push!(valids, idxs)
push!(valid_indices, findall(idxs))
end
cost_options = set_namedtuple_field(cost_options, (:obs_mask, obs_masks))
cost_options = set_namedtuple_field(cost_options, (:obs_y, obs_ys))
cost_options = set_namedtuple_field(cost_options, (:obs_yσ, obs_yσs))
cost_options = set_namedtuple_field(cost_options, (:valids, valids))
cost_options = set_namedtuple_field(cost_options, (:valid_indices, valid_indices))
cost_options = set_namedtuple_field(cost_options, (:is_valid, is_valid))
cost_options = drop_namedtuple_fields(cost_options, (:min_data_points, :temporal_data_aggr, :aggr_func,))
cost_option_table = Table(cost_options)
cost_options_table_filtered = filter(row -> row.is_valid === true , cost_option_table)
return cost_options_table_filtered
endresampleCube
Sindbad.DataLoaders.resampleCube Function
resampleCube(incube, target_dims, method, num_type)Puts a cube on the target dimensions with an interpolation method.
Arguments:
incube: the cubetarget_dims: a Tuple of the target dimensions that the cube hasmethod: a space or time interpolation methodnum_type: the number type of the resampled cube
Returns:
- A lazy cube with the target dimensions in place of those of
incube, in the same dimension order asincube, and with its properties.
Notes:
A space method with a
resampleRadiusgets, for each block of target grids, the source grids around it, seegetResampleBlock.A time method, or a space method without a radius, gets the whole axes: the target dimensions are core dimensions of an
xmap, so each call ofresampleSlice!gets e.g. a whole time series or a whole field, and the other dimensions are looped over. A read of part of the target grid still resamples the whole axes, which suits time, as a run reads whole time series.Missing values are NaN for the method.
Code
function resampleCube(incube, target_dims, method::InterpolationMethod, num_type)
radius = resampleRadius(method)
isnothing(radius) || return getResampleBlock(incube, target_dims, method, num_type, radius)
core_names, out_dims, source_axes, target_axes = getResampleAxes(incube, target_dims, method)
plan = resamplePlan(method, source_axes, target_axes)
out = xmap(resampleSlice!, incube ⊘ core_names;
output=XOutput(out_dims...; outtype=num_type, properties=incube.properties),
inplace=true,
allow_threads=true,
function_kwargs=(; source_axes, target_axes, method, num_type, plan))
# xmap puts the core dimensions first
in_order = DD.name.(DD.dims(incube))
return DD.name.(DD.dims(out)) == in_order ? out : permutedims(out, in_order)
end
function resampleCubeToTarget(incube, target_dims, method_setting, axis, num_type, var_name)
isnothing(target_dims) && return incube
cube_target_dims = filter(target_dim -> hasdim(incube, DD.name(target_dim)), target_dims)
needs_resample = any(cube_target_dims) do target_dim
!isSameDim(DD.lookup(incube, DD.name(target_dim)), DD.lookup(target_dim))
end
needs_resample || return incube
method = getInterpolationMethod(method_setting, axis)
if method isa Union{SpaceInterpNone,TimeInterpNone}
target_names = join(string.(DD.name.(cube_target_dims)), ", ")
print_info(nothing, @__FILE__, @__LINE__, "`$(var_name)` keeps its own $(target_names) because its interpolation_method is None", n_m=4)
return incube
end
return resampleCube(incube, cube_target_dims, method, num_type)
endresampleValues!
Sindbad.DataLoaders.resampleValues! Function
resampleValues!(yout, yin, source_axes, target_axes, method::InterpolationMethod,
plan)Fills yout on the target axes from the values yin on the source axes. This is the function to extend for a new interpolation method.
Arguments:
yout: the output array, with one dimension per target axisyin: the source values in the number type of the run, with NaN for missing values, and one dimension per source axissource_axes,target_axes: one NamedTuple per resampled axis in the order of the dimensions ofyinandyout, seegetAxisInfomethod: the interpolation method with its optionsplan: theresamplePlanof the method on these axes
Notes:
To add a method, add a subtype of
SpaceInterpolationMethodorTimeInterpolationMethodinsrc/Types/InputTypes.jlwith its options as fields with defaults and itspurpose, and a method of this function here. Its name in the settings is the type name withoutSpaceInterporTimeInterpin snake case, e.g."zhao2004"forSpaceInterpZhao2004. A method that only works along some axes also adds acheckInterpolationAxesmethod, and one with options that need checking acheckInterpolationMethod. A space method that only needs the source grids near a target grid adds aresampleRadiusmethod, without which it gets the whole field. What a method can compute from the axes alone goes into aresamplePlanmethod, so it is not computed again at every time step.All resampled axes of a cube come together, e.g. latitude and longitude for space, so a method can combine them, and the cell bounds allow methods that keep sums or means of cells.
Code
function resampleValues!(_, _, _, _, method::InterpolationMethod, _)
error("interpolation method `$(nameof(typeof(method)))` has no resampleValues! method. Add one in src/DataLoaders/resampleCube.jl, see the docstring of resampleValues!.")
end
function resampleValues!(yout, yin, _, _, method::GridInterpolationMethod, plan)
itp = Interpolations.extrapolate(Interpolations.interpolate(yin, interpolationType(method)), getExtrapolation(method.extrapolation))
for (I, idx) ∈ zip(CartesianIndices(yout), Iterators.product(plan...))
yout[I] = itp(idx...)
end
return nothing
end
function resampleValues!(yout, yin, source_axes, target_axes, method::TimeMonotonicInterpolationMethod, _)
x = only(source_axes).points
y = vec(yin)
if x[end] < x[1]
x, y = reverse(x), reverse(y)
end
itp = Interpolations.extrapolate(Interpolations.interpolate(x, y, interpolationType(method)), getExtrapolation(method.extrapolation))
for (i, x_target) ∈ enumerate(only(target_axes).points)
yout[i] = itp(x_target)
end
return nothing
end
function resampleValues!(yout, yin, _, _, ::SpaceInterpZhao2004, plan)
for I ∈ CartesianIndices(yout)
weighted_sum, weight_sum = 0.0, 0.0
for (source_index, weight) ∈ zip(plan.corners[I], plan.weights[I])
value = yin[source_index]
# an invalid grid is left out and the weights of the others are
# normalized among themselves, which the paper does not cover
isnan(value) && continue
weighted_sum += weight * value
weight_sum += weight
end
yout[I] = weight_sum > 0 ? weighted_sum / weight_sum : NaN
end
return nothing
endsaveParameterDataset
Sindbad.DataLoaders.saveParameterDataset Function
saveParameterDataset(params_ds, data_path)Saves the dataset of a parameter file.
Arguments:
params_ds: the dataset with the parameter variablesdata_path: the path of the file to write, a zarr store for the files Sindbad writes
Notes:
savedatasetcannot copy variables of strings, because it sizes its copy buffer from the size of the element type. The file is therefore created as a skeleton and each variable is written directly, which is fine for the small arrays of a parameter file.
Code
function saveParameterDataset(params_ds, data_path)
disk_ds = YAXArrays.savedataset(params_ds, path=data_path, overwrite=true, skeleton=true)
for (var_name, cube) ∈ params_ds.cubes
disk_ds.cubes[var_name].data[:] = Array(cube.data)
end
return data_path
endsubsetAndProcessCube
Sindbad.DataLoaders.subsetAndProcessCube Function
subsetAndProcessCube(yax, forcing_mask, tar_dims, _data_info, info, ::Val{num_type}; clean_data=true, yax_qc=nothing, bounds_qc=nothing) where {num_type}Subset and process YAX data according to specified parameters and quality control criteria.
Arguments
yax: YAX data to be processedforcing_mask: Mask to apply to the datatar_dims: Target dimensions_data_info: Data informationinfo: a SINDBAD NT that includes all information needed for setup and execution of an experiment::Val{num_type}: Value type parameter for numerical type specificationclean_data=true: Boolean flag to enable/disable data cleaning. With cleaning, invalid data points stay invalid as NaN, seecleanData. Without cleaning, which is used for masks and QC flags, invalid points are set to zero, as a mask cannot be NaN and a NaN QC flag would pass every QC bound.yax_qc=nothing: Optional quality control parameters for YAX databounds_qc=nothing: Optional boundary quality control parameters
Returns
Processed and subset YAX data according to specified parameters and quality controls.
Type Parameters
num_type: Numerical type specification for the processed data
Code
function subsetAndProcessCube(yax, forcing_mask, tar_dims, _data_info, info, ::Val{num_type}; clean_data=true, yax_qc=nothing, bounds_qc=nothing) where {num_type}
forcing_data_settings = info.experiment.data_settings.forcing
if !isnothing(forcing_mask)
yax = yax #todo: mask the forcing variables here depending on the mask of 1 and 0
end
if !isnothing(tar_dims)
permutes = getDimPermutation(YAXArrayBase.dimnames(yax), tar_dims)
@debug " -> permuting dimensions to $(tar_dims)..."
yax = permutedims(yax, permutes)
end
time_name = Symbol(forcing_data_settings.data_dimension.time)
if hasproperty(yax, time_name)
init_date = DateTime(info.helpers.dates.date_begin)
# date_end covers its whole timestep, so data stamped within the last period
# such as a daily value at 11:00 UTC is kept and matches the dates range.
last_date = DateTime(info.helpers.dates.date_end) + info.helpers.dates.timestep - Millisecond(1)
yax = getindex(yax; (time_name => init_date .. last_date,)...)
end
if hasproperty(forcing_data_settings, :subset)
yax = getSpatialSubset(forcing_data_settings.subset, yax)
end
# do a proper check on input type for forcing and if is not lazy do the cleanup
if info.helpers.run.run_lazy isa DoNotRunLazy
vNT = Val{num_type}()
if clean_data
yax = mapCleanData(yax, yax_qc, bounds_qc, _data_info, vNT)
else
# masks and QC flags: an invalid point is zero, i.e., false for a mask
yax = map(yax_point -> replace_invalid_number(yax_point, 0.0), yax)
yax = map(num_type, yax)
end
end
return yax
endtoDimStackArray
Sindbad.DataLoaders.toDimStackArray Function
Convert a stacked array into a DimensionalArray with specified dimensions and metadata.
Arguments
stackArr: The input stacked array to be convertedtime_interval: Time interval information for temporal dimensionp_names: Names of pools/variablesname: Optional keyword argument to specify the name of the dimension (default: :pools)
Returns
A DimensionalArray with proper dimensions and labels.
This function is useful for converting raw stacked arrays into properly dimensioned arrays with metadata, particularly for time series data with multiple pools or variables.
sourceCode
function toDimStackArray(stackArr, time_interval, p_names; name=:pools)
return DimArray(stackArr, (p_names=p_names, Ti=time_interval,); name=name,)
endyaxCubeToKeyedArray
Sindbad.DataLoaders.yaxCubeToKeyedArray Function
yaxCubeToKeyedArray(c)Convert a YAXArray cube to a KeyedArray.
Arguments
c: YAXArray input cube to be converted
Returns
KeyedArray representation of the input YAXArray cube
Description
Transforms a YAXArray data cube into a KeyedArray format, preserving the dimensional structure and associated metadata of the original cube.
sourceCode
function yaxCubeToKeyedArray(c)
t_dims = getSindbadDims(c);
return KeyedArray(Array(c.data); t_dims...)
endTypes
AllNaN
Sindbad.DataLoaders.AllNaN Type
AllNaN <: YAXArrays.DAT.ProcFilterSpecialized filter for YAXArrays to skip pixels with all NaN or missing values.
Description
This struct is used as a specialized filter in data processing pipelines to identify or handle cases where all values in a data segment are NaN (Not a Number).
source