Skip to content

Building Models Examples

EasyHybrid.jl allows constructing diverse modeling architectures using the unified HybridModel struct. Previously, users defined bespoke structs (like LinearHM, RespirationRbQ10) for different configurations. Here we demonstrate how those legacy model architectures can be trivially constructed via HybridModel.

Setup

First, let's load our required packages:

julia
using EasyHybrid

1. Linear Hybrid Model

This is a basic model with one neural network predicting a coefficient α, and an explicit global parameter β. The equation is: ŷ = α * x + β

Process-Based Definition

julia
linear_mechanistic(; x, α, β) = (; obs = α .* x .+ β)
linear_mechanistic (generic function with 1 method)

Parameter Setup

julia
params_linear = (
    α = (1.0f0, 0.0f0, 2.0f0),
    β = (1.5f0, -1.0f0, 3.0f0),
)
(α = (1.0f0, 0.0f0, 2.0f0), β = (1.5f0, -1.0f0, 3.0f0))

HybridModel Construction

We use x as forcing data, predict α with a neural network based on some predictors a and b, and leave β as a globally optimized constant parameter.

julia
lhm = constructHybridModel(
    [:a, :b],          # predictors for the NN (predicts α)
    [:x],              # forcing variable
    [:obs],            # targets
    linear_mechanistic, # our mechanistic model
    params_linear,     # parameter container
    [],              # parameters predicted by the NN
    [];              # globally optimized constant parameters
    hidden_layers = [4, 4],
    activation = tanh
)
Hybrid Model (Single NN)
Neural Network: 
  Chain(
      layer_1 = WrappedFunction(identity),
      layer_2 = Dense(2 => 4, tanh),                # 12 parameters
      layer_3 = Dense(4 => 4, tanh),                # 20 parameters
      layer_4 = Dense(4 => 1),                      # 5 parameters
  )         # Total: 37 parameters,
            #        plus 0 states.
Configuration:
  predictors = [:a, :b]
  forcing = [:x]
  targets = [:obs]
  mechanistic_model = linear_mechanistic
  neural_param_names = [:α]
  global_param_names = [:β]
  fixed_param_names = Symbol[]
  scale_nn_outputs = false
  start_from_default = true
  config = (; hidden_layers = [4, 4], activation = tanh, scale_nn_outputs = false, input_batchnorm = false, start_from_default = true,)

Parameters:
  ┌───┬─────────┬───────┬───────┐
  │   │ default │ lower │ upper │
  ├───┼─────────┼───────┼───────┤
  │ α │     1.0 │   0.0 │   2.0 │
  │ β │     1.5 │  -1.0 │   3.0 │
  └───┴─────────┴───────┴───────┘

2. Respiration Rb Q10

A single NN predicting Rb for a Q10 temperature-sensitive respiration formulation. The equation is: R_soil = Rb * Q10^(0.1 * (Temp - 15))

Process-Based Definition

julia
function mRbQ10(; Temp, Rb, Q10)
    R_soil = @. Rb * Q10^(0.1f0 * (Temp - 15.0f0))
    return (; R_soil)
end
mRbQ10 (generic function with 1 method)

Parameter Setup

julia
params_rbq10 = (
    Rb = (1.0f0, 0.0f0, 5.0f0),
    Q10 = (1.5f0, 1.0f0, 3.0f0),
)
(Rb = (1.0f0, 0.0f0, 5.0f0), Q10 = (1.5f0, 1.0f0, 3.0f0))

HybridModel Construction

julia
m_rbq10 = constructHybridModel(
    [:SWC, :TA], # predictors for Rb
    [:Temp],     # forcing variable
    [:R_soil],   # targets
    mRbQ10,      # mechanistic model
    params_rbq10,
    [:Rb],       # predicted by NN
    [:Q10];      # globally optimized
    hidden_layers = [8, 8]
)
Hybrid Model (Single NN)
Neural Network: 
  Chain(
      layer_1 = WrappedFunction(identity),
      layer_2 = Dense(2 => 8, tanh),                # 24 parameters
      layer_3 = Dense(8 => 8, tanh),                # 72 parameters
      layer_4 = Dense(8 => 1),                      # 9 parameters
  )         # Total: 105 parameters,
            #        plus 0 states.
Configuration:
  predictors = [:SWC, :TA]
  forcing = [:Temp]
  targets = [:R_soil]
  mechanistic_model = mRbQ10
  neural_param_names = [:Rb]
  global_param_names = [:Q10]
  fixed_param_names = Symbol[]
  scale_nn_outputs = false
  start_from_default = true
  config = (; hidden_layers = [8, 8], activation = tanh, scale_nn_outputs = false, input_batchnorm = false, start_from_default = true,)

