Skip to content

Added: NonLinMPC and MovingHorizonEstimator now use value_and_gradient!/jacobian! of DI.jl #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "ModelPredictiveControl"
uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c"
authors = ["Francis Gagnon"]
version = "1.5.3"
version = "1.6.0"

[deps]
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
Expand Down
1 change: 1 addition & 0 deletions docs/src/internals/predictive_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ModelPredictiveControl.init_quadprog
ModelPredictiveControl.init_stochpred
ModelPredictiveControl.init_matconstraint_mpc
ModelPredictiveControl.init_nonlincon!
ModelPredictiveControl.get_optim_functions(::NonLinMPC, ::ModelPredictiveControl.GenericModel)
```

## Update Quadratic Optimization
Expand Down
1 change: 1 addition & 0 deletions docs/src/internals/state_estim.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ModelPredictiveControl.relaxX̂
ModelPredictiveControl.relaxŴ
ModelPredictiveControl.relaxV̂
ModelPredictiveControl.init_matconstraint_mhe
ModelPredictiveControl.get_optim_functions(::MovingHorizonEstimator, ::ModelPredictiveControl.GenericModel)
```

## Update Quadratic Optimization
Expand Down
1 change: 1 addition & 0 deletions src/ModelPredictiveControl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ using ProgressLogging

using DifferentiationInterface: ADTypes.AbstractADType, AutoForwardDiff, AutoSparse
using DifferentiationInterface: gradient!, jacobian!, prepare_gradient, prepare_jacobian
using DifferentiationInterface: value_and_gradient!, value_and_jacobian!
using DifferentiationInterface: Constant, Cache
using SparseConnectivityTracer: TracerSparsityDetector
using SparseMatrixColorings: GreedyColoringAlgorithm, sparsity_pattern
Expand Down
150 changes: 71 additions & 79 deletions src/controller/nonlinmpc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,8 @@ equality constraint functions `geqfuncs` and gradients `∇geqfuncs!`.
This method is really intricate and I'm not proud of it. That's because of 3 elements:

- These functions are used inside the nonlinear optimization, so they must be type-stable
and as efficient as possible.
and as efficient as possible. All the function outputs and derivatives are cached and
updated in-place if required to use the efficient [`value_and_jacobian!`](@extref DifferentiationInterface DifferentiationInterface.value_and_jacobian!).
- The `JuMP` NLP syntax forces splatting for the decision variable, which implies use
of `Vararg{T,N}` (see the [performance tip][@extref Julia Be-aware-of-when-Julia-avoids-specializing]
) and memoization to avoid redundant computations. This is already complex, but it's even
Expand All @@ -576,16 +577,17 @@ This method is really intricate and I'm not proud of it. That's because of 3 ele
Inspired from: [User-defined operators with vector outputs](@extref JuMP User-defined-operators-with-vector-outputs)
"""
function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT<:Real
# ----- common cache for Jfunc, gfuncs, geqfuncs called with floats -------------------
# ----------- common cache for Jfunc, gfuncs and geqfuncs ----------------------------
model = mpc.estim.model
grad, jac = mpc.gradient, mpc.jacobian
nu, ny, nx̂, nϵ, nk = model.nu, model.ny, mpc.estim.nx̂, mpc.nϵ, model.nk
Hp, Hc = mpc.Hp, mpc.Hc
ng, nc, neq = length(mpc.con.i_g), mpc.con.nc, mpc.con.neq
nZ̃, nU, nŶ, nX̂, nK = length(mpc.Z̃), Hp*nu, Hp*ny, Hp*nx̂, Hp*nk
nΔŨ, nUe, nŶe = nu*Hc + nϵ, nU + nu, nŶ + ny
strict = Val(true)
myNaN = convert(JNT, NaN) # NaN to force update_simulations! at first call:
::Vector{JNT} = fill(myNaN, nZ̃)
myNaN = convert(JNT, NaN)
J::Vector{JNT} = zeros(JNT, 1)
ΔŨ::Vector{JNT} = zeros(JNT, nΔŨ)
x̂0end::Vector{JNT} = zeros(JNT, nx̂)
K0::Vector{JNT} = zeros(JNT, nK)
Expand All @@ -595,128 +597,118 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
gc::Vector{JNT}, g::Vector{JNT} = zeros(JNT, nc), zeros(JNT, ng)
geq::Vector{JNT} = zeros(JNT, neq)
# ---------------------- objective function -------------------------------------------
function Jfunc(Z̃arg::Vararg{T, N}) where {N, T<:Real}
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
end
return obj_nonlinprog!(Ŷ0, U0, mpc, model, Ue, Ŷe, ΔŨ)::T
end
function Jfunc!(Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
return obj_nonlinprog!(Ŷ0, U0, mpc, model, Ue, Ŷe, ΔŨ)
end
Z̃_∇J = fill(myNaN, nZ̃)
Z̃_∇J = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
∇J_context = (
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
Cache(Û0), Cache(K0), Cache(X̂0),
Cache(gc), Cache(g), Cache(geq),
)
∇J_prep = prepare_gradient(Jfunc!, mpc.gradient, Z̃_∇J, ∇J_context...; strict)
∇J_prep = prepare_gradient(Jfunc!, grad, Z̃_∇J, ∇J_context...; strict)
∇J = Vector{JNT}(undef, nZ̃)
∇Jfunc! = if nZ̃ == 1
function update_objective!(J, ∇J, Z̃, Z̃arg)
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
J[], _ = value_and_gradient!(Jfunc!, ∇J, ∇J_prep, grad, Z̃_∇J, ∇J_context...)
end
end
function Jfunc(Z̃arg::Vararg{T, N}) where {N, T<:Real}
update_objective!(J, ∇J, Z̃_∇J, Z̃arg)
return J[]::T
end
∇Jfunc! = if nZ̃ == 1 # univariate syntax (see JuMP.@operator doc):
function (Z̃arg)
Z̃_∇J .= Z̃arg
gradient!(Jfunc!, ∇J, ∇J_prep, mpc.gradient, Z̃_∇J, ∇J_context...)
return ∇J[begin] # univariate syntax, see JuMP.@operator doc
update_objective!(J, ∇J, Z̃_∇J, Z̃arg)
return ∇J[begin]
end
else
function (∇J::AbstractVector{T}, Z̃arg::Vararg{T, N}) where {N, T<:Real}
Z̃_∇J .= Z̃arg
gradient!(Jfunc!, ∇J, ∇J_prep, mpc.gradient, Z̃_∇J, ∇J_context...)
return ∇J # multivariate syntax, see JuMP.@operator doc
else # multivariate syntax (see JuMP.@operator doc):
function (∇Jarg::AbstractVector{T}, Z̃arg::Vararg{T, N}) where {N, T<:Real}
update_objective!(J, ∇J, Z̃_∇J, Z̃arg)
return ∇Jarg .= ∇J
end
end
# --------------------- inequality constraint functions -------------------------------
gfuncs = Vector{Function}(undef, ng)
for i in eachindex(gfuncs)
gfunc_i = function (Z̃arg::Vararg{T, N}) where {N, T<:Real}
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
update_predictions!(
ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃
)
end
return g[i]::T
end
gfuncs[i] = gfunc_i
end
function gfunc!(g, Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, geq)
return update_predictions!(
ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃
)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
return g
end
Z̃_∇g = fill(myNaN, nZ̃)
Z̃_∇g = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
∇g_context = (
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
Cache(Û0), Cache(K0), Cache(X̂0),
Cache(Û0), Cache(K0), Cache(X̂0),
Cache(gc), Cache(geq),
)
# temporarily enable all the inequality constraints for sparsity detection:
mpc.con.i_g[1:end-nc] .= true
∇g_prep = prepare_jacobian(gfunc!, g, mpc.jacobian, Z̃_∇g, ∇g_context...; strict)
∇g_prep = prepare_jacobian(gfunc!, g, jac, Z̃_∇g, ∇g_context...; strict)
mpc.con.i_g[1:end-nc] .= false
∇g = init_diffmat(JNT, mpc.jacobian, ∇g_prep, nZ̃, ng)
∇g = init_diffmat(JNT, jac, ∇g_prep, nZ̃, ng)
function update_con!(g, ∇g, Z̃, Z̃arg)
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
value_and_jacobian!(gfunc!, g, ∇g, ∇g_prep, jac, Z̃, ∇g_context...)
end
end
gfuncs = Vector{Function}(undef, ng)
for i in eachindex(gfuncs)
gfunc_i = function (Z̃arg::Vararg{T, N}) where {N, T<:Real}
update_con!(g, ∇g, Z̃_∇g, Z̃arg)
return g[i]::T
end
gfuncs[i] = gfunc_i
end
∇gfuncs! = Vector{Function}(undef, ng)
for i in eachindex(∇gfuncs!)
∇gfuncs_i! = if nZ̃ == 1
∇gfuncs_i! = if nZ̃ == 1 # univariate syntax (see JuMP.@operator doc):
function (Z̃arg::T) where T<:Real
if isdifferent(Z̃arg, Z̃_∇g)
Z̃_∇g .= Z̃arg
jacobian!(gfunc!, g, ∇g, ∇g_prep, mpc.jacobian, Z̃_∇g, ∇g_context...)
end
return ∇g[i, begin] # univariate syntax, see JuMP.@operator doc
update_con!(g, ∇g, Z̃_∇g, Z̃arg)
return ∇g[i, begin]
end
else
else # multivariate syntax (see JuMP.@operator doc):
function (∇g_i, Z̃arg::Vararg{T, N}) where {N, T<:Real}
if isdifferent(Z̃arg, Z̃_∇g)
Z̃_∇g .= Z̃arg
jacobian!(gfunc!, g, ∇g, ∇g_prep, mpc.jacobian, Z̃_∇g, ∇g_context...)
end
return ∇g_i .= @views ∇g[i, :] # multivariate syntax, see JuMP.@operator doc
update_con!(g, ∇g, Z̃_∇g, Z̃arg)
return ∇g_i .= @views ∇g[i, :]
end
end
∇gfuncs![i] = ∇gfuncs_i!
end
# --------------------- equality constraint functions ---------------------------------
geqfuncs = Vector{Function}(undef, neq)
for i in eachindex(geqfuncs)
geqfunc_i = function (Z̃arg::Vararg{T, N}) where {N, T<:Real}
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
update_predictions!(
ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃
)
end
return geq[i]::T
end
geqfuncs[i] = geqfunc_i
end
function geqfunc!(geq, Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g)
return update_predictions!(
ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃
)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
return geq
end
Z̃_∇geq = fill(myNaN, nZ̃)
Z̃_∇geq = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
∇geq_context = (
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
Cache(Û0), Cache(K0), Cache(X̂0),
Cache(gc), Cache(g)
)
∇geq_prep = prepare_jacobian(geqfunc!, geq, mpc.jacobian, Z̃_∇geq, ∇geq_context...; strict)
∇geq = init_diffmat(JNT, mpc.jacobian, ∇geq_prep, nZ̃, neq)
∇geq_prep = prepare_jacobian(geqfunc!, geq, jac, Z̃_∇geq, ∇geq_context...; strict)
∇geq = init_diffmat(JNT, jac, ∇geq_prep, nZ̃, neq)
function update_con_eq!(geq, ∇geq, Z̃, Z̃arg)
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
value_and_jacobian!(geqfunc!, geq, ∇geq, ∇geq_prep, jac, Z̃, ∇geq_context...)
end
end
geqfuncs = Vector{Function}(undef, neq)
for i in eachindex(geqfuncs)
geqfunc_i = function (Z̃arg::Vararg{T, N}) where {N, T<:Real}
update_con_eq!(geq, ∇geq, Z̃_∇geq, Z̃arg)
return geq[i]::T
end
geqfuncs[i] = geqfunc_i
end
∇geqfuncs! = Vector{Function}(undef, neq)
for i in eachindex(∇geqfuncs!)
# only multivariate syntax, univariate is impossible since nonlinear equality
# constraints imply MultipleShooting, thus input increment ΔU and state X̂0 in Z̃:
∇geqfuncs_i! =
function (∇geq_i, Z̃arg::Vararg{T, N}) where {N, T<:Real}
if isdifferent(Z̃arg, Z̃_∇geq)
Z̃_∇geq .= Z̃arg
jacobian!(
geqfunc!, geq, ∇geq, ∇geq_prep, mpc.jacobian, Z̃_∇geq, ∇geq_context...
)
end
update_con_eq!(geq, ∇geq, Z̃_∇geq, Z̃arg)
return ∇geq_i .= @views ∇geq[i, :]
end
∇geqfuncs![i] = ∇geqfuncs_i!
Expand Down
79 changes: 40 additions & 39 deletions src/estimator/mhe/construct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1314,97 +1314,98 @@ Also return vectors with the nonlinear inequality constraint functions `gfuncs`,
This method is really intricate and I'm not proud of it. That's because of 3 elements:

- These functions are used inside the nonlinear optimization, so they must be type-stable
and as efficient as possible.
and as efficient as possible. All the function outputs and derivatives are cached and
updated in-place if required to use the efficient [`value_and_jacobian!`](@extref DifferentiationInterface DifferentiationInterface.value_and_jacobian!).
- The `JuMP` NLP syntax forces splatting for the decision variable, which implies use
of `Vararg{T,N}` (see the [performance tip](@extref Julia Be-aware-of-when-Julia-avoids-specializing))
and memoization to avoid redundant computations. This is already complex, but it's even
worse knowing that most automatic differentiation tools do not support splatting.
- The signature of gradient and hessian functions is not the same for univariate (`nZ̃ == 1`)
and multivariate (`nZ̃ > 1`) operators in `JuMP`. Both must be defined.

Inspired from: [User-defined operators with vector outputs](@extref JuMP User-defined-operators-with-vector-outputs)
"""
function get_optim_functions(
estim::MovingHorizonEstimator, ::JuMP.GenericModel{JNT},
) where {JNT <: Real}
# ---------- common cache for Jfunc, gfuncs called with floats ------------------------
# ----------- common cache for Jfunc and gfuncs --------------------------------------
model, con = estim.model, estim.con
grad, jac = estim.gradient, estim.jacobian
nx̂, nym, nŷ, nu, nϵ, nk = estim.nx̂, estim.nym, model.ny, model.nu, estim.nϵ, model.nk
He = estim.He
nV̂, nX̂, ng, nZ̃ = He*nym, He*nx̂, length(con.i_g), length(estim.Z̃)
myNaN = convert(JNT, NaN) # NaN to force update_simulations! at first call
strict = Val(true)
Z̃::Vector{JNT} = fill(myNaN, nZ̃)
myNaN = convert(JNT, NaN)
J::Vector{JNT} = zeros(JNT, 1)
V̂::Vector{JNT}, X̂0::Vector{JNT} = zeros(JNT, nV̂), zeros(JNT, nX̂)
k0::Vector{JNT} = zeros(JNT, nk)
k0::Vector{JNT} = zeros(JNT, nk)
û0::Vector{JNT}, ŷ0::Vector{JNT} = zeros(JNT, nu), zeros(JNT, nŷ)
g::Vector{JNT} = zeros(JNT, ng)
x̄::Vector{JNT} = zeros(JNT, nx̂)
# --------------------- objective functions -------------------------------------------
function Jfunc(Z̃arg::Vararg{T, N}) where {N, T<:Real}
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
update_prediction!(V̂, X̂0, û0, k0, ŷ0, g, estim, Z̃)
end
return obj_nonlinprog!(x̄, estim, model, V̂, Z̃)::T
end
function Jfunc!(Z̃, V̂, X̂0, û0, k0, ŷ0, g, x̄)
update_prediction!(V̂, X̂0, û0, k0, ŷ0, g, estim, Z̃)
return obj_nonlinprog!(x̄, estim, model, V̂, Z̃)
end
Z̃_∇J = fill(myNaN, nZ̃)
Z̃_∇J = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
∇J_context = (
Cache(V̂), Cache(X̂0), Cache(û0), Cache(k0), Cache(ŷ0),
Cache(g),
Cache(x̄),
)
# temporarily "fill" the estimation window for the preparation of the gradient:
estim.Nk[] = He
∇J_prep = prepare_gradient(Jfunc!, estim.gradient, Z̃_∇J, ∇J_context...; strict)
∇J_prep = prepare_gradient(Jfunc!, grad, Z̃_∇J, ∇J_context...; strict)
estim.Nk[] = 0
∇J = Vector{JNT}(undef, nZ̃)
∇Jfunc! = function (∇J::AbstractVector{T}, Z̃arg::Vararg{T, N}) where {N, T<:Real}
function update_objective!(J, ∇J, Z̃, Z̃arg)
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
J[], _ = value_and_gradient!(Jfunc!, ∇J, ∇J_prep, grad, Z̃_∇J, ∇J_context...)
end
end
function Jfunc(Z̃arg::Vararg{T, N}) where {N, T<:Real}
update_objective!(J, ∇J, Z̃_∇J, Z̃arg)
return J[]::T
end
∇Jfunc! = function (∇Jarg::AbstractVector{T}, Z̃arg::Vararg{T, N}) where {N, T<:Real}
# only the multivariate syntax of JuMP.@operator, univariate is impossible for MHE
# since Z̃ comprises the arrival state estimate AND the estimated process noise
Z̃_∇J .= Z̃arg
gradient!(Jfunc!, ∇J, ∇J_prep, estim.gradient, Z̃_∇J, ∇J_context...)
return ∇J
update_objective!(J, ∇J, Z̃_∇J, Z̃arg)
return ∇Jarg .= ∇J
end

