Skip to content

Commit ea1044b

Browse files
authored
Merge pull request #171 from JuliaControl/diff_interface
Changed: major NLP refactoring for flexibility and significantly improve performance
2 parents 4269e90 + fc00d65 commit ea1044b

File tree

7 files changed

+451
-246
lines changed

7 files changed

+451
-246
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ModelPredictiveControl"
22
uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c"
33
authors = ["Francis Gagnon"]
4-
version = "1.4.1"
4+
version = "1.4.2"
55

66
[deps]
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

src/controller/nonlinmpc.jl

+131-160
Original file line numberDiff line numberDiff line change
@@ -505,19 +505,36 @@ function init_optimization!(mpc::NonLinMPC, model::SimModel, optim)
505505
JuMP.set_attribute(optim, "nlp_scaling_max_gradient", 10.0/C)
506506
end
507507
end
508-
Jfunc, gfuncs, geqfuncs = get_optim_functions(mpc, optim)
509-
@operator(optim, J, nZ̃, Jfunc)
508+
Jfunc, ∇Jfunc!, gfuncs, ∇gfuncs!, geqfuncs, ∇geqfuncs! = get_optim_functions(mpc, optim)
509+
@operator(optim, J, nZ̃, Jfunc, ∇Jfunc!)
510510
@objective(optim, Min, J(Z̃var...))
511-
init_nonlincon!(mpc, model, transcription, gfuncs, geqfuncs)
511+
init_nonlincon!(mpc, model, transcription, gfuncs, ∇gfuncs!, geqfuncs, ∇geqfuncs!)
512512
set_nonlincon!(mpc, model, optim)
513513
return nothing
514514
end
515515