Parameters:
  ┌─────┬─────────┬───────┬───────┐
  │     │ default │ lower │ upper │
  ├─────┼─────────┼───────┼───────┤
  │  Rb │     1.0 │   0.0 │   5.0 │
  │ Q10 │     1.5 │   1.0 │   3.0 │
  └─────┴─────────┴───────┴───────┘

3. Respiration Components

A single NN outputting 3 distinct parameters (Rb_het, Rb_root, Rb_myc).

Process-Based Definition

julia
function rs_comp(; Temp, Rb_het, Rb_root, Rb_myc, Q10_het, Q10_root, Q10_myc)
    R_het = @. Rb_het * Q10_het^(0.1f0 * (Temp - 15.0f0))
    R_root = @. Rb_root * Q10_root^(0.1f0 * (Temp - 15.0f0))
    R_myc = @. Rb_myc * Q10_myc^(0.1f0 * (Temp - 15.0f0))
    R_soil = R_het .+ R_root .+ R_myc
    return (; R_soil, R_het, R_root, R_myc)
end
rs_comp (generic function with 1 method)

Parameter Setup

julia
params_rs_comp = (
    Rb_het = (1.0f0, 0.0f0, 5.0f0),
    Rb_root = (1.0f0, 0.0f0, 5.0f0),
    Rb_myc = (1.0f0, 0.0f0, 5.0f0),
    Q10_het = (1.5f0, 1.0f0, 3.0f0),
    Q10_root = (1.5f0, 1.0f0, 3.0f0),
    Q10_myc = (1.5f0, 1.0f0, 3.0f0),
)
(Rb_het = (1.0f0, 0.0f0, 5.0f0), Rb_root = (1.0f0, 0.0f0, 5.0f0), Rb_myc = (1.0f0, 0.0f0, 5.0f0), Q10_het = (1.5f0, 1.0f0, 3.0f0), Q10_root = (1.5f0, 1.0f0, 3.0f0), Q10_myc = (1.5f0, 1.0f0, 3.0f0))

HybridModel Construction

julia
m_rs_comp = constructHybridModel(
    [:SWC, :TA],  # predictors for all 3 Rb parameters
    [:Temp],
    [:R_soil],
    rs_comp,
    params_rs_comp,
    [:Rb_het, :Rb_root, :Rb_myc],
    [:Q10_het, :Q10_root, :Q10_myc];
    hidden_layers = [16, 16]
)
Hybrid Model (Single NN)
Neural Network: 
  Chain(
      layer_1 = WrappedFunction(identity),
      layer_2 = Dense(2 => 16, tanh),               # 48 parameters
      layer_3 = Dense(16 => 16, tanh),              # 272 parameters
      layer_4 = Dense(16 => 3),                     # 51 parameters
  )         # Total: 371 parameters,
            #        plus 0 states.
Configuration:
  predictors = [:SWC, :TA]
  forcing = [:Temp]
  targets = [:R_soil]
  mechanistic_model = rs_comp
  neural_param_names = [:Rb_het, :Rb_root, :Rb_myc]
  global_param_names = [:Q10_het, :Q10_root, :Q10_myc]
  fixed_param_names = Symbol[]
  scale_nn_outputs = false
  start_from_default = true
  config = (; hidden_layers = [16, 16], activation = tanh, scale_nn_outputs = false, input_batchnorm = false, start_from_default = true,)

Parameters:
  ┌──────────┬─────────┬───────┬───────┐
  │          │ default │ lower │ upper │
  ├──────────┼─────────┼───────┼───────┤
  │   Rb_het │     1.0 │   0.0 │   5.0 │
  │  Rb_root │     1.0 │   0.0 │   5.0 │
  │   Rb_myc │     1.0 │   0.0 │   5.0 │
  │  Q10_het │     1.5 │   1.0 │   3.0 │
  │ Q10_root │     1.5 │   1.0 │   3.0 │
  │  Q10_myc │     1.5 │   1.0 │   3.0 │
  └──────────┴─────────┴───────┴───────┘

4. Flux Partitioning with Multiple NNs

A multi-NN architecture predicting RUE (Radiation Use Efficiency) and Rb from different sets of predictors.

Process-Based Definition

julia
function flux_part(; SW_IN, TA, RUE, Rb, Q10)
    GPP = @. SW_IN * RUE / 12.011f0
    RECO = @. Rb * Q10^(0.1f0 * (TA - 15.0f0))
    NEE = RECO .- GPP
    return (; NEE, GPP, RECO)
end
flux_part (generic function with 1 method)

Parameter Setup

julia
params_flux = (
    RUE = (1.0f0, 0.0f0, 5.0f0),
    Rb = (1.0f0, 0.0f0, 5.0f0),
    Q10 = (1.5f0, 1.0f0, 3.0f0),
)
(RUE = (1.0f0, 0.0f0, 5.0f0), Rb = (1.0f0, 0.0f0, 5.0f0), Q10 = (1.5f0, 1.0f0, 3.0f0))

