EasyHybrid.jl
Documentation for EasyHybrid.jl.
EasyHybrid.EasyHybrid Module
EasyHybrid.jlEasyHybrid is a Julia package for hybrid machine learning models, combining neural networks and traditional statistical methods. It provides tools for data preprocessing, model training, and evaluation, making it easier to build and deploy hybrid models.
The hybrid model combines a neural network h(x; θ), with inputs x and learnable parameters θ, together with a mechanistic model M(·, z; ϕ) driven by forcings z and parameterized by ϕ, where ϕ may be known, learned from data, or fixed.
- Documentation https://earthyscience.github.io/EasyHybrid.jl/
EasyHybrid.DataConfig Type
Configuration for data preparation and loading.
Controls array types, observation shuffling, data splitting, cross-validation, and sequence construction for time-series training.
sourceEasyHybrid.HybridModel Type
HybridModel{T, P} <: LuxCore.AbstractLuxContainerLayer{(:NNs,)}A unified hybrid model struct that handles both single and multi neural network architectures. It combines predictive neural networks (NNs) with a mechanistic_model to form a differentiable hybrid model.
EasyHybrid.HybridModel Method
(m::HybridModel)(ds_k::Tuple, ps, st)Forward pass of the hybrid model. Evaluates the neural networks to predict parameters, merges them with scaled global parameters and fixed parameters, and executes the mechanistic model. Returns a tuple (out, st_new).
EasyHybrid.InputBatchNorm Type
InputBatchNorm(chs; kwargs...)A wrapper around BatchNorm that handles 3D sequence input (features, timesteps, batch).
Lux's BatchNorm expects channels in the penultimate dimension, which works for 2D input (features, batch) but fails for 3D input (features, timesteps, batch) where features are in dim 1. This wrapper reshapes 3D input to 2D (features, timesteps * batch) before normalization, then reshapes back. For 2D input, delegates directly to BatchNorm.
EasyHybrid.LoggingLoss Type
LoggingLossA structure to define a logging loss function for hybrid models.
Arguments
loss_types: A vector of loss specifications (Symbol, Function or Tuple)Symbol: predefined loss, e.g.
:mseFunction: custom loss function, e.g.
custom_lossTuple: function with args/kwargs:
(f, args): positional args, e.g.(weighted_loss, (0.5,))(f, kwargs): keyword args, e.g.(scaled_loss, (scale=2.0,))(f, args, kwargs): both, e.g.(complex_loss, (0.5,), (scale=2.0,))
training_loss: The loss specification to use during training (same format as above)extra_loss: Optional function(ŷ, ps; kwargs...) -> NamedTuple(or splattable collection) added to training loss (default:nothing)agg: Function to aggregate losses across targets, e.g.sumormeantrain_mode: If true, usestraining_loss; otherwise usesloss_types.
Examples
# Simple predefined loss
logging = LoggingLoss(
loss_types=[:mse, :mae],
training_loss=:mse
)
# Custom loss function
custom_loss(ŷ, y) = mean(abs2, ŷ .- y)
logging = LoggingLoss(
loss_types=[:mse, custom_loss],
training_loss=custom_loss
)
# With arguments/kwargs
weighted_loss(ŷ, y, w) = w * mean(abs2, ŷ .- y)
scaled_loss(ŷ, y; scale=1.0) = scale * mean(abs2, ŷ .- y)
logging = LoggingLoss(
loss_types=[:mse, (weighted_loss, (0.5,)), (scaled_loss, (scale=2.0,))],
training_loss=(weighted_loss, (0.5,))
)EasyHybrid.ParameterContainer Type
ParameterContainer{NT <: NamedTuple, T}A container for holding the parameter definitions of a model, including their default values, lower bounds, and upper bounds.
sourceEasyHybrid.PerTarget Type
PerTarget(losses)A wrapper to indicate that a tuple of losses should be applied on a per-target basis.
sourceEasyHybrid.RecurrenceOutputDense Type
RecurrenceOutputDense(in_dims => out_dims, [activation])A layer that wraps a Dense layer to handle sequence outputs from Recurrence layers.
When a Recurrence layer has return_sequence=true, it outputs a tuple/vector of arrays (one per timestep). This layer broadcasts the Dense operation over each timestep and reshapes the result to (features, timesteps, batch) format.
Arguments
in_dims::Int: Input dimension (should match Recurrence output dimension)out_dims::Int: Output dimensionactivation: Activation function (default:identity)
Example
# Instead of manually creating:
broadcast_layer = @compact(; layer = Dense(15 => 15)) do x
y = map(layer, x)
@return permutedims(stack(y; dims = 3), (1, 3, 2))
end
# Simply use:
Chain(
Recurrence(LSTMCell(15 => 15), return_sequence = true),
RecurrenceOutputDense(15 => 15)
)EasyHybrid.TrainConfig Type
Configuration for training a hybrid model.
Controls all aspects of the training process including optimization, loss computation, data handling, output, and visualization.
sourceEasyHybrid.TrainResults Type
Output of train, containing the full training history, model state, and diagnostics.
EasyHybrid.WrappedTuples Type
WrappedTuples(vec::Vector{<:NamedTuple})Wraps a vector of named tuples to allow dot-access to each field as a vector.
sourceEasyHybrid._apply_loss Function
_apply_loss(ŷ, y, y_nan, loss_spec)Helper function to apply the appropriate loss function based on the specification type.
Arguments
ŷ: Predictions for a single targety: Target values for a single targety_nan: NaN mask for a single targetloss_spec: Loss specification (Symbol, Function, or Tuple)
Returns
- Computed loss value
EasyHybrid._build_optim_loss Method
Build the scalar loss closure consumed by Optimization.jl, called as loss_fn(p, data). data is an already device-placed / Array-converted batch (shape ((x, forcings), (y, mask))), produced once per (mini)batch by collect_dim_data in the caller — not inside this closure. Keeping the data prep out of the loss is important: L-BFGS line searches call the objective many times per iteration, so re-running collect_dim_data (NamedTuple rebuilds, Array copies, gdev transfers) on every evaluation was a major slowdown, especially on the minibatch path. It also keeps the closure trivially Zygote-differentiable (no pairs(...)/∇map in the AD tape).
EasyHybrid._compute_loss Function
_compute_loss(ŷ, y, y_nan, targets, loss_spec, agg::Function)
_compute_loss(ŷ, y, y_nan, targets, loss_types::Vector, agg::Function)Compute the loss for the given predictions and targets using the specified training loss (or vector of losses) type and aggregation function.
Arguments:
ŷ: Predicted values.y: Target values.y_nan: Mask for NaN values.targets: The targets for which the loss is computed.loss_spec: The loss type to use during training, e.g.,:mse.loss_types::Vector: A vector of loss types to compute, e.g.,[:mse, :mae].agg::Function: The aggregation function to apply to the computed losses, e.g.,sumormean.
Returns a single loss value if loss_spec is provided, or a NamedTuple of losses for each type in loss_types.
EasyHybrid._get_target_nan Function
_get_target_nan(y_nan, target)Helper function to extract target-specific values from y_nan. Supports NamedTuple, KeyedArray, AbstractDimArray, and callables (functions).
EasyHybrid._get_target_y Function
_get_target_y(y, target)Helper function to extract target-specific values from y, handling cases where y can be a tuple of (y_obs, y_sigma). Supports NamedTuple, KeyedArray, AbstractDimArray, Tuple, and callables (functions).
EasyHybrid._init_nn_params Method
_init_nn_params(rng, m::HybridModel{<:Any, <:NamedTuple})Initialize parameters for a multi-neural network architecture. Returns a NamedTuple containing the initialized parameters for each sub-network.
EasyHybrid._init_nn_params Method
_init_nn_params(rng, m::HybridModel{<:Any, <:Vector})Initialize parameters for a single-neural network architecture. Returns a NamedTuple containing a single ps field with the network's parameters.
EasyHybrid._init_nn_states Method
_init_nn_states(rng, m::HybridModel{<:Any, <:NamedTuple})Initialize states for a multi-neural network architecture. Returns a NamedTuple containing the initialized states for each sub-network.
EasyHybrid._init_nn_states Method
_init_nn_states(rng, m::HybridModel{<:Any, <:Vector})Initialize states for a single-neural network architecture. Returns a NamedTuple containing a single st_nn field with the network's states.
EasyHybrid._is_named_leaf Method
_is_named_leaf(name, child, key) -> BoolReturn true when child is an array leaf whose parent field is key (e.g. a Dense layer's weight matrix).
EasyHybrid._run_minibatch! Method
Explicit "repeated minibatch" driver for the full_batch = false Optimization path (Le et al., 2011, ICML, §4.2). For each of cfg.nepochs outer passes we iterate a reshuffled DataLoader; on every fixed minibatch we run cfg.inner_maxiters optimizer iterations warm-started from the current ps, then resample. A validation EpochSnapshot (history / early-stopping / dashboard / checkpoint) is built once per outer pass, so patience is counted in outer passes — consistent with the Optimisers.jl loop.
maxiters / epochs from solve_kwargs are dropped (the per-minibatch budget is cfg.inner_maxiters and the pass count is cfg.nepochs); any remaining solve_kwargs (e.g. g_abstol, f_reltol) are forwarded to each inner solve.
EasyHybrid._run_nn Method
_run_nn(m::HybridModel{<:Any, <:NamedTuple}, ds_k::Tuple, ps, st)Execute the forward pass for a multi-neural network architecture. Applies each sub-network to its specific predictors, and applies scaling to the outputs if required. Returns scaled parameter values, updated states, and raw network outputs.
sourceEasyHybrid._run_nn Method
_run_nn(m::HybridModel{<:Any, <:Vector}, ds_k::Tuple, ps, st)Execute the forward pass for a single-neural network architecture. Applies the neural network to the given predictors, slices the output for multiple predicted parameters, and scales them if required. Returns scaled parameter values, updated states, and raw network outputs.
sourceEasyHybrid._train Method
_train(model, data, train_cfg, data_cfg, solve_kwargs)Dispatcher used by train(...): routes to the original 4-arg _train body (the Lux.Training / Optimisers.jl loop) when train_cfg.opt isa Optimisers.AbstractRule, or to _train_optimization (which delegates batch iteration to Optimization.jl) otherwise. solve_kwargs are forwarded to solve(...) on the Optimization.jl branch and warned about on the Optimisers.jl branch.
EasyHybrid._train_optimization Method
_train_optimization(model, data, train_cfg, data_cfg, solve_kwargs)Optimization.jl-based driver dispatched from _train whenever train_cfg.opt is not an Optimisers.AbstractRule (e.g. Optim.LBFGS() / Optimization.LBFGS()); see the SciML minibatching tutorial.
Two modes, selected by train_cfg.full_batch:
full_batch = true: pass the full training set as a single tuple to oneOptimizationProblemand a singlesolve(...). Batch-method idiom (the recommended L-BFGS setup): the objective is a single consistent function.solve_kwargs(e.g.maxiters,g_abstol,f_reltol) are splatted intosolve, andtrain_cfg.eval_everybuilds a validationEpochSnapshotevery N solver iterations via the callback.full_batch = false: explicit "repeated minibatch" loop grounded in Le et al., 2011 (On Optimization Methods for Deep Learning, ICML, §4.2). For each oftrain_cfg.nepochsouter passes we iterate a (reshuffled)DataLoaderand, on each fixed minibatch, runtrain_cfg.inner_maxitersoptimizer iterations (solve(...; maxiters = inner_maxiters)), warm-starting the next minibatch from the current parameters. Holding the minibatch fixed for a few iterations keeps the objective (and L-BFGS curvature pairs / line search) consistent — naive one-step-per-minibatch L-BFGS does not converge. A validationEpochSnapshotis built once per outer pass. Optimization.jl's ownDataLoaderiteration is not used here because it only applies to theOptimisers.jl-style solvers, not Optim.jl's L-BFGS.
Both modes honour train_cfg.promote_f64: promote ps to Float64 before optimization (workaround for Lux.jl#1260).
EasyHybrid.build_opt_state Method
build_opt_state(opt, ps::NamedTuple; default_rule = Optimisers.Adam())Build the optimizer state tree consumed by Lux.Training.TrainState / Optimisers.update!. Three forms of opt are accepted:
opt::Optimisers.AbstractRule— single rule applied to the whole parameter tree (delegates toOptimisers.setup(opt, ps)).opt::NamedTupleofOptimisers.AbstractRules — each rule is wired to the matching top-level branch ofpsviaOptimisers.setup(rule, ps[name]). Branches missing fromoptusedefault_rule.opt::NamedTupleof pre-built state trees (already returned by a priorOptimisers.setup) — used as-is. Form 2 and 3 can be mixed in the sameNamedTuple.
The returned state tree has the same top-level keys as ps.
Example
ps, _ = LuxCore.setup(rng, hybrid_model) # (; Rb = NN_ps, RUE = NN_ps, Q10 = [v])
# Form 2 — preferred, lets the framework call `Optimisers.setup`:
opt_state = build_opt_state(
(; Rb = Adam(1e-3), RUE = Adam(1e-3), Q10 = Descent(1e-2)),
ps,
)EasyHybrid.build_parameter_matrix Method
build_parameter_matrix(parameter_defaults_and_bounds::NamedTuple)Build a ComponentArray matrix from a NamedTuple containing parameter defaults and bounds.
This function converts a NamedTuple where each value is a tuple of (default, lower, upper) bounds into a ComponentArray with named axes for easy parameter management in hybrid models.
Arguments
parameter_defaults_and_bounds::NamedTuple: A NamedTuple where each key is a parameter name and each value is a tuple of (default, lower, upper) for that parameter.
Returns
ComponentArray: A 2D ComponentArray with:Row axis: Parameter names (from the NamedTuple keys)
Column axis: Bound types (:default, :lower, :upper)
Data: The parameter values organized in a matrix format
Example
# Define parameter defaults and bounds
parameter_defaults_and_bounds = (
θ_s = (0.464f0, 0.302f0, 0.700f0), # Saturated water content [cm³/cm³]
h_r = (1500.0f0, 1500.0f0, 1500.0f0), # Pressure head at residual water content [cm]
α = (log(0.103f0), log(0.01f0), log(7.874f0)), # Shape parameter [cm⁻¹]
n = (log(3.163f0 - 1), log(1.100f0 - 1), log(20.000f0 - 1)), # Shape parameter [-]
)
# Build the ComponentArray
parameter_matrix = build_parameter_matrix(parameter_defaults_and_bounds)
# Access specific parameter bounds
parameter_matrix.θ_s.default # Get default value for θ_s
parameter_matrix[:, :lower] # Get all lower bounds
parameter_matrix[:, :upper] # Get all upper boundsNotes
The function expects each value in the NamedTuple to be a tuple with exactly 3 elements
The order of bounds is always (default, lower, upper)
The resulting ComponentArray can be used for parameter optimization and constraint handling
EasyHybrid.compute_loss Method
compute_loss(HM, x, (y_t, y_nan), ps, st, logging::LoggingLoss)Main loss function for hybrid models that handles both training and evaluation modes.
Arguments
HM: The hybrid model (AbstractLuxContainerLayer or specific model type)x: Input data for the model(y_t, y_nan): Tuple containing target values and NaN mask functions/arraysps: Model parametersst: Model statelogging: LoggingLoss configuration
Returns
In training mode (
logging.train_mode = true):(loss_value, st): Single loss value and updated state
In evaluation mode (
logging.train_mode = false):(loss_values, st, ŷ): NamedTuple of losses, state and predictions
EasyHybrid.constructHybridModel Method
constructHybridModel(predictors::NamedTuple, forcing, targets, mechanistic_model, parameters, global_param_names; kwargs...)Construct a HybridModel with multiple neural network architectures. A separate neural network is built for each key in the predictors NamedTuple.
Arguments:
predictors::NamedTuple: A NamedTuple where keys are network names, and values are vectors of predictor variables for that network.forcing: Variables passed directly to the mechanistic model.targets: The target variables to predict.mechanistic_model: A function implementing the process-based model.parameters: A parameter container defining defaults, lowers, and uppers.global_param_names: Names of the parameters to be globally optimized.kwargs: Additional configuration.hidden_layersandactivationcan also be NamedTuples to configure each network independently.
EasyHybrid.constructHybridModel Method
constructHybridModel(predictors::Vector{Symbol}, forcing, targets, mechanistic_model, parameters, neural_param_names, global_param_names; kwargs...)Construct a HybridModel with a single neural network architecture predicting all neural_param_names from the predictors.
Arguments:
predictors::Vector{Symbol}: Variables used as inputs to the neural network.forcing: Variables passed directly to the mechanistic model.targets: The target variables to predict.mechanistic_model: A function implementing the process-based model.parameters: A parameter container defining defaults, lowers, and uppers.neural_param_names: Names of the parameters to be predicted by the neural network.global_param_names: Names of the parameters to be globally optimized.kwargs: Additional configuration likehidden_layers,activation,scale_nn_outputs, etc.
EasyHybrid.constructNNModel Method
constructNNModel(predictors, targets; hidden_layers, activation, scale_nn_outputs)Main constructor: hidden_layers can be either • a Vector{Int} of sizes, or • a Chain of hidden-layer Dense blocks.
EasyHybrid.extract_weights Method
extract_weights(ps; key=:weight) -> Vector{AbstractArray}Walk the parameter tree ps (a ComponentArray, NamedTuple, or any nested combination of them) and return all leaf arrays whose immediate parent field name equals key.
Defaults to :weight, so you get the weight matrices of Dense/Conv layers and skip biases, BatchNorm scale/bias, running statistics in st, and any scalar global parameters.
The returned arrays are views/aliases into ps. When ps is the argument the autodiff is differentiating w.r.t., gradients of any function of these views flow back into the trainable weights.
EasyHybrid.filter_sequences Method
filter_sequences(x, y) -> (x_filtered, y_filtered)Drop 3rd-dim samples where any predictor is NaN or all targets are NaN. Accepts x as a 3D array or (x_array, forcings) Tuple.
EasyHybrid.get_loss_value Method
get_loss_value(losses, loss_spec, agg)Extract loss value from losses based on the loss specification type.
Arguments
losses: NamedTuple containing loss valuesloss_spec: Loss specification (Symbol, Function or Tuple)agg: Aggregation function name as Symbol
Returns
- Loss value for the specified loss function
Examples
# Symbol case
val = get_loss_value(losses, :mse, :sum)
# Function case
custom_loss(ŷ, y) = mean(abs2, ŷ .- y)
val = get_loss_value(losses, custom_loss, :mean)
# Tuple case
weighted_loss(ŷ, y, w) = w * mean(abs2, ŷ .- y)
val = get_loss_value(losses, (weighted_loss, (0.5,)), :sum)EasyHybrid.get_mechanistic_model_config Method
get_mechanistic_model_config(f::Function)Build an OrderedDict describing a function for YAML output: its name, and for each Method, its source file, starting line, and the full source text extracted from disk by parsing one complete expression starting at that line. The single-method case is flattened so the YAML stays compact.
Used to record mechanistic_model in the saved config so the exact function definition (not just its name) is preserved alongside the run.
EasyHybrid.get_parameters_config Method
get_parameters_config(pc::ParameterContainer)Serialize the parameter table (default, lower, upper per parameter) of a ParameterContainer into a nested OrderedDict suitable for YAML output. Without this, only the compact show of the struct (e.g. ParameterContainer(RUE, Rb, Q10)) would be written, which drops all of the actual default and bound values.
EasyHybrid.get_prediction_target_names Method
get_prediction_target_names(hm)Utility function to extract predictor/forcing and target names from a hybrid model.
Arguments:
hm: The Hybrid Model
Returns a tuple of (predictors_forcing, targets) names.
sourceEasyHybrid.initialize_plotting_observables Method
initialize_plotting_observables(init_ŷ_train, init_ŷ_val, y_train, y_val, l_init_train, l_init_val, training_loss, agg, monitor_names, target_names)Initialize plotting observables for training visualization if the Makie extension is loaded.
sourceEasyHybrid.is_optimisers_rule Method
is_optimisers_rule(opt) -> BoolReturn true when opt originates from the Optimisers.jl package (e.g. Adam, AdamW, RMSProp, OptimiserChain), or when opt is a NamedTuple describing a per-branch optimizer (see is_per_branch_opt).
The check on single rules is by source package (nameof(parentmodule(typeof(opt))) === :Optimisers) rather than by isa Optimisers.AbstractRule, because in some package combinations Optim.jl optimizers were observed to satisfy the AbstractRule test and get misrouted to the Lux.Training loop.
The Lux.Training-based loop dispatches on this; everything else (including Optim.jl and Optimization.jl optimizers) is routed through the Optimization.jl driver.
EasyHybrid.is_per_branch_opt Method
is_per_branch_opt(opt) -> BoolReturn true when opt is a NamedTuple describing a per-branch optimizer specification — i.e. one of:
a
NamedTupleofOptimisers.AbstractRules (e.g.(; Rb = Adam(1e-3), Q10 = Descent(1e-2))), ora
NamedTupleof pre-built optimizer state trees as returned byOptimisers.setup(rule, ps_branch), ora mix of the two.
Branches of the parameter tree not listed in opt fall back to the default rule Adam() (see build_opt_state).
This is detected purely by opt isa NamedTuple; the per-branch dispatch is checked again per-leaf when build_opt_state walks the spec.
EasyHybrid.kwargs_to_configs Method
kwargs_to_configs(save_ps, kwargs) -> (TrainConfig, DataConfig, NamedTuple)Build a fresh (TrainConfig, DataConfig) pair from a flat collection of kwargs. Kwargs are split between the two configs based on fieldnames(TrainConfig) and fieldnames(DataConfig); anything left over is returned as the third element and forwarded to solve(...) on the Optimization.jl path (or warned about on the Optimisers.jl path — see _train).
save_ps is the deprecated positional argument from train(model, data, save_ps; ...); when non-empty it is forwarded as tracked_params on the resulting TrainConfig.
EasyHybrid.load_timeseries_netcdf Method
load_timeseries_netcdf(path::AbstractString; timedim::AbstractString = "time") -> DataFrameReads a NetCDF file where all data variables are 1D over the specified timedim and returns a tidy DataFrame with one row per time step.
Only includes variables whose sole dimension is
timedim.Does not attempt to parse or convert time units; all columns are read as-is.
EasyHybrid.loss_fn Function
loss_fn(ŷ, y, y_nan, loss_type)Compute the loss for given predictions and targets using various loss specifications.
Arguments
ŷ: Predicted valuesy: Target valuesy_nan: Mask for NaN valuesloss_type: One of the following:Val(:rmse): Root Mean Square ErrorVal(:mse): Mean Square ErrorVal(:mae): Mean Absolute ErrorVal(:pearson): Pearson correlation coefficientVal(:r2): R-squaredVal(:pearsonLoss): 1 - Pearson correlation coefficientVal(:nseLoss): 1 - NSE::Function: Custom loss function with signaturef(ŷ, y)::Tuple{Function, Tuple}: Custom loss with argsf(ŷ, y, args...)::Tuple{Function, NamedTuple}: Custom loss with kwargsf(ŷ, y; kwargs...)::Tuple{Function, Tuple, NamedTuple}: Custom loss with bothf(ŷ, y, args...; kwargs...)
Examples
# Predefined loss
loss = loss_fn(ŷ, y, y_nan, Val(:mse))
# Custom loss function
custom_loss(ŷ, y) = mean(abs2, ŷ .- y)
loss = loss_fn(ŷ, y, y_nan, custom_loss)
# With positional arguments
weighted_loss(ŷ, y, w) = w * mean(abs2, ŷ .- y)
loss = loss_fn(ŷ, y, y_nan, (weighted_loss, (0.5,)))
# With keyword arguments
scaled_loss(ŷ, y; scale=1.0) = scale * mean(abs2, ŷ .- y)
loss = loss_fn(ŷ, y, y_nan, (scaled_loss, (scale=2.0,)))
# With both args and kwargs
complex_loss(ŷ, y, w; scale=1.0) = scale * w * mean(abs2, ŷ .- y)
loss = loss_fn(ŷ, y, y_nan, (complex_loss, (0.5,), (scale=2.0,)))You can define additional predefined loss functions by adding more methods:
import EasyHybrid: loss_fn
function EasyHybrid.loss_fn(ŷ, y, y_nan, ::Val{:nse})
return 1 - sum((ŷ[y_nan] .- y[y_nan]).^2) / sum((y[y_nan] .- mean(y[y_nan])).^2)
endEasyHybrid.make_folds Method
make_folds(df::DataFrame; k::Int=5, shuffle=true) -> Vector{Int}Assigns each observation in the DataFrame df to one of k folds for cross-validation.
Arguments
df::DataFrame: The input DataFrame whose rows are to be split into folds.k::Int=5: Number of folds to create.shuffle=true: Whether to shuffle the data before assigning folds.
Returns
folds::Vector{Int}: A vector of lengthnrow(df)where each entry is an integer in1:kindicating the fold assignment for that observation.
EasyHybrid.override_config Method
override_config(cfg, overrides::NamedTuple)Return a new cfg of the same type with the fields named in overrides replaced. Works with any @kwdef struct (e.g. TrainConfig, DataConfig).
EasyHybrid.override_configs Method
override_configs(train_cfg, data_cfg, kwargs) -> (TrainConfig, DataConfig, NamedTuple)Return (train_cfg′, data_cfg′, solve_kwargs) where any field present in kwargs overrides the corresponding field of train_cfg/data_cfg. Fields not mentioned in kwargs are kept as-is. Anything left over is returned as solve_kwargs and forwarded to solve(...) on the Optimization.jl path (or warned about on the Optimisers.jl path — see _train).
EasyHybrid.prepare_data Function
prepare_data(hm, data::DataFrame; array_type=:KeyedArray, drop_missing_rows=true)
prepare_data(hm, data::KeyedArray)
prepare_data(hm, data::AbstractDimArray)
prepare_data(hm, data::Tuple)Prepare data for training by extracting predictor/forcing and target variables based on the hybrid model's configuration.
Arguments:
hm: The Hybrid Modeldata: The input data, which can be a DataFrame, KeyedArray, or DimensionalData array.array_type: (DataFrame only) Output array type::KeyedArray(default) or:DimArray.drop_missing_rows: (DataFrame only) Iftrue(default), drop rows where any predictor is NaN or all targets are NaN.
Returns:
If
datais a DataFrame: a tuple of (predictors_forcing, targets) as KeyedArrays or DimArrays depending onarray_type.If
datais a KeyedArray: a tuple of (predictors_forcing, targets) as KeyedArrays.If
datais an AbstractDimArray: a tuple of (predictors_forcing, targets) as DimArrays.If
datais already a Tuple, it is returned as-is.
Method
prepare_hidden_chain(hidden_layers, in_dim, out_dim; activation, input_batchnorm=false)Construct a neural network Chain for use in NN models.
Arguments
hidden_layers::Union{Vector{Int}, Chain}:If a
Vector{Int}, specifies the sizes of each hidden layer. For example,[32, 16]creates two hidden layers with 32 and 16 units, respectively.If a
Chain, the user provides a pre-built chain of hidden layers (excluding input/output layers). If the chain ends with aRecurrencelayer, aRecurrenceOutputDenselayer is automatically added to handle the sequence output format.
in_dim::Int: Number of input features (input dimension).out_dim::Int: Number of output features (output dimension).activation: Activation function to use in hidden layers (default:tanh).input_batchnorm::Bool: Iftrue, applies aBatchNormlayer to the input (default:false).
Returns
- A
Chainobject representing the full neural network, with the following structure:Optional input batch normalization (if
input_batchnorm=true)Input layer:
Dense(in_dim, h₁, activation)whereh₁is the first hidden sizeHidden layers: either user-supplied
Chainor constructed fromhidden_layersIf last hidden layer is a
Recurrence, aRecurrenceOutputDenseis added to handle sequence outputOutput layer:
Dense(hₖ, out_dim)wherehₖis the last hidden size
where h₁ is the first hidden size and hₖ the last.
Example with Recurrence (LSTM)
# User only needs to define:
NN_Memory = Chain(
Recurrence(LSTMCell(15 => 15), return_sequence = true),
)
# The function automatically adds the RecurrenceOutputDense layer to handle sequence output
model = constructHybridModel(..., hidden_layers = NN_Memory, ...)EasyHybrid.scale_single_param Method
scale_single_param(name, raw_val, parameters)Scale a single parameter using the sigmoid scaling function.
sourceEasyHybrid.scale_single_param_minmax Method
scale_single_param_minmax(name, hm::AbstractHybridModel)Scale a single parameter using the minmax scaling function.
sourceEasyHybrid.split_data Function
split_data(data, hybridModel; split_by_id=nothing, shuffleobs=false, split_data_at=0.8, kwargs...)
split_data(data::Union{DataFrame, KeyedArray}, hybridModel; split_by_id=nothing, shuffleobs=false, split_data_at=0.8, folds=nothing, val_fold=nothing, kwargs...)
split_data(data::AbstractDimArray, hybridModel; split_by_id=nothing, shuffleobs=false, split_data_at=0.8, kwargs...)
split_data(data::Tuple, hybridModel; split_by_id=nothing, shuffleobs=false, split_data_at=0.8, kwargs...)
split_data(data::Tuple{Tuple, Tuple}, hybridModel; kwargs...)Split data into training and validation sets, either randomly, by grouping by ID, or using external fold assignments.
Arguments:
data: The data to split, which can be a DataFrame, KeyedArray, AbstractDimArray, or TuplehybridModel: The hybrid model object used for data preparationsplit_by_id=nothing: Eithernothingfor random splitting, aSymbolfor column-based splitting, or anAbstractVectorfor custom ID-based splittingshuffleobs=false: Whether to shuffle observations during splittingsplit_data_at=0.8: Ratio of data to use for trainingfolds: Vector or column name of fold assignments (1..k), one per sample/column for k-fold cross-validationval_fold: The validation fold to use whenfoldsis providedsequence_kwargs=nothing: NamedTuple of keyword arguments forwarded tosplit_into_sequences(e.g.(; input_window=10, output_window=1, output_shift=1, lead_time=2)). When set, data is windowed into 3D sequences before splitting.
Behavior:
For DataFrame/KeyedArray: Supports random splitting, ID-based splitting, and external fold assignments
For AbstractDimArray/Tuple: Random splitting only after data preparation
For pre-split Tuple{Tuple, Tuple}: Returns input unchanged
Returns:
((x_train, y_train), (x_val, y_val)): Tuple containing training and validation data pairs
EasyHybrid.split_data Method
split_data(df::DataFrame, target, xvars, seqID; f=0.8, batchsize=32, shuffle=true, partial=true)
sourceEasyHybrid.split_data Method
split_data(df::DataFrame, target, xvars; f=0.8, batchsize=32, shuffle=true, partial=true)
sourceEasyHybrid.split_into_sequences Method
split_into_sequences(x, y; input_window=5, output_window=1, output_shift=1, lead_time=1)Slide a (input_window + lead_time) window over 2D (feature, time) arrays to produce 3D (feature, time, batch) tensors for sequence-to-sequence training.
Arguments:
x: 2D input array(feature, time), or a(x_array, forcings)Tuple fromprepare_data.y: 2D target array(target, time), or aNamedTupleof 1D target vectors.input_window: number of input time steps per sample.output_window: number of target time steps per sample.output_shift: stride between consecutive samples.lead_time: gap between end of input window and end of output window.
Returns:
(X, Y)as 3D arrays. Whenxis a Tuple, returns((X, forcings_windowed), Y).
EasyHybrid.toDataFrame Method
toDataFrame(arr, target_names)Extract specific target variables from a labeled array into a DataFrame with _pred suffix.
Arguments
arr: A labeled array or NamedTuple-like object with property accesstarget_names: Vector of target variable names to extract
Returns
DataFramewith columns named<target>_predfor each target
EasyHybrid.toDataFrame Method
toDataFrame(nt::NamedTuple, target_names, y_ref::NamedTuple)Extract targets from prediction NamedTuple nt into a DataFrame with _pred suffix. When the prediction has more rows than the reference y_ref (e.g. input_window > output_window), the last output_window rows are selected before flattening.
EasyHybrid.toDataFrame Method
toDataFrame(nt::NamedTuple)Convert a NamedTuple of arrays (vectors or matrices) into a DataFrame. Matrix values are flattened via vec so each key becomes a single column.
EasyHybrid.toDataFrame Method
toDataFrame(arr::Union{KeyedArray{T, 2}, AbstractDimArray{T, 2}}, cols_dim=:variable, index_dim=:batch_size; index_col=:index)Convert a 2D labeled array (KeyedArray or DimArray) to a DataFrame.
Arguments
arr: The 2D labeled array to convertcols_dim: Dimension name to use as DataFrame columns (default::variable)index_dim: Dimension name to use as DataFrame row index (default::batch_size)index_col: Name for the index column in the result (default::index)
Returns
DataFramewith columns fromcols_dimkeys and an index column fromindex_dimkeys
EasyHybrid.toDataFrame Method
toDataFrame(arr::AbstractLabeledArray{T, 3}, cols_dim=:variable, index_dim=:batch_size; slice_dim=:time, index_col=:index)Convert a 3D labeled array (KeyedArray or DimArray) to a Dict of DataFrames, one per slice.
Arguments
arr: The 3D labeled array to convertcols_dim: Dimension name to use as DataFrame columns (default::variable)index_dim: Dimension name to use as DataFrame row index (default::batch_size)slice_dim: Dimension name to slice along (default::time)index_col: Name for the index column in each result DataFrame (default::index)
Returns
Dict{Any, DataFrame}mapping slice keys to DataFrames
EasyHybrid.toNamedTuple Method
toNamedTuple(ka::KeyedArray, variable::Symbol) Extract a single variable from a KeyedArray and return it as a vector.
Arguments:
ka: The KeyedArray or DimArray to unpackvariable: Symbol representing the variable to extract
Returns:
- Vector containing the variable data
Example:
# Extract just SW_IN from an array
sw_in = toNamedTuple(ds, :SW_IN)EasyHybrid.toNamedTuple Method
toNamedTuple(ka::Union{KeyedArray, AbstractDimArray}, variables::Vector{Symbol})Extract specified variables from a KeyedArray or DimArray and return them as a NamedTuple of vectors.
Arguments:
ka: The KeyedArray or DimArray to unpackvariables: Vector of symbols representing the variables to extract
Returns:
- NamedTuple with variable names as keys and vectors as values
Example:
# Extract SW_IN and TA from an array
data = toNamedTuple(ds, [:SW_IN, :TA])
sw_in = data.SW_IN
ta = data.TAEasyHybrid.toNamedTuple Method
toNamedTuple(ka::KeyedArray) Extract all variables from a KeyedArray and return them as a NamedTuple of vectors.
Arguments:
ka: The KeyedArray to unpack
Returns:
- NamedTuple with all variable names as keys and vectors as values
Example:
# Extract all variables from an array
data = toNamedTuple(ds)
# Access individual variables
sw_in = data.SW_IN
ta = data.TA
nee = data.NEEEasyHybrid.to_keyedArray Function
tokeyedArray(dfg::Union{Vector,GroupedDataFrame{DataFrame}}, vars=All())
sourceEasyHybrid.train Method
train(model, data; train_cfg::TrainConfig = TrainConfig(), data_cfg::DataConfig = DataConfig())
train(model, data; kwargs...)Train a hybrid model using the provided data.
Two equivalent calling styles are supported:
- Typed configs — pass complete
TrainConfig/DataConfigobjects:
train(model, data;
train_cfg = TrainConfig(nepochs=100, batchsize=32),
data_cfg = DataConfig(split_data_at=0.8),
)- Flat kwargs — pass
TrainConfig/DataConfigfield names directly:
train(model, data; nepochs=100, batchsize=32, split_data_at=0.8)The two styles can also be mixed; flat kwargs override the corresponding fields of the supplied train_cfg / data_cfg:
train(model, data; train_cfg = TrainConfig(nepochs=100), nepochs = 10) # nepochs = 10Returns nothing if data preparation fails (zero-size dimension in training or validation data).
Arguments
model: The hybrid model to train.data: Training data, a singleDimArray, a singleDataFrame, or a singleKeyedArray.
Keyword Arguments
train_cfg: Training configuration. SeeTrainConfigfor all options.data_cfg: Data preparation configuration. SeeDataConfigfor all options.Any other kwargs are forwarded as overrides to
train_cfg/data_cfg.
Returns
A TrainResults with the following fields:
train_losses: Per-epoch training losses.val_losses: Per-epoch validation losses.snapshots: Model parameter snapshots taken during training.train_obs_pred: Observed vs. predicted values on the training set.val_obs_pred: Observed vs. predicted values on the validation set.train_diffs: Additional diagnostic variables computed on the training set.val_diffs: Additional diagnostic variables computed on the validation set.ps: Final (or best) model parameters.st: Final (or best) model state.best_epoch: Epoch at which the best validation loss was achieved.best_loss: Best validation loss recorded during training.
EasyHybrid.tune Method
tune(hybrid_model, data, mspec::ModelSpec; kwargs...)
tune(hybrid_model, data; kwargs...)
tune(hybrid_model, data, train_cfg::TrainConfig; data_cfg::DataConfig = DataConfig(), kwargs...)Construct a new hybrid model from hybrid_model plus hyperparameters, then call train.
Returns a TrainResults (or nothing if data preparation fails, as in train).
EasyHybrid.weight_l2 Method
weight_l2(ps; key=:weight, normalize=false) -> RealSum of squared Frobenius norms over all parameter arrays in ps whose immediate parent field name is key (default :weight).
With normalize=true, returns the mean squared weight (sum divided by the number of scalar weights), so the value is independent of network width/depth.
Unlike sum(abs2, extract_weights(ps)), this fuses the tree walk with the reduction so it is safe to use inside Zygote-differentiated losses, e.g.:
extra_loss = (ŷ, ps) -> (; l2_Rb = λ * weight_l2(ps.Rb; normalize=true),)When ps is the loss function argument, gradients flow into the weight arrays.
EasyHybrid.@hybrid Macro
@hybrid ModelName α β γMacro to define hybrid model structs with arbitrary numbers of physical parameters.
This defines a struct with:
Default fields:
NN(neural network),predictors,forcing,targets.Additional physical parameters, i.e.,
α β γ.
Examples
@hybrid MyModel α β γ
@hybrid FluidModel (:viscosity, :density)
@hybrid SimpleModel :a :b