# --------------------- inequality constraint functions -------------------------------
gfuncs = Vector{Function}(undef, ng)
for i in eachindex(gfuncs)
gfunc_i = function (Z̃arg::Vararg{T, N}) where {N, T<:Real}
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
update_prediction!(V̂, X̂0, û0, k0, ŷ0, g, estim, Z̃)
end
return g[i]::T
end
gfuncs[i] = gfunc_i
end
function gfunc!(g, Z̃, V̂, X̂0, û0, k0, ŷ0)
return update_prediction!(V̂, X̂0, û0, k0, ŷ0, g, estim, Z̃)
end
Z̃_∇g = fill(myNaN, nZ̃)
Z̃_∇g = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
∇g_context = (
Cache(V̂), Cache(X̂0), Cache(û0), Cache(k0), Cache(ŷ0),
)
# temporarily enable all the inequality constraints for sparsity detection:
estim.con.i_g .= true
estim.Nk[] = He
∇g_prep = prepare_jacobian(gfunc!, g, estim.jacobian, Z̃_∇g, ∇g_context...; strict)
∇g_prep = prepare_jacobian(gfunc!, g, jac, Z̃_∇g, ∇g_context...; strict)
estim.con.i_g .= false
estim.Nk[] = 0
∇g = init_diffmat(JNT, estim.jacobian, ∇g_prep, nZ̃, ng)
∇g = init_diffmat(JNT, jac, ∇g_prep, nZ̃, ng)
function update_con!(g, ∇g, Z̃, Z̃arg)
if isdifferent(Z̃arg, Z̃)
Z̃ .= Z̃arg
value_and_jacobian!(gfunc!, g, ∇g, ∇g_prep, jac, Z̃, ∇g_context...)
end
end
gfuncs = Vector{Function}(undef, ng)
for i in eachindex(gfuncs)
gfunc_i = function (Z̃arg::Vararg{T, N}) where {N, T<:Real}
update_con!(g, ∇g, Z̃_∇g, Z̃arg)
return g[i]::T
end
gfuncs[i] = gfunc_i
end
∇gfuncs! = Vector{Function}(undef, ng)
for i in eachindex(∇gfuncs!)
∇gfuncs![i] = function (∇g_i, Z̃arg::Vararg{T, N}) where {N, T<:Real}
# only the multivariate syntax of JuMP.@operator, see above for the explanation
if isdifferent(Z̃arg, Z̃_∇g)
Z̃_∇g .= Z̃arg
jacobian!(gfunc!, g, ∇g, ∇g_prep, estim.jacobian, Z̃_∇g, ∇g_context...)
end
update_con!(g, ∇g, Z̃_∇g, Z̃arg)
return ∇g_i .= @views ∇g[i, :]
end
end
Expand Down