HybridModel Construction

By passing a NamedTuple to predictors, HybridModel automatically provisions an independent Neural Network for each key.

julia
predictors_multi = (
    RUE = [:SWC, :TA, :SW_IN],
    Rb = [:SWC, :TA],
)

m_flux = constructHybridModel(
    predictors_multi, # Triggers Multi-NN construction
    [:SW_IN, :TA],    # Forcing variables
    [:NEE],           # Targets
    flux_part,        # Mechanistic model
    params_flux,
    [:Q10];           # Global parameter
    hidden_layers = (RUE = [8, 8], Rb = [4, 4]), # Custom architectures per NN
    activation = (RUE = Lux.sigmoid, Rb = tanh)
)
Hybrid Model (Multi NN)
Neural Networks:
  RUE:
    Chain(
        layer_1 = WrappedFunction(identity),
        layer_2 = Dense(3 => 8, σ),                   # 32 parameters
        layer_3 = Dense(8 => 8, σ),                   # 72 parameters
        layer_4 = Dense(8 => 1),                      # 9 parameters
    )         # Total: 113 parameters,
              #        plus 0 states.
  Rb:
    Chain(
        layer_1 = WrappedFunction(identity),
        layer_2 = Dense(2 => 4, tanh),                # 12 parameters
        layer_3 = Dense(4 => 4, tanh),                # 20 parameters
        layer_4 = Dense(4 => 1),                      # 5 parameters
    )         # Total: 37 parameters,
              #        plus 0 states.
Configuration:
  predictors:
    RUE = [:SWC, :TA, :SW_IN]
    Rb = [:SWC, :TA]
  forcing = [:SW_IN, :TA]
  targets = [:NEE]
  mechanistic_model = flux_part
  neural_param_names = [:RUE, :Rb]
  global_param_names = [:Q10]
  fixed_param_names = Symbol[]
  scale_nn_outputs = false
  start_from_default = true
  config = (; hidden_layers = (RUE = [8, 8], Rb = [4, 4]), activation = (RUE = NNlib.σ, Rb = tanh), scale_nn_outputs = false, input_batchnorm = false, start_from_default = true,)

Parameters:
  ┌─────┬─────────┬───────┬───────┐
  │     │ default │ lower │ upper │
  ├─────┼─────────┼───────┼───────┤
  │ RUE │     1.0 │   0.0 │   5.0 │
  │  Rb │     1.0 │   0.0 │   5.0 │
  │ Q10 │     1.5 │   1.0 │   3.0 │
  └─────┴─────────┴───────┴───────┘

5. Process-Based Model (Zero NNs)

A purely process-based configuration where all parameters are optimized globally, and no Neural Networks are built.

Process-Based Definition

julia
function mRbQ10_0(; Temp, Rb, Q10)
    R_soil = @. Rb * Q10^(0.1f0 * (Temp - 0.0f0))
    return (; R_soil)
end
mRbQ10_0 (generic function with 1 method)

HybridModel Construction

Passing an empty Symbol[] array to predictors prevents any Neural Networks from being created.

julia
m_pbm = constructHybridModel(
    Symbol[],      # No predictors -> No Neural Network
    [:Temp],       # Forcing
    [:R_soil],     # Target
    mRbQ10_0,
    params_rbq10,
    Symbol[],      # No neural params
    [:Rb, :Q10]    # Both are optimized as global parameters
)
Hybrid Model (Single NN)
Neural Network: 
  Chain(
      layer_1 = Lux.NoOpLayer(),
  )         # Total: 0 parameters,
            #        plus 0 states.
Configuration:
  predictors = Symbol[]
  forcing = [:Temp]
  targets = [:R_soil]
  mechanistic_model = mRbQ10_0
  neural_param_names = Symbol[]
  global_param_names = [:Rb, :Q10]
  fixed_param_names = Symbol[]
  scale_nn_outputs = false
  start_from_default = true
  config = (; hidden_layers = [32, 32], activation = tanh, scale_nn_outputs = false, input_batchnorm = false, start_from_default = true,)

Parameters:
  ┌─────┬─────────┬───────┬───────┐
  │     │ default │ lower │ upper │
  ├─────┼─────────┼───────┼───────┤
  │  Rb │     1.0 │   0.0 │   5.0 │
  │ Q10 │     1.5 │   1.0 │   3.0 │
  └─────┴─────────┴───────┴───────┘

Summary

As demonstrated above, HybridModel provides a highly flexible, unified interface. By simply modifying the predictors argument and your mechanistic function, you can rapidly scale from a purely process-based model, to a single Neural Network hybrid model, all the way up to complex multi-Neural Network architectures!