Skip to content

CC BY-SA 4.0

Soil respiration

julia
using EasyHybrid
using CairoMakie

Create synthetic data

Exponential temperature relationship

Resp=Resp0exp(kT);Resp0=f(SM)
julia
using Random
Random.seed!(2314)

T = rand(500) .* 40 .- 10      # Random temperature
SM = rand(500) .* 0.8 .+ 0.1   # Random soil moisture
SM_fac = exp.(-8.0*(SM .- 0.6) .^ 2)
Resp0 = 1.1 .* SM_fac # Base respiration dependent on soil moisture
Resp = Resp0 .* exp.(0.07 .* T)
Resp_obs = Resp .+ randn(length(Resp)) .* 0.05 .* mean(Resp);  # Add some noise
julia
df = DataFrame(; T, SM, SM_fac, Resp0, Resp, Resp_obs)
first(df, 5)
5×6 DataFrame
RowTSMSM_facResp0RespResp_obs
Float64Float64Float64Float64Float64Float64
125.37430.1822960.2476310.2723941.609131.71876
2-2.161370.7983550.7299650.8029610.690220.733988
3-0.8544650.373790.664070.7304770.6880660.903229
414.26470.1482610.1954310.2149740.58350.63434
55.418210.7975350.7318630.8050491.176361.14584

Define the Process-based Model

julia
function Expo_resp_model(;T, Resp0, k)
    Resp_obs = Resp0 .* exp.(k .* T)
    return (; Resp_obs, Resp0, k)
end;

about Expo_resp_model

Mechanistic model for soil respiration based on exponential temperature response

Mathematical Model: Resp = Resp0 * exp(k * T). Implements Arrhenius-type temperature dependence for biological processes (true connection?).

Arguments

  • T : Air or soil temperature [°C]

  • Resp0 : Basal respiration rate - which is equal to Resp at T=0°C [μmol CO₂ m⁻² s⁻¹]

  • k : Temperature sensitivity parameter [°C⁻¹], typical range 0.05-0.15

Returns

  • Resp_obs : Predicted soil respiration [μmol CO₂ m⁻² s⁻¹]

  • Resp0 : Basal respiration (passed through)

  • k : Temperature sensitivity (passed through)

Scientific Context

Models temperature dependence of soil CO₂ efflux for carbon cycle studies. Exponential relationship reflects enzyme kinetics in microbial decomposition.

Configure Model Parameters

julia
parameters = (
    # name: (default, lower_bound, upper_bound) # Description
    k     = (0.01f0, 0.0f0, 0.2f0),  # Exponent
    Resp0 = (2.0f0,  0.0f0, 8.0f0),  # Basal respiration [μmol/m²/s]
);

Construct the Hybrid Model

julia
targets = [:Resp_obs]
forcings = [:T]
predictors = (Resp0=[:SM],);

Define global parameters (none for this model, Q10 is fixed)

julia
global_param_names = [:k]
1-element Vector{Symbol}:
 :k
julia
hybrid_model = constructHybridModel(
    predictors,
    forcings,
    targets,
    Expo_resp_model,
    parameters,
    global_param_names,
    scale_nn_outputs=false, # TODO `true` also works with good lower and upper bounds
    hidden_layers = [16, 16],
    activation = sigmoid,
    input_batchnorm = true
)
Neural Networks:
Resp0:
    Chain(
        layer_1 = BatchNorm(1, affine=false, track_stats=true),
        layer_2 = Dense(1 => 16, σ),        # 32 parameters
        layer_3 = Dense(16 => 16, σ),       # 272 parameters
        layer_4 = Dense(16 => 1),           # 17 parameters
    )         # Total: 321 parameters,
              #        plus 3 states.
Predictors: Resp0: [:SM]
Forcing: [:T]
Neural parameters: [:Resp0]
Global parameters: [:k]
Fixed parameters: Symbol[]
Scale NN outputs: false
Parameter defaults and bounds:
    HybridParams{typeof(Main.Expo_resp_model)}(
    ┌───────┬─────────┬───────┬───────┐
    │       │ default │ lower │ upper │
    ├───────┼─────────┼───────┼───────┤
    │     k │    0.01 │   0.0 │   0.2 │
    │ Resp0 │     2.0 │   0.0 │   8.0 │
    └───────┴─────────┴───────┴───────┘
    )

Train the Model

julia
out =  train(hybrid_model, df, (:k,); nepochs=300, batchsize=64,
    opt=AdamW(0.01, (0.9, 0.999), 0.01), loss_types=[:mse, :nse],
    training_loss=:nse, random_seed=123, yscale = identity,
    monitor_names=[:Resp0, :k],
    show_progress=false,
    hybrid_name="expo_response"
    );
[ Info: Check the saved output (.png, .mp4, .jld2) from training at: /home/runner/work/EasyHybrid.jl/EasyHybrid.jl/docs/build
[ Info: Returning best model from epoch 270 of 300 epochs with best validation loss wrt mse: 0.052962676