516516
"""
517-
get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel) -> Jfunc, gfuncs, geqfuncs
517+
get_optim_functions(
518+
mpc::NonLinMPC, optim::JuMP.GenericModel
519+
) -> Jfunc, ∇Jfunc!, gfuncs, ∇gfuncs!, geqfuncs, ∇geqfuncs!
518520
519-
Get the objective `Jfunc` function, and constraint `gfuncs` and `geqfuncs` function vectors
520-
for [`NonLinMPC`](@ref).
521+
Return the functions for the nonlinear optimization of `mpc` [`NonLinMPC`](@ref) controller.
522+
523+
Return the nonlinear objective `Jfunc` function, and `∇Jfunc!`, to compute its gradient.
524+
Also return vectors with the nonlinear inequality constraint functions `gfuncs`, and
525+
`∇gfuncs!`, for the associated gradients. Lastly, also return vectors with the nonlinear
526+
equality constraint functions `geqfuncs` and gradients `∇geqfuncs!`.
527+
528+
This method is really indicated and I'm not proud of it. That's because of 3 elements:
529+
530+
- These functions are used inside the nonlinear optimization, so they must be type-stable
531+
and as efficient as possible.
532+
- The `JuMP` NLP syntax forces splatting for the decision variable, which implies use
533+
of `Vararg{T,N}` (see the [performance tip](https://docs.julialang.org/en/v1/manual/performance-tips/#Be-aware-of-when-Julia-avoids-specializing))
534+
and memoization to avoid redundant computations. This is already complex, but it's even
535+
worse knowing that most automatic differentiation tools do not support splatting.
536+
- The signature of gradient and hessian functions is not the same for univariate (`nZ̃ == 1`)
537+
and multivariate (`nZ̃ > 1`) operators in `JuMP`. Both must be defined.
521538
522539
Inspired from: [User-defined operators with vector outputs](https://jump.dev/JuMP.jl/stable/tutorials/nonlinear/tips_and_tricks/#User-defined-operators-with-vector-outputs)
523540
"""
@@ -529,6 +546,7 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
529546
nΔŨ, nUe, nŶe = nu*Hc + nϵ, nU + nu, nŶ + ny
530547
Ncache = nZ̃ + 3
531548
myNaN = convert(JNT, NaN) # fill Z̃ with NaNs to force update_simulations! at 1st call:
549+
# ---------------------- differentiation cache ---------------------------------------
532550
Z̃_cache::DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache(fill(myNaN, nZ̃), Ncache)
533551
ΔŨ_cache::DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache(zeros(JNT, nΔŨ), Ncache)
534552
x̂0end_cache::DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache(zeros(JNT, nx̂), Ncache)
@@ -541,22 +559,26 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
541559
gc_cache::DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache(zeros(JNT, nc), Ncache)
542560
g_cache::DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache(zeros(JNT, ng), Ncache)
543561
geq_cache::DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache(zeros(JNT, neq), Ncache)
544-
function update_simulations!(Z̃, Z̃tup::NTuple{N, T}) where {N, T<:Real}
545-
if any(new !== old for (new, old) in zip(Z̃tup, Z̃)) # new Z̃tup, update predictions:
546-
Z̃1 = Z̃tup[begin]
547-
for i in eachindex(Z̃tup)
548-
Z̃[i] = Z̃tup[i] # Z̃ .= Z̃tup seems to produce a type instability
562+
# --------------------- update simulation function ------------------------------------
563+
function update_simulations!(
564+
Z̃arg::Union{NTuple{N, T}, AbstractVector{T}}, Z̃cache
565+
) where {N, T<:Real}
566+
if isdifferent(Z̃cache, Z̃arg)
567+
for i in eachindex(Z̃cache)
568+
# Z̃cache .= Z̃arg is type unstable with Z̃arg::NTuple{N, FowardDiff.Dual}
569+
Z̃cache[i] = Z̃arg[i]
549570
end
571+
= Z̃cache
550572
ϵ = (nϵ 0) ? Z̃[end] : zero(T) # ϵ = 0 if nϵ == 0 (meaning no relaxation)
551-
ΔŨ = get_tmp(ΔŨ_cache, Z̃1)
552-
x̂0end = get_tmp(x̂0end_cache, Z̃1)
553-
Ue, Ŷe = get_tmp(Ue_cache, Z̃1), get_tmp(Ŷe_cache, Z̃1)
554-
U0, Ŷ0 = get_tmp(U0_cache, Z̃1), get_tmp(Ŷ0_cache, Z̃1)
555-
X̂0, Û0 = get_tmp(X̂0_cache, Z̃1), get_tmp(Û0_cache, Z̃1)
556-
gc, g = get_tmp(gc_cache, Z̃1), get_tmp(g_cache, Z̃1)
557-
geq = get_tmp(geq_cache, Z̃1)
573+
ΔŨ = get_tmp(ΔŨ_cache, T)
574+
x̂0end = get_tmp(x̂0end_cache, T)
575+
Ue, Ŷe = get_tmp(Ue_cache, T), get_tmp(Ŷe_cache, T)
576+
U0, Ŷ0 = get_tmp(U0_cache, T), get_tmp(Ŷ0_cache, T)
577+
X̂0, Û0 = get_tmp(X̂0_cache, T), get_tmp(Û0_cache, T)
578+
gc, g = get_tmp(gc_cache, T), get_tmp(g_cache, T)
579+
geq = get_tmp(geq_cache, T)
558580
U0 = getU0!(U0, mpc, Z̃)
559-
ΔŨ = getΔŨ!(ΔŨ, mpc, mpc.transcription, Z̃)
581+
ΔŨ = getΔŨ!(ΔŨ, mpc, transcription, Z̃)
560582
Ŷ0, x̂0end = predict!(Ŷ0, x̂0end, X̂0, Û0, mpc, model, transcription, U0, Z̃)
561583
Ue, Ŷe = extended_vectors!(Ue, Ŷe, mpc, U0, Ŷ0)
562584
gc = con_custom!(gc, mpc, Ue, Ŷe, ϵ)
@@ -565,160 +587,109 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
565587
end
566588
return nothing
567589
end
568-
function Jfunc(Z̃tup::Vararg{T, N}) where {N, T<:Real}
569-
Z̃1 = Z̃tup[begin]
570-
= get_tmp(Z̃_cache, Z̃1)
571-
update_simulations!(Z̃, Z̃tup)
572-
ΔŨ = get_tmp(ΔŨ_cache, Z̃1)
573-
Ue, Ŷe = get_tmp(Ue_cache, Z̃1), get_tmp(Ŷe_cache, Z̃1)
574-
U0, Ŷ0 = get_tmp(U0_cache, Z̃1), get_tmp(Ŷ0_cache, Z̃1)
590+
# --------------------- objective functions -------------------------------------------
591+
function Jfunc(Z̃arg::Vararg{T, N}) where {N, T<:Real}
592+
update_simulations!(Z̃arg, get_tmp(Z̃_cache, T))
593+
ΔŨ = get_tmp(ΔŨ_cache, T)
594+
Ue, Ŷe = get_tmp(Ue_cache, T), get_tmp(Ŷe_cache, T)
595+
U0, Ŷ0 = get_tmp(U0_cache, T), get_tmp(Ŷ0_cache, T)
575596
return obj_nonlinprog!(Ŷ0, U0, mpc, model, Ue, Ŷe, ΔŨ)::T
576597
end
577-
function gfunc_i(i, Z̃tup::NTuple{N, T}) where {N, T<:Real}
578-
Z̃1 = Z̃tup[begin]
579-
= get_tmp(Z̃_cache, Z̃1)
580-
update_simulations!(Z̃, Z̃tup)
581-
g = get_tmp(g_cache, Z̃1)
582-
return g[i]::T
598+
function Jfunc_vec(Z̃arg::AbstractVector{T}) where T<:Real
599+
update_simulations!(Z̃arg, get_tmp(Z̃_cache, T))
600+
ΔŨ = get_tmp(ΔŨ_cache, T)
601+
Ue, Ŷe = get_tmp(Ue_cache, T), get_tmp(Ŷe_cache, T)
602+
U0, Ŷ0 = get_tmp(U0_cache, T), get_tmp(Ŷ0_cache, T)
603+
return obj_nonlinprog!(Ŷ0, U0, mpc, model, Ue, Ŷe, ΔŨ)::T
583604
end
584-
gfuncs = Vector{Function}(undef, ng)
585-
for i in 1:ng
586-
# this is another syntax for anonymous function, allowing parameters T and N:
587-
gfuncs[i] = function (Z̃tup::Vararg{T, N}) where {N, T<:Real}
588-
return gfunc_i(i, Z̃tup)
605+
Z̃_∇J = fill(myNaN, nZ̃)
606+
∇J = Vector{JNT}(undef, nZ̃) # gradient of objective J
607+
∇J_buffer = GradientBuffer(Jfunc_vec, Z̃_∇J)
608+
∇Jfunc! = if nZ̃ == 1
609+
function (Z̃arg::T) where T<:Real
610+
Z̃_∇J .= Z̃arg
611+
gradient!(∇J, ∇J_buffer, Z̃_∇J)
612+
return ∇J[begin] # univariate syntax, see JuMP.@operator doc
589613
end
590-
end
591-
function gfunceq_i(i, Z̃tup::NTuple{N, T}) where {N, T<:Real}
592-
Z̃1 = Z̃tup[begin]
593-
= get_tmp(Z̃_cache, Z̃1)
594-
update_simulations!(Z̃, Z̃tup)
595-
geq = get_tmp(geq_cache, Z̃1)
596-
return geq[i]::T
597-
end
598-
geqfuncs = Vector{Function}(undef, neq)
599-
for i in 1:neq
600-
geqfuncs[i] = function (Z̃tup::Vararg{T, N}) where {N, T<:Real}
601-
return gfunceq_i(i, Z̃tup)
614+
else
615+
function (∇J::AbstractVector{T}, Z̃arg::Vararg{T, N}) where {N, T<:Real}
616+
Z̃_∇J .= Z̃arg
617+
gradient!(∇J, ∇J_buffer, Z̃_∇J)
618+
return ∇J # multivariate syntax, see JuMP.@operator doc
602619
end
603620
end
604-
return Jfunc, gfuncs, geqfuncs
605-
end
606-
607-
"""
608-
init_nonlincon!(
609-
mpc::NonLinMPC, model::LinModel, ::TranscriptionMethod, gfuncs, geqfuncs
610-
)
611-
612-
Init nonlinear constraints for [`LinModel`](@ref).
613-
614-
The only nonlinear constraints are the custom inequality constraints `gc`.
615-
"""
616-
function init_nonlincon!(
617-
mpc::NonLinMPC, ::LinModel, ::TranscriptionMethod,
618-
gfuncs::Vector{<:Function}, geqfuncs::Vector{<:Function}
619-
)
620-
optim, con = mpc.optim, mpc.con
621-
nZ̃ = length(mpc.Z̃)
622-
if length(con.i_g) 0
623-
i_base = 0
624-
for i in 1:con.nc
625-
name = Symbol("g_c_$i")
626-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
621+
# --------------------- inequality constraint functions -------------------------------
622+
gfuncs = Vector{Function}(undef, ng)
623+
for i in eachindex(gfuncs)
624+
func_i = function (Z̃arg::Vararg{T, N}) where {N, T<:Real}
625+
update_simulations!(Z̃arg, get_tmp(Z̃_cache, T))
626+
g = get_tmp(g_cache, T)
627+
return g[i]::T
627628
end
629+
gfuncs[i] = func_i
628630
end
629-
return nothing
630-
end
631-
632-
"""
633-
init_nonlincon!(
634-
mpc::NonLinMPC, model::NonLinModel, ::MultipleShooting, gfuncs, geqfuncs
635-
)
636-
637-
Init nonlinear constraints for [`NonLinModel`](@ref) and [`MultipleShooting`](@ref).
638-
639-
The nonlinear constraints are the output prediction `Ŷ` bounds, the custom inequality
640-
constraints `gc` and all the nonlinear equality constraints `geq`.
641-
"""
642-
function init_nonlincon!(
643-
mpc::NonLinMPC, ::NonLinModel, ::MultipleShooting,
644-
gfuncs::Vector{<:Function}, geqfuncs::Vector{<:Function}
645-
)
646-
optim, con = mpc.optim, mpc.con
647-
ny, nx̂, Hp, nZ̃ = mpc.estim.model.ny, mpc.estim.nx̂, mpc.Hp, length(mpc.Z̃)
648-
# --- nonlinear inequality constraints ---
649-
if length(con.i_g) 0
650-
i_base = 0
651-
for i in eachindex(con.Y0min)
652-
name = Symbol("g_Y0min_$i")
653-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
654-
end
655-
i_base = 1Hp*ny
656-
for i in eachindex(con.Y0max)
657-
name = Symbol("g_Y0max_$i")
658-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
631+
function gfunc_vec!(g, Z̃vec::AbstractVector{T}) where T<:Real
632+
update_simulations!(Z̃vec, get_tmp(Z̃_cache, T))
633+
g .= get_tmp(g_cache, T)
634+
return g
635+
end
636+
Z̃_∇g = fill(myNaN, nZ̃)
637+
g_vec = Vector{JNT}(undef, ng)
638+
∇g = Matrix{JNT}(undef, ng, nZ̃) # Jacobian of inequality constraints g
639+
∇g_buffer = JacobianBuffer(gfunc_vec!, g_vec, Z̃_∇g)
640+
∇gfuncs! = Vector{Function}(undef, ng)
641+
for i in eachindex(∇gfuncs!)
642+
∇gfuncs![i] = if nZ̃ == 1
643+
function (Z̃arg::T) where T<:Real
644+
if isdifferent(Z̃arg, Z̃_∇g)
645+
Z̃_∇g .= Z̃arg
646+
jacobian!(∇g, ∇g_buffer, g_vec, Z̃_∇g)
647+
end
648+
return ∇g[i, begin] # univariate syntax, see JuMP.@operator doc
649+
end
650+
else
651+
function (∇g_i, Z̃arg::Vararg{T, N}) where {N, T<:Real}
652+
if isdifferent(Z̃arg, Z̃_∇g)
653+
Z̃_∇g .= Z̃arg
654+
jacobian!(∇g, ∇g_buffer, g_vec, Z̃_∇g)
655+
end
656+
return ∇g_i .= @views ∇g[i, :] # multivariate syntax, see JuMP.@operator doc
657+
end
659658
end
660-
i_base = 2Hp*ny
661-
for i in 1:con.nc
662-
name = Symbol("g_c_$i")
663-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
659+
end
660+
# --------------------- equality constraint functions ---------------------------------
661+
geqfuncs = Vector{Function}(undef, neq)
662+
for i in eachindex(geqfuncs)
663+
func_i = function (Z̃arg::Vararg{T, N}) where {N, T<:Real}
664+
update_simulations!(Z̃arg, get_tmp(Z̃_cache, T))
665+
geq = get_tmp(geq_cache, T)
666+
return geq[i]::T
664667
end
668+
geqfuncs[i] = func_i
665669
end
666-
# --- nonlinear equality constraints ---
667-
Z̃var = optim[:Z̃var]
668-
for i in 1:con.neq
669-
name = Symbol("geq_$i")
670-
geqfunc_i = JuMP.add_nonlinear_operator(optim, nZ̃, geqfuncs[i]; name)
671-
# set with @constrains here instead of set_nonlincon!, since the number of nonlinear
672-
# equality constraints is known and constant (±Inf are impossible):
673-
@constraint(optim, geqfunc_i(Z̃var...) == 0)
670+
function geqfunc_vec!(geq, Z̃vec::AbstractVector{T}) where T<:Real
671+
update_simulations!(Z̃vec, get_tmp(Z̃_cache, T))
672+
geq .= get_tmp(geq_cache, T)
673+
return geq
674674
end
675-
return nothing
676-
end
677-
678-
"""
679-
init_nonlincon!(
680-
mpc::NonLinMPC, model::NonLinModel, ::SingleShooting, gfuncs, geqfuncs
681-
)
682-
683-
Init nonlinear constraints for [`NonLinModel`](@ref) and [`SingleShooting`](@ref).
684-
685-
The nonlinear constraints are the custom inequality constraints `gc`, the output
686-
prediction `Ŷ` bounds and the terminal state `x̂end` bounds.
687-
"""
688-
function init_nonlincon!(
689-
mpc::NonLinMPC, ::NonLinModel, ::SingleShooting,
690-
gfuncs::Vector{<:Function}, geqfuncs::Vector{<:Function}
691-
)
692-
optim, con = mpc.optim, mpc.con
693-
ny, nx̂, Hp, nZ̃ = mpc.estim.model.ny, mpc.estim.nx̂, mpc.Hp, length(mpc.Z̃)
694-
if length(con.i_g) 0
695-
i_base = 0
696-
for i in eachindex(con.Y0min)
697-
name = Symbol("g_Y0min_$i")
698-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
699-
end
700-
i_base = 1Hp*ny
701-
for i in eachindex(con.Y0max)
702-
name = Symbol("g_Y0max_$i")
703-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
704-
end
705-
i_base = 2Hp*ny
706-
for i in eachindex(con.x̂0min)
707-
name = Symbol("g_x̂0min_$i")
708-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
709-
end
710-
i_base = 2Hp*ny + nx̂
711-
for i in eachindex(con.x̂0max)
712-
name = Symbol("g_x̂0max_$i")
713-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
714-
end
715-
i_base = 2Hp*ny + 2nx̂
716-
for i in 1:con.nc
717-
name = Symbol("g_c_$i")
718-
optim[name] = JuMP.add_nonlinear_operator(optim, nZ̃, gfuncs[i_base+i]; name)
719-
end
675+
Z̃_∇geq = fill(myNaN, nZ̃) # NaN to force update at 1st call
676+
geq_vec = Vector{JNT}(undef, neq)
677+
∇geq = Matrix{JNT}(undef, neq, nZ̃) # Jacobian of equality constraints geq
678+
∇geq_buffer = JacobianBuffer(geqfunc_vec!, geq_vec, Z̃_∇geq)
679+
∇geqfuncs! = Vector{Function}(undef, neq)
680+
for i in eachindex(∇geqfuncs!)
681+
# only multivariate syntax, univariate is impossible since nonlinear equality
682+
# constraints imply MultipleShooting, thus input increment ΔU and state X̂0 in Z̃:
683+
∇geqfuncs![i] =
684+
function (∇geq_i, Z̃arg::Vararg{T, N}) where {N, T<:Real}
685+
if isdifferent(Z̃arg, Z̃_∇geq)
686+
Z̃_∇geq .= Z̃arg
687+
jacobian!(∇geq, ∇geq_buffer, geq_vec, Z̃_∇geq)
688+
end
689+
return ∇geq_i .= @views ∇geq[i, :]
690+
end
720691
end
721-
return nothing
692+
return Jfunc, ∇Jfunc!, gfuncs, ∇gfuncs!, geqfuncs, ∇geqfuncs!
722693
end
723694

724695
"""

0 commit comments

Comments
 (0)