@@ -505,19 +505,36 @@ function init_optimization!(mpc::NonLinMPC, model::SimModel, optim)
505
505
JuMP. set_attribute (optim, " nlp_scaling_max_gradient" , 10.0 / C)
506
506
end
507
507
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! )
510
510
@objective (optim, Min, J (Z̃var... ))
511
- init_nonlincon! (mpc, model, transcription, gfuncs, geqfuncs)
511
+ init_nonlincon! (mpc, model, transcription, gfuncs, ∇gfuncs!, geqfuncs, ∇geqfuncs! )
512
512
set_nonlincon! (mpc, model, optim)
513
513
return nothing
514
514
end
515
515
516
516
"""
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!
518
520
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.
521
538
522
539
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)
523
540
"""
@@ -529,6 +546,7 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
529
546
nΔŨ, nUe, nŶe = nu* Hc + nϵ, nU + nu, nŶ + ny
530
547
Ncache = nZ̃ + 3
531
548
myNaN = convert (JNT, NaN ) # fill Z̃ with NaNs to force update_simulations! at 1st call:
549
+ # ---------------------- differentiation cache ---------------------------------------
532
550
Z̃_cache:: DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache (fill (myNaN, nZ̃), Ncache)
533
551
ΔŨ_cache:: DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache (zeros (JNT, nΔŨ), Ncache)
534
552
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
541
559
gc_cache:: DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache (zeros (JNT, nc), Ncache)
542
560
g_cache:: DiffCache{Vector{JNT}, Vector{JNT}} = DiffCache (zeros (JNT, ng), Ncache)
543
561
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]
549
570
end
571
+ Z̃ = Z̃cache
550
572
ϵ = (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 )
558
580
U0 = getU0! (U0, mpc, Z̃)
559
- ΔŨ = getΔŨ! (ΔŨ, mpc, mpc . transcription, Z̃)
581
+ ΔŨ = getΔŨ! (ΔŨ, mpc, transcription, Z̃)
560
582
Ŷ0, x̂0end = predict! (Ŷ0, x̂0end, X̂0, Û0, mpc, model, transcription, U0, Z̃)
561
583
Ue, Ŷe = extended_vectors! (Ue, Ŷe, mpc, U0, Ŷ0)
562
584
gc = con_custom! (gc, mpc, Ue, Ŷe, ϵ)
@@ -565,160 +587,109 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
565
587
end
566
588
return nothing
567
589
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)
575
596
return obj_nonlinprog! (Ŷ0, U0, mpc, model, Ue, Ŷe, ΔŨ):: T
576
597
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
583
604
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
589
613
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
602
619
end
603
620
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
627
628
end
629
+ gfuncs[i] = func_i
628
630
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
659
658
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
664
667
end
668
+ geqfuncs[i] = func_i
665
669
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
674
674
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
720
691
end
721
- return nothing
692
+ return Jfunc, ∇Jfunc!, gfuncs, ∇gfuncs!, geqfuncs, ∇geqfuncs!
722
693
end
723
694
724
695
"""
0 commit comments