Sindbad.Experiment Module
ExperimentThe Experiment module provides tools for designing, running, and analyzing experiments in the SINDBAD framework. It integrates SINDBAD modules and utilities to streamline the experimental workflow, from data preparation to model execution and output analysis.
Purpose
High-level interface for conducting experiments using the SINDBAD framework (workflow orchestration + output handling).
Dependencies
Related (SINDBAD ecosystem)
OmniTools: Shared utilities.ErrorMetrics: Metric implementations used in cost/diagnostics.
Internal (within Sindbad)
Sindbad.DataLoadersSindbad.ParameterOptimizationSindbad.SetupSindbad.SimulationSindbad.VisualizationSindbadTEM
Included Files
runExperiment.jl: Experiment execution and orchestration.saveOutput.jl: Utilities for saving experiment outputs in supported formats.
Notes
- Designed to be extensible, enabling users to customize and expand the experimental workflow that combines different SINDBAD modules as needed.
Examples
julia> using Sindbad
julia> # Run a forward experiment from a configuration file
julia> # out = runExperimentForward("path/to/experiment_config.json")
julia> # Prepare experiment configuration and forcing
julia> # info, forcing = prepExperiment("path/to/experiment_config.json")
julia> # Run experiment with different modes
julia> # result = runExperiment(info, forcing, DoRunForward())Functions
prepExperiment
Sindbad.Experiment.prepExperiment Function
prepExperiment(sindbad_experiment::String; replace_info::Dict=Dict())Prepare experiment configuration, forcing data, and output settings.
Arguments
sindbad_experiment::String: Path to the experiment configuration filereplace_info::Dict: Dictionary of configuration overrides (default: empty Dict)
Returns
info::NamedTuple: A NamedTuple containing the experiment configurationforcing::NamedTuple: A NamedTuple containing the forcing data
Description
This function initializes an experiment by:
Reading and processing the experiment configuration
Setting up forcing data based on the configuration
Preparing output settings
Examples
julia> using Sindbad
julia> # Prepare experiment from configuration file
julia> # info, forcing = prepExperiment("experiment_config.json")
julia> # Prepare with configuration overrides
julia> # info, forcing = prepExperiment("experiment_config.json"; replace_info=Dict("output" => Dict("save_all" => true)))Code
function prepExperiment(sindbad_experiment::String; replace_info=Dict())
print_figlet_banner("SINDBAD")
info = getExperimentInfo(sindbad_experiment; replace_info=replace_info)
print_info_separator()
forcing = getForcing(info)
return info, forcing
endrunExperiment
Sindbad.Experiment.runExperiment Function
runExperiment(info::NamedTuple, forcing::NamedTuple, mode::RunFlag)Run a SINDBAD experiment in different modes.
Arguments
info::NamedTuple: A SINDBAD NamedTuple containing all information needed for setup and execution of an experimentforcing::NamedTuple: A forcing NamedTuple containing the forcing time series set for ALL locationsmode::RunFlag: Type dispatch parameter determining the mode of experiment:DoCalcCost: Calculate cost between model output and observationsDoRunForward: Run forward simulation without optimizationDoNotRunOptimization: Run without optimizationDoRunOptimization: Run with optimization enabled
Returns
For
DoCalcCostmode:(; forcing, info, loss=loss_vector, observation=obs_array, output=forward_output)
For
DoRunForwardorDoNotRunOptimizationmode:(; forcing, info, output=run_output)
For
DoRunOptimizationmode:(; forcing, info, observation=obs_array, params=run_output)
Description
This function is the main entry point for running SINDBAD experiments. It supports different modes of simulation:
Cost calculation: Compares model output with observations
Forward run: Executes the model without optimization
ParameterOptimization: Runs the model with parameter optimization
The function handles different spatial configurations and can operate on both single-pixel and spatial domains.
sourceCode
function runExperiment end
function runExperiment(info::NamedTuple, forcing::NamedTuple, ::DoCalcCost)
print_info_separator(sep_text="Forward Simulation + Cost Calculation")
observations = getObservation(info, forcing.helpers)
obs_array = [Array(_o) for _o in observations.data]; # TODO: necessary now for performance because view of keyedarray is slow
print_info(runExperiment, @__FILE__, @__LINE__, "do forward run...")
forward_output = runForward(forcing, info, DoNotRunLazy()).output
print_info(runExperiment, @__FILE__, @__LINE__, "calculate cost...")
cost_options = prepCostOptions(obs_array, info.optimization.cost_options)
loss_vector = metricVector(forward_output, obs_array, cost_options)
for _cp in Pair.(Pair.(cost_options.variable, nameof.(typeof.(cost_options.cost_metric))), loss_vector)
println(_cp)
end
return (; forcing, info, loss=loss_vector, observation=obs_array, output=forward_output)
end
function runExperiment(info::NamedTuple, forcing::NamedTuple, ::DoCalcCost)
print_info_separator(sep_text="Forward Simulation + Cost Calculation")
observations = getObservation(info, forcing.helpers)
obs_array = [Array(_o) for _o in observations.data]; # TODO: necessary now for performance because view of keyedarray is slow
print_info(runExperiment, @__FILE__, @__LINE__, "do forward run...")
forward_output = runForward(forcing, info, DoNotRunLazy()).output
print_info(runExperiment, @__FILE__, @__LINE__, "calculate cost...")
cost_options = prepCostOptions(obs_array, info.optimization.cost_options)
loss_vector = metricVector(forward_output, obs_array, cost_options)
for _cp in Pair.(Pair.(cost_options.variable, nameof.(typeof.(cost_options.cost_metric))), loss_vector)
println(_cp)
end
return (; forcing, info, loss=loss_vector, observation=obs_array, output=forward_output)
end
function runExperiment(info::NamedTuple, forcing::NamedTuple, ::Union{DoRunForward, DoNotRunOptimization})
run_output = runForward(forcing, info, info.helpers.run.run_lazy)
return (; forcing, info, output=run_output.output, restart=run_output.restart, spinup=run_output.spinup, output_saved=run_output.output_saved, lazy_store=run_output.lazy_store)
end
function runExperiment(info::NamedTuple, forcing::NamedTuple, ::DoRunOptimization)
info = withoutSpinupStore(info)
observations = getObservation(info, forcing.helpers)
additionaldims = setdiff(keys(forcing.helpers.sizes), info.experiment.data_settings.forcing.data_dimension.time)
run_output = nothing
if isempty(additionaldims)
print_info(runExperiment, @__FILE__, @__LINE__, "run optimization per pixel...")
run_output = optimizeTEMLazy(forcing, info.tem, info.optimization, observations; max_cache=getYaxMaxCache(info, (forcing.data..., observations.data...); n_forcing=length(forcing.data)))
else
print_info(runExperiment, @__FILE__, @__LINE__, "run optimization for spatial domain...")
obs_array = [Array(_o) for _o in observations.data]; # TODO: necessary now for performance because view of keyedarray is slow
optim_params = optimizeTEM(forcing, obs_array, info)
optim_file = joinpath(info.output.dirs.optimization, "model_parameters_optimized.csv")
print_info(runExperiment, @__FILE__, @__LINE__, "saving optimized parameters to file: $(optim_file)")
CSV.write(optim_file, optim_params)
saveParameterCubes(info, optim_params, forcing.helpers)
run_output = optim_params
end
return (; forcing, info, observation=obs_array, parameters=run_output)
end
function runExperimentCost(sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
setExperimentMode!(replace_info, :cost)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
cost_output = runExperiment(info, forcing, info.helpers.run.calc_cost)
set_log_level()
return cost_output
end
function runExperimentForward(sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
print_info_separator(sep_text="Forward Simulation")
setExperimentMode!(replace_info, :forward)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
run_output = runExperiment(info, forcing, info.helpers.run.run_forward)
output_dims = getOutDims(info, forcing.helpers)
run_output.output_saved || saveOutCubes(info, values(run_output.output), output_dims, info.output.variables)
saveRestartCubes(info, run_output.restart)
saveSpinupCubes(info, run_output.spinup)
removeLazyStore(run_output.lazy_store)
# the figures need the forcing and output in memory, which a lazy run does not
# have
info.helpers.run.run_lazy isa DoRunLazy || plotForwardRun(info, forcing, run_output.output, run_output.spinup)
set_log_level()
return run_output
end
function runExperimentForwardParams(params_vector::Vector, sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
print_info_separator(sep_text="Forward Simulation with Input/Optimized Parameters")
print_info(runExperimentForwardParams, @__FILE__, @__LINE__, "running forward simulation with input/optimized parameters...", n_m=1)
replace_info = deepcopy(replace_info)
setExperimentMode!(replace_info, :cost)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
default_models = info.models.forward;
default_output = runForward(default_models, forcing, info, DoNotRunLazy()).output
parameter_table = info.optimization.parameter_table;
optimized_models = updateModelParameters(parameter_table, default_models, params_vector)
optimized_run = runForward(optimized_models, forcing, info, DoNotRunLazy())
optimized_output = optimized_run.output
output_dims = getOutDims(info, forcing.helpers)
saveOutCubes(info, values(optimized_output), output_dims, info.output.variables)
saveRestartCubes(info, optimized_run.restart)
saveSpinupCubes(info, optimized_run.spinup)
plotForwardRun(info, forcing, default_output, optimized_run.spinup; opt_output=optimized_output)
forward_output = (; optimized=optimized_output, default=default_output)
set_log_level()
return (; forcing, info, output=forward_output, spinup=optimized_run.spinup)
end
function runExperimentFullOutput(sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
print_info_separator(sep_text="Forward Simulation + Output of All Variables")
replace_info = deepcopy(replace_info)
setExperimentMode!(replace_info, :forward)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
info = @set info.helpers.run.land_output_type = PreAllocArrayAll()
run_helpers = prepTEM(info.models.forward, forcing, info)
info = @set info.output.variables = run_helpers.output_vars
runTEM!(run_helpers.space_selected_models, run_helpers.space_forcing, run_helpers.space_spinup, run_helpers.loc_forcing_t, run_helpers.space_output, run_helpers.space_restart, run_helpers.space_land, run_helpers.tem_info; show_progress=true)
output_dims = run_helpers.output_dims
run_output = run_helpers.output_array
saveOutCubes(info, run_output, output_dims, run_helpers.output_vars)
saveRestartCubes(info, run_helpers.restart)
spinup = prepSpinupOut(info, run_helpers)
saveSpinupCubes(info, spinup)
full_output = (; Pair.(getUniqueVarNames(run_helpers.output_vars), run_output)...)
plotForwardRun(info, forcing, full_output, spinup)
set_log_level()
return (; forcing, info, output=full_output, spinup)
end
function runExperimentOpti(sindbad_experiment::String; replace_info=Dict(), display_level=:warn)
set_log_level(display_level)
print_info_separator(sep_text="ParameterOptimization Experiment")
setExperimentMode!(replace_info, :optimization)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
# the final forward run in runExperimentForwardParams prepares its own info, so
# it stores the spinup when the experiment says so
info = withoutSpinupStore(info)
run_helpers = prepTEM(info.models.forward, forcing, info)
opti_output = runExperiment(info, forcing, info.helpers.run.run_optimization)
fp_output = runExperimentForwardParams(opti_output.parameters.optimized, sindbad_experiment; replace_info=replace_info)
print_info_separator()
cost_options = prepCostOptions(opti_output.observation, info.optimization.cost_options)
loss_vector = metricVector(fp_output.output.optimized, opti_output.observation, cost_options)
loss_vector_def = metricVector(fp_output.output.default, opti_output.observation, cost_options)
loss_table = Table((; variable=cost_options.variable, metric=cost_options.cost_metric, loss_opt=loss_vector, loss_def=loss_vector_def))
println("Total loss:: optimized: $(sum(loss_vector)), default: $(sum(loss_vector_def))")
display(loss_table)
savePerformanceMetrics(fp_output.info, cost_options, opti_output.observation, fp_output.output.default, fp_output.output.optimized)
parameters_nt = convertParametersToNamedTuple(opti_output.parameters, :model, :name)
out_opti = (; forcing, cost_options, run_helpers, info=fp_output.info, loss=loss_table, observation=opti_output.observation, output=fp_output.output, spinup=fp_output.spinup, parameters=opti_output.parameters, parameters_nt=parameters_nt)
# the forcing, debug and spinup figures of the final forward run come from
# runExperimentForwardParams
plotTimeSeries(out_opti)
plotPerformanceHistograms(out_opti)
return out_opti
end
function runExperimentSensitivity(sindbad_experiment::String; replace_info=Dict(), batch=true, display_level=:warn)
set_log_level(display_level)
print_info_separator(sep_text="Sensitivity Analysis Experiment")
setExperimentMode!(replace_info, :optimization)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
info = withoutSpinupStore(info)
observations = getObservation(info, forcing.helpers)
obs_array = [Array(_o) for _o in observations.data]; # TODO: necessary now for performance because view of keyedarray is slow
opti_helpers = prepOpti(forcing, obs_array, info, info.optimization.run_options.cost_method; algorithm_info_field=:sensitivity_analysis);
# parameter_table = opti_helpers.parameter_table
p_bounds=Tuple.(Pair.(opti_helpers.lower_bounds,opti_helpers.upper_bounds))
cost_function = opti_helpers.cost_function
# d_opt = getproperty(Setup, :GSAMorris)()
method_options =info.optimization.sensitivity_analysis.options
sensitivity = globalSensitivity(cost_function, method_options, p_bounds, info.optimization.sensitivity_analysis.method, batch=batch)
sensitivity_output = (; opti_helpers..., info=info, forcing=forcing, obs_array=obs_array, observations=observations,sensitivity=sensitivity, p_bounds=p_bounds)
set_log_level(:info)
sensitivity_output_file = joinpath(info.output.dirs.data, "sensitivity_analysis_$(nameof(typeof(info.optimization.sensitivity_analysis.method)))_$(length(opti_helpers.cost_vector))-cost_evals.jld2")
print_info(runExperimentSensitivity, @__FILE__, @__LINE__, "saving sensitivity output to file: `$(sensitivity_output_file)`", n_m=1)
@save sensitivity_output_file sensitivity_output
return sensitivity_output
endrunExperimentCost
Sindbad.Experiment.runExperimentCost Function
runExperimentCost(sindbad_experiment::String; replace_info::Dict=Dict(), display_level::Symbol=:info)Calculate cost for a given experiment through the runExperiment function in DoCalcCost mode.
Arguments
sindbad_experiment::String: Path to the experiment configuration filereplace_info::Dict: Dictionary of configuration overrides (default: empty Dict)display_level::Symbol: Level of the log and of whatprint_infoshows, seeset_log_level(default: :info)
Returns
- A NamedTuple containing the experiment results including cost calculations
Code
function runExperimentCost(sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
setExperimentMode!(replace_info, :cost)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
cost_output = runExperiment(info, forcing, info.helpers.run.calc_cost)
set_log_level()
return cost_output
endrunExperimentForward
Sindbad.Experiment.runExperimentForward Function
runExperimentForward(sindbad_experiment::String; replace_info::Dict=Dict(), display_level::Symbol=:info)Run forward simulation for a given experiment through the runExperiment function in DoRunForward mode.
Arguments
sindbad_experiment::String: Path to the experiment configuration filereplace_info::Dict: Dictionary of configuration overrides (default: empty Dict)display_level::Symbol: Level of the log and of whatprint_infoshows, seeset_log_level(default: :info)
Returns
- A NamedTuple containing the experiment results including model outputs
Code
function runExperimentForward(sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
print_info_separator(sep_text="Forward Simulation")
setExperimentMode!(replace_info, :forward)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
run_output = runExperiment(info, forcing, info.helpers.run.run_forward)
output_dims = getOutDims(info, forcing.helpers)
run_output.output_saved || saveOutCubes(info, values(run_output.output), output_dims, info.output.variables)
saveRestartCubes(info, run_output.restart)
saveSpinupCubes(info, run_output.spinup)
removeLazyStore(run_output.lazy_store)
# the figures need the forcing and output in memory, which a lazy run does not
# have
info.helpers.run.run_lazy isa DoRunLazy || plotForwardRun(info, forcing, run_output.output, run_output.spinup)
set_log_level()
return run_output
end
function runExperimentForwardParams(params_vector::Vector, sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
print_info_separator(sep_text="Forward Simulation with Input/Optimized Parameters")
print_info(runExperimentForwardParams, @__FILE__, @__LINE__, "running forward simulation with input/optimized parameters...", n_m=1)
replace_info = deepcopy(replace_info)
setExperimentMode!(replace_info, :cost)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
default_models = info.models.forward;
default_output = runForward(default_models, forcing, info, DoNotRunLazy()).output
parameter_table = info.optimization.parameter_table;
optimized_models = updateModelParameters(parameter_table, default_models, params_vector)
optimized_run = runForward(optimized_models, forcing, info, DoNotRunLazy())
optimized_output = optimized_run.output
output_dims = getOutDims(info, forcing.helpers)
saveOutCubes(info, values(optimized_output), output_dims, info.output.variables)
saveRestartCubes(info, optimized_run.restart)
saveSpinupCubes(info, optimized_run.spinup)
plotForwardRun(info, forcing, default_output, optimized_run.spinup; opt_output=optimized_output)
forward_output = (; optimized=optimized_output, default=default_output)
set_log_level()
return (; forcing, info, output=forward_output, spinup=optimized_run.spinup)
endrunExperimentForwardParams
Sindbad.Experiment.runExperimentForwardParams Function
runExperimentForwardParams(params_vector::Vector, sindbad_experiment::String; replace_info::Dict=Dict(), display_level::Symbol=:info)Run forward simulation of the model with default as well as modified settings with input/optimized parameters through call of the runTEM! function.
Arguments
params_vector::Vector: Vector of parameters to use for the simulationsindbad_experiment::String: Path to the experiment configuration filereplace_info::Dict: Dictionary of configuration overrides (default: empty Dict)display_level::Symbol: Level of the log and of whatprint_infoshows, seeset_log_level(default: :info)
Returns
- A NamedTuple containing both default and optimized model outputs
Code
function runExperimentForwardParams(params_vector::Vector, sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
print_info_separator(sep_text="Forward Simulation with Input/Optimized Parameters")
print_info(runExperimentForwardParams, @__FILE__, @__LINE__, "running forward simulation with input/optimized parameters...", n_m=1)
replace_info = deepcopy(replace_info)
setExperimentMode!(replace_info, :cost)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
default_models = info.models.forward;
default_output = runForward(default_models, forcing, info, DoNotRunLazy()).output
parameter_table = info.optimization.parameter_table;
optimized_models = updateModelParameters(parameter_table, default_models, params_vector)
optimized_run = runForward(optimized_models, forcing, info, DoNotRunLazy())
optimized_output = optimized_run.output
output_dims = getOutDims(info, forcing.helpers)
saveOutCubes(info, values(optimized_output), output_dims, info.output.variables)
saveRestartCubes(info, optimized_run.restart)
saveSpinupCubes(info, optimized_run.spinup)
plotForwardRun(info, forcing, default_output, optimized_run.spinup; opt_output=optimized_output)
forward_output = (; optimized=optimized_output, default=default_output)
set_log_level()
return (; forcing, info, output=forward_output, spinup=optimized_run.spinup)
endrunExperimentFullOutput
Sindbad.Experiment.runExperimentFullOutput Function
runExperimentFullOutput(sindbad_experiment::String; replace_info::Dict=Dict(), display_level::Symbol=:info)Run forward simulation of the model through runExperiment function in DoRunForward mode but with all output variables saved.
Arguments
sindbad_experiment::String: Path to the experiment configuration filereplace_info::Dict: Dictionary of configuration overrides (default: empty Dict)display_level::Symbol: Level of the log and of whatprint_infoshows, seeset_log_level(default: :info)
Returns
- A NamedTuple containing the complete model outputs
Code
function runExperimentFullOutput(sindbad_experiment::String; replace_info=Dict(), display_level=:info)
set_log_level(display_level)
print_info_separator(sep_text="Forward Simulation + Output of All Variables")
replace_info = deepcopy(replace_info)
setExperimentMode!(replace_info, :forward)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
info = @set info.helpers.run.land_output_type = PreAllocArrayAll()
run_helpers = prepTEM(info.models.forward, forcing, info)
info = @set info.output.variables = run_helpers.output_vars
runTEM!(run_helpers.space_selected_models, run_helpers.space_forcing, run_helpers.space_spinup, run_helpers.loc_forcing_t, run_helpers.space_output, run_helpers.space_restart, run_helpers.space_land, run_helpers.tem_info; show_progress=true)
output_dims = run_helpers.output_dims
run_output = run_helpers.output_array
saveOutCubes(info, run_output, output_dims, run_helpers.output_vars)
saveRestartCubes(info, run_helpers.restart)
spinup = prepSpinupOut(info, run_helpers)
saveSpinupCubes(info, spinup)
full_output = (; Pair.(getUniqueVarNames(run_helpers.output_vars), run_output)...)
plotForwardRun(info, forcing, full_output, spinup)
set_log_level()
return (; forcing, info, output=full_output, spinup)
endrunExperimentOpti
Sindbad.Experiment.runExperimentOpti Function
runExperimentOpti(sindbad_experiment::String; replace_info::Dict=Dict(), display_level::Symbol=:warn)Run optimization experiment through runExperiment function in DoRunOptimization mode, followed by forward run with optimized parameters.
Arguments
sindbad_experiment::String: Path to the experiment configuration filereplace_info::Dict: Dictionary of configuration overrides (default: empty Dict)display_level::Symbol: Level of the log and of whatprint_infoshows, seeset_log_level(default: :warn)
Returns
- A NamedTuple containing optimization results, model outputs, and cost metrics
Code
function runExperimentOpti(sindbad_experiment::String; replace_info=Dict(), display_level=:warn)
set_log_level(display_level)
print_info_separator(sep_text="ParameterOptimization Experiment")
setExperimentMode!(replace_info, :optimization)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
# the final forward run in runExperimentForwardParams prepares its own info, so
# it stores the spinup when the experiment says so
info = withoutSpinupStore(info)
run_helpers = prepTEM(info.models.forward, forcing, info)
opti_output = runExperiment(info, forcing, info.helpers.run.run_optimization)
fp_output = runExperimentForwardParams(opti_output.parameters.optimized, sindbad_experiment; replace_info=replace_info)
print_info_separator()
cost_options = prepCostOptions(opti_output.observation, info.optimization.cost_options)
loss_vector = metricVector(fp_output.output.optimized, opti_output.observation, cost_options)
loss_vector_def = metricVector(fp_output.output.default, opti_output.observation, cost_options)
loss_table = Table((; variable=cost_options.variable, metric=cost_options.cost_metric, loss_opt=loss_vector, loss_def=loss_vector_def))
println("Total loss:: optimized: $(sum(loss_vector)), default: $(sum(loss_vector_def))")
display(loss_table)
savePerformanceMetrics(fp_output.info, cost_options, opti_output.observation, fp_output.output.default, fp_output.output.optimized)
parameters_nt = convertParametersToNamedTuple(opti_output.parameters, :model, :name)
out_opti = (; forcing, cost_options, run_helpers, info=fp_output.info, loss=loss_table, observation=opti_output.observation, output=fp_output.output, spinup=fp_output.spinup, parameters=opti_output.parameters, parameters_nt=parameters_nt)
# the forcing, debug and spinup figures of the final forward run come from
# runExperimentForwardParams
plotTimeSeries(out_opti)
plotPerformanceHistograms(out_opti)
return out_opti
endrunExperimentSensitivity
Sindbad.Experiment.runExperimentSensitivity Function
runExperimentSensitivity(sindbad_experiment::String; replace_info::Dict=Dict(), batch::Bool=true, display_level::Symbol=:warn)Run sensitivity analysis for a given experiment.
Arguments
sindbad_experiment::String: Path to the experiment configuration filereplace_info::Dict: Dictionary of configuration overrides (default: empty Dict)batch::Bool: Whether to run sensitivity analysis in batch mode (default: true)display_level::Symbol: Level of the log and of whatprint_infoshows, seeset_log_level(default: :warn)
Returns
- A NamedTuple containing sensitivity analysis results and related data
Code
function runExperimentSensitivity(sindbad_experiment::String; replace_info=Dict(), batch=true, display_level=:warn)
set_log_level(display_level)
print_info_separator(sep_text="Sensitivity Analysis Experiment")
setExperimentMode!(replace_info, :optimization)
info, forcing = prepExperiment(sindbad_experiment; replace_info=replace_info)
info = withoutSpinupStore(info)
observations = getObservation(info, forcing.helpers)
obs_array = [Array(_o) for _o in observations.data]; # TODO: necessary now for performance because view of keyedarray is slow
opti_helpers = prepOpti(forcing, obs_array, info, info.optimization.run_options.cost_method; algorithm_info_field=:sensitivity_analysis);
# parameter_table = opti_helpers.parameter_table
p_bounds=Tuple.(Pair.(opti_helpers.lower_bounds,opti_helpers.upper_bounds))
cost_function = opti_helpers.cost_function
# d_opt = getproperty(Setup, :GSAMorris)()
method_options =info.optimization.sensitivity_analysis.options
sensitivity = globalSensitivity(cost_function, method_options, p_bounds, info.optimization.sensitivity_analysis.method, batch=batch)
sensitivity_output = (; opti_helpers..., info=info, forcing=forcing, obs_array=obs_array, observations=observations,sensitivity=sensitivity, p_bounds=p_bounds)
set_log_level(:info)
sensitivity_output_file = joinpath(info.output.dirs.data, "sensitivity_analysis_$(nameof(typeof(info.optimization.sensitivity_analysis.method)))_$(length(opti_helpers.cost_vector))-cost_evals.jld2")
print_info(runExperimentSensitivity, @__FILE__, @__LINE__, "saving sensitivity output to file: `$(sensitivity_output_file)`", n_m=1)
@save sensitivity_output_file sensitivity_output
return sensitivity_output
endsaveOutCubes
Sindbad.Experiment.saveOutCubes Function
saveOutCubes(data_dir, global_metadata, var_pairs, data, data_dims, out_format, t_step, <: OutputStrategy)
saveOutCubes(info, out_cubes, output_dims, output_vars)saves the output variables from the run as one file
Arguments:
data_dir: directory of the output files, which are named after the variablesglobal_metadata: a collection of global metadata information to write to the output filedata: data to be written to filedata_dims: a vector of dimension of data for each variable to be written to a filevar_pairs: a tuple of pairs of sindbad variables to write including the field and subfield of land as the first and last elementout_format: format of the output filet_step: a string for time step of the model run to be used in the units attribute of variablescompression: keyword for the compression level of NetCDF and Zarr files, with -1 for no compression. The defaultnothinguses 4 for NetCDF and 5 for Zarr.digits: keyword for the number of significant decimal digits kept in the data. The defaultnothingwrites the data at full precision.<: OutputStrategy: Dispatch type indicating file output mode with the following options:::DoSaveSingleFile: single file with all the variables::DoNotSaveSingleFile: single file per variable
note: this function is overloaded to handle different dispatch types and the version with fewer arguments is used as a shorthand for the single file output mode
Examples
julia> using Sindbad
julia> # Save output cubes (shorthand version)
julia> # saveOutCubes(info, out_cubes, output_dims, output_vars)
julia> # Save to single file
julia> # saveOutCubes(data_dir, global_metadata, var_pairs, data, data_dims, "nc", t_step, DoSaveSingleFile())Code
function saveOutCubes end
function saveOutCubes(data_dir, global_metadata, data, data_dims, var_pairs, out_format, t_step, ::DoSaveSingleFile; compression=nothing, digits=nothing)
print_info(saveOutCubes, @__FILE__, @__LINE__, "saving one file for all variables")
catalog_names = getVarFull.(var_pairs)
variable_names = getUniqueVarNames(var_pairs)
all_yax = Tuple(getYaxForVariable.(roundToDigits.(data, digits), data_dims, variable_names, catalog_names, Ref(t_step)))
data_path = joinpath(data_dir, "all_variables.$(out_format)")
print_info(nothing, @__FILE__, @__LINE__, "saved all variables to `$(data_path)`", n_m=4)
ds_new = DataLoaders.YAXArrays.Dataset(; (; zip(variable_names, all_yax)...)..., properties=global_metadata)
DataLoaders.YAXArrays.savedataset(ds_new; path=data_path, append=true, overwrite=true, getSaveKwargs(out_format, compression)...)
return nothing
end
function saveOutCubes(data_dir, global_metadata, data, data_dims, var_pairs, out_format, t_step, ::DoSaveSingleFile; compression=nothing, digits=nothing)
print_info(saveOutCubes, @__FILE__, @__LINE__, "saving one file for all variables")
catalog_names = getVarFull.(var_pairs)
variable_names = getUniqueVarNames(var_pairs)
all_yax = Tuple(getYaxForVariable.(roundToDigits.(data, digits), data_dims, variable_names, catalog_names, Ref(t_step)))
data_path = joinpath(data_dir, "all_variables.$(out_format)")
print_info(nothing, @__FILE__, @__LINE__, "saved all variables to `$(data_path)`", n_m=4)
ds_new = DataLoaders.YAXArrays.Dataset(; (; zip(variable_names, all_yax)...)..., properties=global_metadata)
DataLoaders.YAXArrays.savedataset(ds_new; path=data_path, append=true, overwrite=true, getSaveKwargs(out_format, compression)...)
return nothing
end
function saveOutCubes(data_dir, global_metadata, data, data_dims, var_pairs, out_format, t_step, ::DoNotSaveSingleFile; compression=nothing, digits=nothing)
print_info(saveOutCubes, @__FILE__, @__LINE__, "saving one file per variable")
catalog_names = getVarFull.(var_pairs)
variable_names = getUniqueVarNames(var_pairs)
for vn ∈ eachindex(var_pairs)
catalog_name = catalog_names[vn]
variable_name = variable_names[vn]
data_yax = getYaxForVariable(roundToDigits(data[vn], digits), data_dims[vn], variable_name, catalog_name, t_step)
data_path = joinpath(data_dir, "$(variable_name).$(out_format)")
print_info(nothing, @__FILE__, @__LINE__, "saved `$(variable_name)` to `$(data_path)`", n_m=4)
ds_new = DataLoaders.YAXArrays.Dataset(; (variable_name => data_yax,)..., properties=global_metadata)
DataLoaders.YAXArrays.savedataset(ds_new; path=data_path, overwrite=true, getSaveKwargs(out_format, compression)...)
end
return nothing
end
function saveOutCubes(info, out_cubes, output_dims, output_vars)
saveOutCubes(info.output.dirs.data, info.output.file_info.global_metadata, out_cubes, output_dims, output_vars, info.output.format, info.experiment.basics.temporal_resolution, info.helpers.run.save_single_file; compression=info.output.data_compression.level, digits=info.output.data_compression.digits)
end
function saveOutCubes(info, out_cubes::DataLoaders.YAXArrays.Dataset, output_dims, output_vars)
t_step = info.experiment.basics.temporal_resolution
out_format = info.output.format
data_dir = info.output.dirs.data
save_kwargs = getSaveKwargs(out_format, info.output.data_compression.level)
variable_names = getUniqueVarNames(output_vars)
foreach(variable_names, getVarFull.(output_vars)) do variable_name, catalog_name
merge!(out_cubes.cubes[variable_name].properties, getVariableInfo(catalog_name, t_step))
end
global_metadata = info.output.file_info.global_metadata
if info.helpers.run.save_single_file isa DoSaveSingleFile
data_path = joinpath(data_dir, "all_variables.$(out_format)")
ds_new = DataLoaders.YAXArrays.Dataset(; (v => out_cubes.cubes[v] for v ∈ variable_names)..., properties=global_metadata)
DataLoaders.YAXArrays.savedataset(ds_new; path=data_path, overwrite=true, save_kwargs...)
print_info(nothing, @__FILE__, @__LINE__, "saved all variables to `$(data_path)`", n_m=4)
else
for variable_name ∈ variable_names
data_path = joinpath(data_dir, "$(variable_name).$(out_format)")
ds_new = DataLoaders.YAXArrays.Dataset(; (variable_name => out_cubes.cubes[variable_name],)..., properties=global_metadata)
DataLoaders.YAXArrays.savedataset(ds_new; path=data_path, overwrite=true, save_kwargs...)
print_info(nothing, @__FILE__, @__LINE__, "saved `$(variable_name)` to `$(data_path)`", n_m=4)
end
end
return nothing
endsaveParameterCubes
Sindbad.Experiment.saveParameterCubes Function
saveParameterCubes(info, parameter_table, forcing_helpers)Saves the parameters of an optimization run as a parameter file with one variable per parameter, and writes its params.json next to it.
Arguments:
info: a SINDBAD NamedTuple with all information needed for setup and execution of an experimentparameter_table: the table of the optimized parameters, whoseoptimizedcolumn is savedforcing_helpers: the forcing helpers, which give the spatial dimensions and coordinates
Returns:
- A NamedTuple with the
data_pathof the parameter file and thejson_path.
Notes:
The file is in the optimization output directory, next to the csv of the optimized parameters. Its name is
params.zarr, seegetParameterFilePath. The json has the same name with a.jsonextension.Each variable is named
<model>__<parameter>, seeparameterVariableName, and has the spatial dimensions and coordinates of the forcing without time, e.g., the site names.The optimization gives one value for the whole domain, so every location has the same value.
The attributes of each variable are the columns of the parameter table, see
parameterVariableAttributes. The values are in the units of this run, given by theunitsandtimescale_runattributes.The file and its json can be given as the
parametersconfig file of another experiment to set the parameters per location. Single-site files can be concatenated along the site dimension first.
Code
function saveParameterCubes(info, parameter_table, forcing_helpers)
data_path = getParameterFilePath(info)
space_dims = Symbol.(forcing_helpers.dimensions.space)
axes_values = Dict(Symbol(first(f_axis)) => last(f_axis) for f_axis ∈ forcing_helpers.axes)
yax_dims = Tuple(DataLoaders.DD.Dim{s_dim}(collect(axes_values[s_dim])) for s_dim ∈ space_dims)
space_sizes = Tuple(forcing_helpers.sizes[s_dim] for s_dim ∈ space_dims)
variable_attributes = Pair{String,Any}[]
all_yax = Pair{Symbol,Any}[]
for p_index ∈ eachindex(parameter_table.name)
p_value = parameter_table.optimized[p_index]
if !(p_value isa Number)
@warn "The parameter `$(parameter_table.name_full[p_index])` is not a scalar and is not saved to the parameter file."
continue
end
variable_name = parameterVariableName(parameter_table.model[p_index], parameter_table.name[p_index])
attributes = parameterVariableAttributes(parameter_table, p_index)
p_data = fill(p_value, space_sizes...)
push!(all_yax, Symbol(variable_name) => DataLoaders.YAXArray(yax_dims, p_data, Dict{String,Any}(attributes)))
push!(variable_attributes, variable_name => attributes)
end
param_metadata = copy(info.output.file_info.global_metadata)
param_metadata["parameter_source"] = "optimization"
param_metadata["experiment_name"] = info.experiment.basics.name
param_metadata["experiment_domain"] = info.experiment.basics.domain
param_metadata["temporal_resolution"] = info.experiment.basics.temporal_resolution
params_ds = DataLoaders.YAXArrays.Dataset(; all_yax..., properties=param_metadata)
print_info(saveParameterCubes, @__FILE__, @__LINE__, "saving the optimized parameters to the parameter file `$(data_path)`", n_m=4)
DataLoaders.saveParameterDataset(params_ds, data_path)
json_path = first(splitext(rstrip(data_path, '/'))) * ".json"
writeParameterInputJson(json_path, abspath(data_path), variable_attributes; space_dimensions=space_dims)
print_info(nothing, @__FILE__, @__LINE__, "saved the parameters input of the parameter file to `$(json_path)`", n_m=4)
return (; data_path, json_path)
endsavePerformanceMetrics
Sindbad.Experiment.savePerformanceMetrics Function
savePerformanceMetrics(info, cost_options, observations, def_output, opt_output)Saves the performance of the default and the optimized output for every observation constraint of the cost, in every metric of ErrorMetrics, to performance.csv next to the csv of the optimized parameters, see getPerformanceFilePath. Returns the path.
Arguments:
info: the experiment information of the forward runscost_options: the cost options fromprepCostOptionsobservations: the observation arrays of the costdef_output,opt_output: the output of the forward runs with the default and the optimized parameters
Notes:
One row per constraint, with its cost metric and weight, the number of points, the mean of the observation and of both outputs,
loss_defandloss_opt, and a<metric>_defand<metric>_optcolumn per metric.loss_defandloss_optare the weighted cost of the constraint, as in the optimization.The other columns use the data of the cost, after its aggregation, on the points where the observation, its uncertainty and both outputs are valid, so that both outputs are compared on the same points.
A metric that is not finite, e.g. a correlation of one point, is NaN.
Code
function savePerformanceMetrics(info, cost_options, observations, def_output, opt_output)
metrics = errorMetrics()
loss_def = metricVector(def_output, observations, cost_options)
loss_opt = metricVector(opt_output, observations, cost_options)
rows = map(enumerate(cost_options)) do (i, cost_option)
y, yσ, y_def = getData(def_output, observations, cost_option)
_, _, y_opt = getData(opt_output, observations, cost_option)
y, yσ, y_def, y_opt = (Float64.(vec(collect(a))) for a ∈ (y, yσ, y_def, y_opt))
valid = isfinite.(y) .& isfinite.(yσ) .& isfinite.(y_def) .& isfinite.(y_opt)
y, yσ, y_def, y_opt = y[valid], yσ[valid], y_def[valid], y_opt[valid]
metric_values = Pair{Symbol,Float64}[]
for m ∈ metrics, (run_name, ŷ) ∈ (("def", y_def), ("opt", y_opt))
push!(metric_values, Symbol("$(nameof(typeof(m)))_$(run_name)") => performanceMetricValue(m, ŷ, y, yσ))
end
(; variable=String(cost_option.variable),
cost_metric=String(nameof(typeof(cost_option.cost_metric))),
cost_weight=Float64(cost_option.cost_weight), n=length(y),
mean_obs=meanOrNaN(y), mean_def=meanOrNaN(y_def), mean_opt=meanOrNaN(y_opt),
loss_def=Float64(loss_def[i]), loss_opt=Float64(loss_opt[i]), metric_values...)
end
performance_path = getPerformanceFilePath(info)
CSV.write(performance_path, rows)
print_info(savePerformanceMetrics, @__FILE__, @__LINE__, "saved the performance of the default and optimized output to `$(performance_path)`", n_m=4)
return performance_path
endsaveRestartCubes
Sindbad.Experiment.saveRestartCubes Function
saveRestartCubes(info, restart)Saves the pools of a forward run after the spinup step whose save_restart is true to the restart file. That is the last step unless the settings mark another one.
Arguments:
info: a SINDBAD NamedTuple with all information needed for setup and execution of an experimentrestart: the restart data of the run, which is either:a NamedTuple with the
variables,dimsanddataarrays of an eager run (run_helpers.restart)a NamedTuple with the restart
Datasetandvariablesof a lazy run (prepTEMLazy)
Notes:
The file is
restartin the data output directory, in the output format, for examplerestart.zarr.It is always one file with all variables, whatever
save_single_fileis set to.The variables have no time dimension. A variable with more than one layer has a layer dimension followed by the spatial dimensions, and all other variables have the spatial dimensions only. The spinup runs before the simulation period, so the
restart_dateglobal attribute is the start date of the simulation. Thespinup_sequence_methodglobal attribute is the name of the spinup sequence method, e.g.,sequence_listorsequence_with_age, andspinup_modeis the spinup mode of the step that saved the restart, e.g.,LoopTEM.Each variable is named after its subfield in
land.pools, so the file can be added to the forcing for theSpinupInputspinup mode.
Code
function saveRestartCubes(info, restart)
data_path = getRestartFilePath(info)
restart_metadata = getRestartMetadata(info, restart)
restart_ds = getRestartDataset(info, restart, restart_metadata)
print_info(nothing, @__FILE__, @__LINE__, "saving pools after the spinup step with save_restart to `$(data_path)`", n_m=4)
DataLoaders.YAXArrays.savedataset(restart_ds; path=data_path, overwrite=true, getSaveKwargs(info)...)
return nothing
endsaveSpinupCubes
Sindbad.Experiment.saveSpinupCubes Function
saveSpinupCubes(info, spinup)Saves cEco and TWS at every stored step of the spinup to the spinup file.
Arguments:
info: a SINDBAD NamedTuple with all information needed for setup and execution of an experimentspinup: the stored spinup of an eager run fromprepSpinupOut, ornothing, in which case no file is written
Notes:
The file is
spinupin the data output directory, in the output format, for examplespinup.zarr.The variables and their dimensions are described in
prepSpinupOut.The
spinup_sequence_methodglobal attribute is the name of the spinup sequence method, andspinup_sequencelists the steps of the sequence of the first location, asmode:forcing:n_repeat:n_timestepsseparated by;.
Code
function saveSpinupCubes end
saveSpinupCubes(_, ::Nothing) = nothing
function saveSpinupCubes(info, spinup)
data_path = getSpinupFilePath(info)
spinup_metadata = copy(info.output.file_info.global_metadata)
method_name = string(nameof(typeof(info.spinup.method)))
spinup_metadata["spinup_sequence_method"] = lowercase(replace(method_name, r"(?<!^)([A-Z])" => s"_\1"))
spinup_metadata["spinup_sequence"] = join([join((nameof(typeof(seq.spinup_mode)), seq.forcing, seq.n_repeat, seq.n_timesteps), ":") for seq ∈ first(spinup.sequences)], ";")
t_step = info.experiment.basics.temporal_resolution
all_yax = map(spinup.variables, spinup.dims, spinup.data) do var_pair, var_dims, var_data
var_info = isnothing(var_pair) ? Dict{String,Any}("long_name" => "index of the spinup sequence step of the row, 0 before the spinup and -1 after the end of the sequence", "units" => "-") : getVariableInfo(getVarFull(var_pair), t_step)
DataLoaders.YAXArray(var_dims, var_data, var_info)
end
spinup_ds = DataLoaders.YAXArrays.Dataset(; zip(spinup.names, all_yax)..., properties=spinup_metadata)
print_info(nothing, @__FILE__, @__LINE__, "saving pools at every stored spinup step to `$(data_path)`", n_m=4)
DataLoaders.YAXArrays.savedataset(spinup_ds; path=data_path, overwrite=true, getSaveKwargs(info)...)
return nothing
end
function saveSpinupCubes(info, spinup)
data_path = getSpinupFilePath(info)
spinup_metadata = copy(info.output.file_info.global_metadata)
method_name = string(nameof(typeof(info.spinup.method)))
spinup_metadata["spinup_sequence_method"] = lowercase(replace(method_name, r"(?<!^)([A-Z])" => s"_\1"))
spinup_metadata["spinup_sequence"] = join([join((nameof(typeof(seq.spinup_mode)), seq.forcing, seq.n_repeat, seq.n_timesteps), ":") for seq ∈ first(spinup.sequences)], ";")
t_step = info.experiment.basics.temporal_resolution
all_yax = map(spinup.variables, spinup.dims, spinup.data) do var_pair, var_dims, var_data
var_info = isnothing(var_pair) ? Dict{String,Any}("long_name" => "index of the spinup sequence step of the row, 0 before the spinup and -1 after the end of the sequence", "units" => "-") : getVariableInfo(getVarFull(var_pair), t_step)
DataLoaders.YAXArray(var_dims, var_data, var_info)
end
spinup_ds = DataLoaders.YAXArrays.Dataset(; zip(spinup.names, all_yax)..., properties=spinup_metadata)
print_info(nothing, @__FILE__, @__LINE__, "saving pools at every stored spinup step to `$(data_path)`", n_m=4)
DataLoaders.YAXArrays.savedataset(spinup_ds; path=data_path, overwrite=true, getSaveKwargs(info)...)
return nothing
end