@@ -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
514514end
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
522539Inspired 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̃ = 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- Z̃ = 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- Z̃ = 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- Z̃ = 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 = 1 Hp* 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 = 2 Hp* 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 = 1 Hp* 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 = 2 Hp* 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 = 2 Hp* 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 = 2 Hp* ny + 2 nx̂
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!
722693end
723694
724695"""
0 commit comments