API
This section documents the public ApplicationDrivenLearning API.
Functions and types whose name starts with an underscore are internal implementation details: they are not part of the public API, are not listed here, and may change without a breaking release.
Constructors
ApplicationDrivenLearning.Model — Type
Model <: JuMP.AbstractModelCreate an empty ApplicationDrivenLearning.Model with empty plan and assess models, missing forecast model and default settings.
Besides the Policy and Forecast variable pairs, the model keeps the plan-side and assess-side variables split into their own concretely typed vectors. Those are what the per-sample cost evaluation actually iterates over, so they are maintained as variables are declared rather than rebuilt on every access.
ApplicationDrivenLearning.ForecastModel — Type
ForecastModel(; inputs = nothing, architecture, outputs)One forecast model: an architecture, the input columns it reads and the Forecast variables it predicts.
A forecast is described by a vector of these, handed to set_forecast_model, which assembles them into the FullForecastModel stored on the Model. Everything in a unit is about the same one architecture, so there are no parallel vectors to keep aligned and no map iteration order to know.
Arguments
inputs: the input columns this architecture reads, as column positions ([1, 3]) or column names ([:temp, :hour]). A scalar is accepted for a one-input architecture.nothingmeans "the whole input row", which is only resolvable when the row width is known — either because this is the only unit, or because another unit selects by name and so fixes the schema. Writing the names here is what declares the input schema; there is no separate place to declare it.architecture: anything callable on a(features x samples)matrix — aFlux.Dense, aFlux.Chain, or your own layer. It is not copied here, so that reusing one object across units can be reported as an error; it is deep-copied when the units are assembled, so training never touches the caller's object.outputs: theForecastvariables this architecture predicts, in the order it produces them. Say where a variable's realized values live inYby writingvariable => :columnfor a named container, orvariable => positionfor a matrix; either way the answer sits next to its variable, so there is no second ordering to line up. Left unsaid, a variable reads theYcolumn carrying its own declared name.A name and a position are two spellings of one thing, so all units of a forecast use one or the other. Positions are the only way to pin the layout of a matrix
Y: without them its columns are read in the order theForecastvariables were declared on theModel, which nothing states and nothing checks.
All units of one forecast must select their inputs the same way — all by position or all by name — since a mix leaves the input schema undefined.
Examples
One network reading two named columns:
model = ApplicationDrivenLearning.Model()
@variable(model, demand, ApplicationDrivenLearning.Forecast)
set_forecast_model(
model,
ForecastModel(
inputs = [:temp, :hour],
architecture = Flux.Dense(2 => 1),
outputs = [demand],
),
)One network per forecast variable, each with its own parameters, naming the Y columns because the variables are called load[1] and load[2]:
@variable(model, load[1:2], ApplicationDrivenLearning.Forecast)
set_forecast_model(
model,
[
ForecastModel(
inputs = :temp,
architecture = Flux.Dense(1 => 1),
outputs = [load[1] => :load_a],
),
ForecastModel(
inputs = :wind,
architecture = Flux.Dense(1 => 1),
outputs = [load[2] => :load_b],
),
],
)To reuse one architecture for several forecasts, give each unit its own copy — deepcopy(nn), or a function returning a fresh network. Passing the same object to two units is an error rather than a silent choice between "same shape" and "same weights".
ApplicationDrivenLearning.FullForecastModel — Type
FullForecastModel(units, output_variables, input_size, output_size; input_names, output_columns, sample_key)The assembled forecast of a Model: every architecture, the input columns each reads, the Forecast variables each predicts, and the schema of the data containers.
Built by set_forecast_model from a vector of ForecastModel units, which is how it should be constructed — this explicit form exists because the fields are what the prediction loop, the optimizers and the parallel backends work on, and because Functors rebuilds the struct positionally.
Arguments
units: theResolvedUnits, one perForecastModelthe forecast was assembled from. Each carries its own architecture, the input columns it reads and the prediction rows it writes, so there are no parallel vectors to keep in step. The architectures are deep-copied when the units are assembled, so the caller's objects are left untouched.output_variables: the forecast variables in the order the model produces them, i.e. the row order of a prediction. Every row is written by exactly one unit, which is checked here.input_size::Int: number of columns the input has.output_size::Int: number of forecast variables.input_names::Union{Vector{Symbol},Nothing}: name of each input column, in the order the model expects them. This is the model's declared input schema: it is what lets a table input be matched by name instead of by column order.nothingmeans the model declares no schema, and a table input is then rejected rather than guessed at.output_columns::Union{Vector{Symbol},Vector{Int},Nothing}: where inYeach variable ofoutput_variablesis, in that same order — as table column names, or as column positions in a matrix.nothingmeans the declared names of theForecastvariables themselves are used. Assembled from thevariable => :columnandvariable => positionpairs of the units, which is also why only one of the two spellings can be present.sample_key::Union{Symbol,Nothing}: name of the column that identifies a row, the wayinput_namesidentifies a column — a timestamp or an id. When bothXandYare tables carrying it, the realized values are looked up by key for each row ofXinstead of being trusted to arrive in the same order.nothingmeans rows are matched by position.
ApplicationDrivenLearning.Plan — Function
Plan(model::ApplicationDrivenLearning.Model)Return the inner JuMP.Model used for planning, i.e. the problem solved with the predictive model output in place of the Forecast variables. Use it as the target of the @variable, @constraint and @objective macros.
ApplicationDrivenLearning.Assess — Function
Assess(model::ApplicationDrivenLearning.Model)Return the inner JuMP.Model used for assessment, i.e. the problem solved with the realized values in place of the Forecast variables and the Policy variables fixed to the plan decision. Use it as the target of the @variable, @constraint and @objective macros.
JuMP variable types
ApplicationDrivenLearning.Policy — Type
Policy{T}Policy variable type that holds plan and assess variables.
ApplicationDrivenLearning.Forecast — Type
Forecast{T}Forecast variable type that holds plan and assess variables.
Structs
ApplicationDrivenLearning.Options — Type
Options(mode; params...)Options struct to hold optimization mode and mode parameters.
mode must be a subtype of AbstractOptimizationMode (the type itself, not an instance); the accepted keyword arguments are documented on each mode.
...
Example
options = Options(
GradientMode;
rule = Flux.RMSProp(0.01),
epochs = 100,
batch_size = 10,
)...
ApplicationDrivenLearning.Solution — Type
Solution{C<:Real,P<:Real}Result of a train! call.
The cost and the parameters are stored with their own concrete element types, which usually differ: costs come from the solver and are Float64, while parameters follow the precision of the Flux model (often Float32).
...
Fields
cost::C: best assessed cost found during training, averaged over the training samples.params::Vector{P}: predictive model parameters that achievecost, flattened in the order used byextract_params. The model itself is also updated in place with these values. ...
Modes
ApplicationDrivenLearning.OptimMode — Type
OptimMode <: AbstractOptimizationModeUsed to solve the application driven learning training problem with any algorithm from the Optim.jl package.
The objective handed to Optim is the assessed cost as a function of the flat parameter vector of the predictive model. Derivative-free algorithms need nothing further; gradient-based ones are given dC/dθ automatically, obtained by carrying the same reverse pass the GradientMode loop uses one step further, from the forecasts to the parameters.
...
Parameters
algorithmis the Optim.jl algorithm object to use. Defaults toOptim.NelderMead(). Anything Optim provides works, for exampleOptim.ParticleSwarm(),Optim.SimulatedAnnealing(),Optim.LBFGS(),Optim.BFGS()orOptim.ConjugateGradient().parallelis the parallel backend used for the per-sample cost and gradient evaluations. Defaults toSerialBackend(); passMPIBackend()to distribute them across MPI processes, orDistributedBackend()acrossDistributedworker processes. SeeAbstractParallelBackend.- Any other parameter accepted by
Optim.Options, such asiterations,time_limit,show_traceorg_abstol. ...
Example
options =
Options(OptimMode; algorithm = Optim.ParticleSwarm(), iterations = 500)ApplicationDrivenLearning.NLoptMode — Type
NLoptMode <: AbstractOptimizationModeUsed to solve the application driven learning training problem with an algorithm from the NLopt.jl package.
Provided by a package extension: using NLopt is required before training with this mode. The mode type itself always exists, so that a missing using produces a readable error rather than an UndefVarError.
...
Parameters
algorithm::Symbolis the NLopt algorithm to use. Defaults to:LN_NELDERMEAD. The derivative-free families are:LN_*(local, e.g.:LN_BOBYQA,:LN_COBYLA) and:GN_*(global, e.g.:GN_DIRECT); the:LD_*family is gradient-based and is givendC/dθautomatically.- Any other parameter is set on the NLopt
Optobject, so the stopping criteria are named as NLopt names them:xtol_rel,xtol_abs,ftol_rel,ftol_abs,maxeval,maxtime,stopval. Note this differs fromOptimMode, where such keywords go toOptim.Optionsinstead. parallelis the parallel backend used for the per-sample cost and gradient evaluations. Defaults toSerialBackend(); passMPIBackend()to distribute them across MPI processes, orDistributedBackend()acrossDistributedworker processes. SeeAbstractParallelBackend.
...
Example
using NLopt
options =
Options(NLoptMode; algorithm = :LN_BOBYQA, xtol_rel = 1e-6, maxeval = 500)ApplicationDrivenLearning.GradientMode — Type
GradientMode <: AbstractOptimizationModeUsed to solve the application driven learning training problem using the gradient optimization method
...
Parameters
ruleis the Flux/Optimisers rule to be used in the gradient optimization process. Defaults toFlux.Descent().epochsis the number of epochs to be used in the gradient optimization process. Defaults to100.batch_sizeis the batch size to be used in the gradient optimization process. Defaults to-1, meaning the full dataset is used at every epoch. When positive, each epoch drawsbatch_sizesample indexes uniformly with replacement, so a batch may repeat samples and an epoch does not sweep the whole dataset.verboseis the flag of whether to print the training process. Defaults totrue.compute_cost_everyis the epoch frequency for computing the cost and evaluating best solution. Defaults to1.time_limitis the time limit for the training process, in seconds. Defaults toInf.g_tolis the tolerance on the infinity norm of the cost gradients with respect to the forecasts, below which training stops. Defaults to0, which disables the check.parallelis the parallel backend used for the per-sample cost and gradient evaluations. Defaults toSerialBackend(); passMPIBackend()to distribute them across MPI processes, orDistributedBackend()acrossDistributedworker processes. SeeAbstractParallelBackend. ...
ApplicationDrivenLearning.BilevelMode — Type
BilevelMode <: AbstractOptimizationModeUsed to solve the application driven learning training problem as a bilevel optimization problem by using the BilevelJuMP.jl package.
...
Parameters
optimizer::Functionis equivalent tosolverin BilevelJuMP.BilevelModel.silent::Boolis equivalent tosilentin BilevelJuMP.BilevelModel.mode::Union{Nothing, BilevelJuMP.BilevelMode}is equivalent tomodein BilevelJuMP.BilevelModel. ...
Deprecated Modes
ApplicationDrivenLearning.NelderMeadMode — Type
NelderMeadMode <: AbstractOptimizationModeUsed to solve the application driven learning training problem using the Nelder-Mead optimization method implementation from Optim.jl package.
Superseded by OptimMode, which reaches every Optim.jl algorithm rather than just this one. NelderMeadMode behaves exactly as before and is kept so that existing code keeps working; new code should prefer
Options(OptimMode; algorithm = Optim.NelderMead())Note that initial_simplex and parameters are Nelder-Mead specific, so under OptimMode they move into the algorithm object: Optim.NelderMead(initial_simplex = ...).
...
Parameters
initial_simplexis the initial simplex of solutions to be applied.parametersis the parameters to be applied to the Nelder-Mead optimization method. ...
ApplicationDrivenLearning.NelderMeadMPIMode — Type
NelderMeadMPIMode <: AbstractOptimizationModeMPI implementation of NelderMeadMode, which distributes the per-sample cost evaluations across MPI processes.
Parallelism is now an option rather than a mode, so any optimizer can be run over MPI rather than just Nelder-Mead:
Options(OptimMode; algorithm = Optim.NelderMead(), parallel = MPIBackend())This mode keeps working and forwards to exactly that, translating mpi_finalize onto the backend.
...
Parameters
mpi_finalize::Boolcontrols whetherMPI.Finalize()is called at the end of training. Defaults totrue.- Any other parameter accepted by
Optim.Options. ...
ApplicationDrivenLearning.GradientMPIMode — Type
GradientMPIMode <: AbstractOptimizationModeMPI implementation of GradientMode, which distributes the per-sample cost and gradient evaluations across MPI processes.
Parallelism is now an option rather than a mode:
Options(GradientMode; parallel = MPIBackend())This mode keeps working and forwards to exactly that, translating mpi_finalize onto the backend.
...
Parameters
- The same parameters as
GradientMode. mpi_finalize::Boolcontrols whetherMPI.Finalize()is called at the end of training. Defaults totrue. ...
Parallel backends
ApplicationDrivenLearning.AbstractParallelBackend — Type
AbstractParallelBackendHow the per-sample work of a training run is distributed.
Every setting of a backend lives on the backend object. The one exception is mpi_finalize, which predates this design and is reachable only through the deprecated *MPIMode aliases.
BilevelMode is deliberately outside this: it builds a single MIP over all samples and never evaluates a per-sample cost, so it has nothing to distribute.
Implementations: SerialBackend, MPIBackend, DistributedBackend.
ApplicationDrivenLearning.SerialBackend — Type
SerialBackend <: AbstractParallelBackendEvaluate everything in the current process. The default.
ApplicationDrivenLearning.MPIBackend — Type
MPIBackend(; finalize = true) <: AbstractParallelBackendDistribute the per-sample cost and gradient evaluations across MPI processes with JobQueueMPI.jl.
Every rank runs the same script and so builds its own copy of the model; the controller then drives the optimizer while the workers serve evaluations. Only the controller returns a meaningful Solution.
finalize controls whether MPI.Finalize() is called when training ends. Set it to false when several train! calls share one MPI session.
ApplicationDrivenLearning.DistributedBackend — Type
DistributedBackend(; workers = nothing, verify = true) <: AbstractParallelBackendDistribute the per-sample cost and gradient evaluations across Distributed worker processes.
Unlike MPIBackend, where every rank runs the same script and so builds its own model, here only the driver runs train! — so the workers need a way to obtain a model of their own. That is why this backend must be used with the builder form of train!:
sol = train!(
build_case,
X,
Y,
Options(GradientMode; parallel = DistributedBackend()),
)Passing a Model instead is an error. The builder is called once per worker per train!, never per sample, and must be defined on the workers (@everywhere, or in a package they load).
It must also construct the model rather than close over one: an ApplicationDrivenLearning.Model that has had set_optimizer called on it cannot be serialized at all — the attempt is a ReadOnlyMemoryError that takes the process down, since the solver's C pointers travel as raw addresses.
Keyword arguments
workers: the worker ids to use. Defaults toDistributed.workers()read when training starts, so callingaddprocsafter building theOptionsstill works.verify: check at start-up that the workers' models match the driver's. Their sizes are always compared, which is free;verifyadditionally compares the assessed cost of one sample, which costs one extra pair of solves on the driver.Since the driver and the workers now build from the same function, this guards a narrower thing than it once did: a builder that is not deterministic — one closing over mutable global state, or whose structure varies per process. Cheap enough to leave on.
Example
using Distributed
addprocs(4)
@everywhere using ApplicationDrivenLearning, HiGHS, Flux
@everywhere function build_case()
m = ApplicationDrivenLearning.Model()
@variable(m, demand, ApplicationDrivenLearning.Forecast)
# ... policy variables, constraints, objectives ...
set_optimizer(m, HiGHS.Optimizer)
ApplicationDrivenLearning.set_forecast_model(
m,
ApplicationDrivenLearning.ForecastModel(
architecture = Dense(1 => 1),
outputs = [demand],
),
)
return m
end
sol = train!(
build_case,
X,
Y,
Options(GradientMode; parallel = DistributedBackend()),
)Attributes getters and setters
ApplicationDrivenLearning.plan_policy_vars — Function
plan_policy_vars(model::Model)Return the vector of Policy variables that belong to the plan model, in declaration order.
The vector is owned by model and is not a copy; do not mutate it.
ApplicationDrivenLearning.assess_policy_vars — Function
assess_policy_vars(model::Model)Return the vector of Policy variables that belong to the assess model, in declaration order (matching plan_policy_vars).
The vector is owned by model and is not a copy; do not mutate it.
ApplicationDrivenLearning.plan_forecast_vars — Function
plan_forecast_vars(model::Model)Return the vector of Forecast variables that belong to the plan model, in declaration order.
The vector is owned by model and is not a copy; do not mutate it.
ApplicationDrivenLearning.assess_forecast_vars — Function
assess_forecast_vars(model::Model)Return the vector of Forecast variables that belong to the assess model, in declaration order (matching plan_forecast_vars).
The vector is owned by model and is not a copy; do not mutate it.
ApplicationDrivenLearning.set_forecast_model — Function
set_forecast_model(model::Model, units; sample_key = nothing)Attach a forecast to model, described by ForecastModel units.
units is one ForecastModel or a vector of them. Between them they must predict every Forecast variable declared on model, each exactly once and none belonging to another model — all three are checked here, naming whatever is wrong.
model = ApplicationDrivenLearning.Model()
@variable(model, demand, ApplicationDrivenLearning.Forecast)
set_forecast_model(
model,
ForecastModel(
inputs = [:temp, :hour],
architecture = Flux.Dense(2 => 1),
outputs = [demand],
),
)Everything about the data is declared on the units: writing a unit's inputs as column names is what lets X be matched by name, and writing its outputs as variable => :column names the Y column holding a variable.
sample_key is the one thing that cannot live on a unit, and the only keyword here: it names the column that identifies a row rather than a column, so that realized values are looked up per sample instead of trusted to arrive in the same order as the input.
The units' Forecast variables are reordered to follow model.forecast_vars, so that the rows of a prediction line up with the forecast parameters of the plan model, and their column names follow them.
Returns the stored FullForecastModel.
ApplicationDrivenLearning.extract_params — Function
extract_params(model)Extract the parameters of a FullForecastModel into a single vector.
ApplicationDrivenLearning.apply_params — Function
apply_params(model, θ)Return model after fixing the parameters from an adequate vector of parameters.
ApplicationDrivenLearning.apply_gradient! — Function
apply_gradient!(model, dCdy, X, opt_state)Apply per-sample cost gradients to the model parameters.
The optimization layer provides dCdy, the gradient of the assessment cost with respect to the forecasts, for each sample. Because the optimization step itself is not differentiable by the AD backend, these gradients are propagated through the forecast model with a linear surrogate loss whose parameter-gradient equals the chain-rule term (1/T) Σₜ (dC/dŷₜ)·(dŷₜ/dθ).
...
Arguments
model::FullForecastModel: model to be updated.dCdy::AbstractMatrix{<:Real}: per-sample cost gradients, size(T, output_size), with rowtaligned to samplet(rowtofX).X::AbstractMatrix{<:Real}: input data, size(T, input_size).opt_state: Optimisers optimisation state. ...
Other functions
ApplicationDrivenLearning.compute_cost — Function
compute_cost(model, X, Y, with_gradients=false)Compute the cost function (C) based on the model predictions and the true values matrix.
...
Arguments
model::ApplicationDrivenLearning.Model: model to evaluate.X: input data of size(T, input_size). A matrix or vector, whose columns are taken in order, or a Tables.jl-compatible table such as aDataFrame, whose columns are selected by the predictive model'sinput_names.Y: realized values. A(T, output_size)matrix, a vector, a Tables.jl-compatible table, or aDictmapping eachForecastvariable to its series. Table columns are matched to the forecast variables by name, never by position; seeset_forecast_modelfor how those names are declared.with_gradients::Bool=false: flag to compute and return the cost gradients with respect to the forecasts. When set, the second returned value is the per-sample gradient matrix of size(T, output_size).aggregate::Bool=true: when true, the returned cost is averaged over theTsamples. Only affects the cost; gradients are always per-sample. ...
compute_cost(model, X, Y, with_gradients=false, aggregate=true)Variant of compute_cost accepting any combination of the supported input containers — matrices, vectors, Tables.jl-compatible tables such as a DataFrame, and a Dict mapping each Forecast variable to its series. Both arguments are normalized to matrices together — which is what lets the rows be matched by sample_key — and the call is forwarded to the matrix method.
X and Y are @nospecialized. Without it this wrapper is specialized on the concrete container types, and specializing it drags the whole compute_cost body — sample loop, solver calls and all — through inference again in the new context: measured at 174 s for the first table-typed call, against 0.001 s once compiled. Nothing here benefits from specialization, since both arguments are immediately converted to matrices.
ApplicationDrivenLearning.train! — Function
train!(model::Model, X, Y, options::Options)Train the predictive model of model so that it minimizes the assessed cost of the application.
...
Arguments
model::ApplicationDrivenLearning.Model: model to train. Its forecast model must have been set withset_forecast_model.X: input data of size(T, input_size). A matrix or vector, whose columns are taken in order, or a Tables.jl-compatible table such as aDataFrame, whose columns are selected by the predictive model'sinput_names.Y: realized values of size(T, output_size). A matrix or vector whose columns follow the predictive model output order, a Tables.jl-compatible table whose columns are matched to theForecastvariables by name, or a dictionary mapping eachForecastvariable to its length-Tseries.
A table is always matched by name and never falls back to column order; see set_forecast_model for how those names are declared.
options::Options: training mode and its parameters.
Returns a Solution with the best cost found and the corresponding parameter vector. The predictive model of model is updated in place with those parameters. ...
train!(build_model::Function, X, Y, options::Options)Train a model built by build_model, a zero-argument function returning a ready-to-solve Model — variables, constraints, objectives, set_optimizer and set_forecast_model all done.
Equivalent to building the model and calling train! on it, except that the function itself is available to the parallel backend. That is what DistributedBackend needs: its workers never run your script, and a model that has had set_optimizer called on it cannot be sent to them at all, so a builder is the only way they can obtain one. Passing a Model under that backend is an error.
Using one function for the whole run is also what makes the driver's model and the workers' models the same problem by construction, rather than two descriptions that have to be checked against each other.
The caller keeps no reference to the model this builds. The fitted parameters are in the returned Solution, so a fitted model is recovered with
sol = train!(build_model, X, Y, options)
m = build_model()
ApplicationDrivenLearning.apply_params(m.forecast, sol.params)Both methods return a Solution; a return type that depended on which one was called would be worse than this.