Skip to content

Commit c55ca85

Browse files
authored
Merge pull request #360 from JuliaControl/mhe_custom_con
added: custom nonlinear inequality constraints for `MovingHorizonEstimator` 🥳
2 parents d1ccc9a + 08b7d9b commit c55ca85

11 files changed

Lines changed: 886 additions & 377 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ModelPredictiveControl"
22
uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c"
3-
version = "2.3.2"
3+
version = "2.4.0"
44
authors = ["Francis Gagnon"]
55

66
[deps]

docs/src/index.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ The documentation is divided in two parts:
3030
```@contents
3131
Depth = 2
3232
Pages = [
33-
"manual/installation.md",
34-
"manual/linmpc.md",
35-
"manual/nonlinmpc.md",
36-
"manual/mtk.md"
33+
joinpath("manual", "installation.md"),
34+
joinpath("manual", "linmpc.md"),
35+
joinpath("manual", "nonlinmpc.md"),
36+
joinpath("manual", "mtk.md")
3737
]
3838
```
3939

@@ -42,11 +42,11 @@ Pages = [
4242
```@contents
4343
Depth = 2
4444
Pages = [
45-
"public/sim_model.md",
46-
"public/state_estim.md",
47-
"public/predictive_control.md",
48-
"public/generic_func.md",
49-
"public/plot_sim.md",
45+
joinpath("public", "sim_model.md"),
46+
joinpath("public", "state_estim.md"),
47+
joinpath("public", "predictive_control.md"),
48+
joinpath("public", "generic_func.md"),
49+
joinpath("public", "plot_sim.md")
5050
]
5151
```
5252

@@ -55,8 +55,8 @@ Pages = [
5555
```@contents
5656
Depth = 1
5757
Pages = [
58-
"internals/sim_model.md",
59-
"internals/state_estim.md",
60-
"internals/predictive_control.md",
58+
joinpath("internals", "sim_model.md"),
59+
joinpath("internals", "state_estim.md"),
60+
joinpath("internals", "predictive_control.md")
6161
]
6262
```

src/controller/execute.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ The argument `Z̃orΔŨ` can be the augmented decision vector ``\mathbf{Z̃}``
206206
input increment vector ``\mathbf{ΔŨ}``, it works with both.
207207
"""
208208
function getϵ(mpc::PredictiveController, Z̃orΔŨ::AbstractVector{NT}) where NT<:Real
209-
return mpc. 0 ? Z̃orΔŨ[end] : zero(NT)
209+
return mpc.> 0 ? Z̃orΔŨ[end] : zero(NT)
210210
end
211211

212212
"""

src/controller/nonlinmpc.jl

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ struct NonLinMPC{
121121
# dummy vals (updated just before optimization):
122122
d0, D̂0, D̂e = zeros(NT, nd), zeros(NT, nd*Hp), zeros(NT, nd + nd*Hp)
123123
Uop, Yop, Dop = repeat(model.uop, Hp), repeat(model.yop, Hp), repeat(model.dop, Hp)
124-
test_custom_functions(NT, model, JE, gc!, nc, Uop, Yop, Dop, p)
124+
test_custom_function_mpc(NT, model, JE, gc!, nc, Uop, Yop, Dop, p)
125125
Mo, Co, λo = init_orthocolloc(model, transcription)
126126
nZ̃ = get_nZ(estim, transcription, Hp, Hc) +
127127
= zeros(NT, nZ̃)
@@ -167,7 +167,7 @@ controller minimizes the following objective function at each discrete time ``k`
167167
```
168168
subject to [`setconstraint!`](@ref) bounds, and the custom inequality constraints:
169169
```math
170-
\mathbf{g_c}(\mathbf{U_e}, \mathbf{Ŷ_e}, \mathbf{D̂_e}, \mathbf{p}, ϵ) ≤ \mathbf{0}
170+
\mathbf{g_c}(\mathbf{U_e, Ŷ_e, D̂_e, p}, ϵ) ≤ \mathbf{0}
171171
```
172172
with the decision variables ``\mathbf{Z}`` and slack ``ϵ``. By default, a [`SingleShooting`](@ref)
173173
transcription method is used, hence ``\mathbf{Z=ΔU}``. The economic function ``J_E`` can
@@ -222,8 +222,8 @@ This controller allocates memory at each time step for the optimization.
222222
- `JE=(_,_,_,_,_)->0.0` : economic or custom cost function ``J_E(\mathbf{U_e}, \mathbf{Ŷ_e},
223223
\mathbf{D̂_e}, \mathbf{p}, ϵ)``.
224224
- `gc=(_,_,_,_,_,_)->nothing` or `gc!` : custom nonlinear inequality constraint function
225-
``\mathbf{g_c}(\mathbf{U_e}, \mathbf{Ŷ_e}, \mathbf{D̂_e}, \mathbf{p}, ϵ)``, mutating or
226-
not (details in Extended Help).
225+
``\mathbf{g_c}(\mathbf{U_e}, \mathbf{Ŷ_e, D̂_e, p}, ϵ)``, mutating or not (details in
226+
Extended Help).
227227
- `nc=0` : number of custom nonlinear inequality constraints.
228228
- `p=model.p` : ``J_E`` and ``\mathbf{g_c}`` functions parameter ``\mathbf{p}`` (any type).
229229
- `transcription=SingleShooting()` : a [`TranscriptionMethod`](@ref) for the optimization.
@@ -276,22 +276,24 @@ NonLinMPC controller with a sample time Ts = 10.0 s:
276276
extended vectors ``\mathbf{U_e}``, ``\mathbf{Ŷ_e}`` and ``\mathbf{D̂_e}`` as arguments.
277277
They also receives the slack ``ϵ`` (scalar), which is always zero if `Cwt=Inf`. The
278278
following table details the vector sizes and the time steps of the first and last data
279-
point in them:
279+
point in them.
280280
281-
| VECTOR | SIZE | FIRST TIME STEP | LAST TIME STEP |
282-
| :--------------- | :------------- | :-------------- | :------------- |
283-
| ``\mathbf{U_e}`` | `(nu*(Hp+1),)` | ``k + 0`` | ``k + H_p`` |
284-
| ``\mathbf{Ŷ_e}`` | `(ny*(Hp+1),)` | ``k + 0`` | ``k + H_p`` |
285-
| ``\mathbf{D̂_e}`` | `(nd*(Hp+1),)` | ``k + 0`` | ``k + H_p`` |
281+
| ARGUMENT | SIZE | FIRST SAMPLE | LAST SAMPLE |
282+
| :--------------- | :------------- | :----------- | :-----------|
283+
| ``\mathbf{U_e}`` | `((Hp+1)*nu,)` | ``k`` | ``k + H_p`` |
284+
| ``\mathbf{Ŷ_e}`` | `((Hp+1)*ny,)` | ``k`` | ``k + H_p`` |
285+
| ``\mathbf{D̂_e}`` | `((Hp+1)*nd,)` | ``k`` | ``k + H_p`` |
286+
| ``\mathbf{p}`` | var. | — | — |
287+
| ``ϵ`` | `()` | — | — |
286288
287289
More precisely, the last two time steps in ``\mathbf{U_e}`` are forced to be equal, i.e.
288290
``\mathbf{u}(k+H_p) = \mathbf{u}(k+H_p-1)``, since ``H_c ≤ H_p`` implies that
289291
``\mathbf{Δu}(k+H_p) = \mathbf{0}``. The vectors ``\mathbf{ŷ}(k)`` and ``\mathbf{d}(k)``
290292
are the current state estimator output and measured disturbance, respectively, and
291293
``\mathbf{Ŷ}`` and ``\mathbf{D̂}``, their respective predictions from ``k+1`` to ``k+H_p``.
292294
If `LHS` represents the result of the left-hand side in the inequality
293-
``\mathbf{g_c}(\mathbf{U_e}, \mathbf{Ŷ_e}, \mathbf{D̂_e}, \mathbf{p}, ϵ) ≤ \mathbf{0}``,
294-
the function `gc` can be implemented in two possible ways:
295+
``\mathbf{g_c}(\mathbf{U_e, Ŷ_e, D̂_e, p}, ϵ) ≤ \mathbf{0}``, the function `gc` can be
296+
implemented in two possible ways:
295297
296298
1. **Non-mutating function** (out-of-place): define it as `gc(Ue, Ŷe, D̂e, p, ϵ) -> LHS`.
297299
This syntax is simple and intuitive but it allocates more memory.
@@ -442,7 +444,7 @@ function NonLinMPC(
442444
nb = move_blocking(Hp, Hc)
443445
Hc = get_Hc(nb)
444446
validate_JE(NT, JE)
445-
gc! = get_mutating_gc(NT, gc)
447+
gc! = get_mutating_gc_mpc(NT, gc)
446448
weights = ControllerWeights(estim.model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt, Ewt)
447449
hessian = validate_hessian(hessian, gradient, DEFAULT_NONLINMPC_HESSIAN)
448450
return NonLinMPC{NT}(
@@ -471,18 +473,23 @@ function validate_JE(NT, JE)
471473
end
472474

473475
"""
474-
validate_gc(NT, gc) -> ismutating
476+
validate_gc_mpc(NT, gc) -> ismutating
475477
476-
Validate `gc` function argument signature and return `true` if it is mutating.
478+
Validate `gc` function argument signature for MPC and return `true` if it is mutating.
477479
"""
478-
function validate_gc(NT, gc)
480+
function validate_gc_mpc(NT, gc)
479481
ismutating = hasmethod(
480482
gc,
481483
# LHS, Ue, Ŷe, D̂e, p, ϵ
482484
Tuple{Vector{NT}, Vector{NT}, Vector{NT}, Vector{NT}, Any, NT}
483485
)
486+
isnonmutating = hasmethod(
487+
gc,
488+
# Ue, Ŷe, D̂e, p, ϵ
489+
Tuple{Vector{NT}, Vector{NT}, Vector{NT}, Any, NT}
490+
)
484491
# Ue, Ŷe, D̂e, p, ϵ
485-
if !(ismutating || hasmethod(gc, Tuple{Vector{NT}, Vector{NT}, Vector{NT}, Any, NT}))
492+
if !(ismutating || isnonmutating)
486493
error(
487494
"the custom constraint function has no method with type signature "*
488495
"gc(Ue::Vector{$(NT)}, Ŷe::Vector{$(NT)}, D̂e::Vector{$(NT)}, p::Any, ϵ::$(NT)) "*
@@ -494,8 +501,8 @@ function validate_gc(NT, gc)
494501
end
495502

496503
"Get mutating custom constraint function `gc!` from the provided function in argument."
497-
function get_mutating_gc(NT, gc)
498-
ismutating_gc = validate_gc(NT, gc)
504+
function get_mutating_gc_mpc(NT, gc)
505+
ismutating_gc = validate_gc_mpc(NT, gc)
499506
gc! = if ismutating_gc
500507
gc
501508
else
@@ -508,15 +515,15 @@ function get_mutating_gc(NT, gc)
508515
end
509516

510517
"""
511-
test_custom_functions(NT, model::SimModel, JE, gc!, nc, Uop, Yop, Dop, p)
518+
test_custom_function_mpc(NT, model::SimModel, JE, gc!, nc, Uop, Yop, Dop, p)
512519
513520
Test the custom functions `JE` and `gc!` at the operating point `Uop`, `Yop`, `Dop`.
514521
515522
This function is called at the end of `NonLinMPC` construction. It warns the user if the
516523
custom cost `JE` and constraint `gc!` functions crash at `model` operating points. This
517524
should ease troubleshooting of simple bugs e.g.: the user forgets to set the `nc` argument.
518525
"""
519-
function test_custom_functions(NT, model::SimModel, JE, gc!, nc, Uop, Yop, Dop, p)
526+
function test_custom_function_mpc(NT, model::SimModel, JE, gc!, nc, Uop, Yop, Dop, p)
520527
uop, dop, yop = model.uop, model.dop, model.yop
521528
Ue, Ŷe, D̂e = [Uop; uop], [yop; Yop], [dop; Dop]
522529
ϵ = zero(NT)
@@ -729,11 +736,11 @@ function init_optimization!(
729736
mpc::NonLinMPC, model::SimModel, optim::JuMP.GenericModel{JNT}
730737
) where JNT<:Real
731738
# --- variables and linear constraints ---
732-
con, transcription = mpc.con, mpc.transcription
739+
con = mpc.con
733740
nZ̃ = length(mpc.Z̃)
734741
JuMP.num_variables(optim) == 0 || JuMP.empty!(optim)
735742
JuMP.set_silent(optim)
736-
limit_solve_time(mpc.optim, mpc.estim.model.Ts)
743+
limit_solve_time(mpc.optim, model.Ts)
737744
@variable(optim, Z̃var[1:nZ̃])
738745
A = con.A[con.i_b, :]
739746
b = con.b[con.i_b]
@@ -742,15 +749,8 @@ function init_optimization!(
742749
beq = con.beq
743750
@constraint(optim, linconstrainteq, Aeq*Z̃var .== beq)
744751
# --- nonlinear optimization init ---
745-
if mpc.== 1 && JuMP.solver_name(optim) == "Ipopt"
746-
C = mpc.weights.Ñ_Hc[end]
747-
try
748-
JuMP.get_attribute(optim, "nlp_scaling_max_gradient")
749-
catch
750-
# default "nlp_scaling_max_gradient" to `10.0/C` if not already set:
751-
JuMP.set_attribute(optim, "nlp_scaling_max_gradient", 10.0/C)
752-
end
753-
end
752+
C = mpc.> 0 ? mpc.weights.Ñ_Hc[end, end] : Inf
753+
set_scaling_gradient!(optim, C)
754754
J_op = get_nonlinobj_op(mpc, optim)
755755
g_oracle, geq_oracle = get_nonlincon_oracle(mpc, optim)
756756
@objective(optim, Min, J_op(Z̃var...))
@@ -926,7 +926,7 @@ function get_nonlincon_oracle(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JN
926926
myNaN, myInf = convert(JNT, NaN), convert(JNT, Inf)
927927
ΔŨ::Vector{JNT} = zeros(JNT, nΔŨ)
928928
x̂0end::Vector{JNT} = zeros(JNT, nx̂)
929-
K::Vector{JNT} = zeros(JNT, nK)
929+
K::Vector{JNT} = zeros(JNT, nK)
930930
Ue::Vector{JNT}, Ŷe::Vector{JNT} = zeros(JNT, nUe), zeros(JNT, nŶe)
931931
U0::Vector{JNT}, Ŷ0::Vector{JNT} = zeros(JNT, nU), zeros(JNT, nŶ)
932932
Û0::Vector{JNT}, X̂0::Vector{JNT} = zeros(JNT, nU), zeros(JNT, nX̂)
@@ -1081,7 +1081,7 @@ function update_predictions!(
10811081
ΔŨ = getΔŨ!(ΔŨ, mpc, transcription, Z̃)
10821082
Ŷ0, x̂0end = predict!(Ŷ0, x̂0end, X̂0, Û0, K, mpc, model, transcription, U0, Z̃)
10831083
Ue, Ŷe = extended_vectors!(Ue, Ŷe, mpc, U0, Ŷ0)
1084-
ϵ = getϵ(mpc, Z̃)
1084+
ϵ = getϵ(mpc, Z̃)
10851085
gc = con_custom!(gc, mpc, Ue, Ŷe, ϵ)
10861086
g = con_nonlinprog!(g, mpc, model, transcription, x̂0end, Ŷ0, gc, ϵ)
10871087
geq = con_nonlinprogeq!(geq, X̂0, Û0, K, mpc, model, transcription, U0, Z̃)
@@ -1114,7 +1114,7 @@ end
11141114
Evaluate the custom inequality constraint `gc` in-place and return it.
11151115
"""
11161116
function con_custom!(gc, mpc::NonLinMPC, Ue, Ŷe, ϵ)
1117-
mpc.con.nc 0 && mpc.con.gc!(gc, Ue, Ŷe, mpc.D̂e, mpc.p, ϵ)
1117+
mpc.con.nc > 0 && mpc.con.gc!(gc, Ue, Ŷe, mpc.D̂e, mpc.p, ϵ)
11181118
return gc
11191119
end
11201120

src/controller/transcription.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1330,7 +1330,7 @@ The method mutates the `g` vectors in argument and returns it. Only the custom c
13301330
`gc` are include in the `g` vector.
13311331
"""
13321332
function con_nonlinprog!(
1333-
g, ::PredictiveController, ::LinModel, ::TranscriptionMethod, _ , _ , gc, ϵ
1333+
g, ::PredictiveController, ::LinModel, ::TranscriptionMethod, _ , _ , gc, _
13341334
)
13351335
for i in eachindex(g)
13361336
g[i] = gc[i]

src/estimator/construct.jl

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ struct KalmanCovariances{
9595
rethrow()
9696
end
9797
end
98+
invQ̂_He = Hermitian(repeatdiag(invQ̂, He), :L)
9899
try
99100
inv!(invR̂)
100101
catch err
@@ -104,10 +105,7 @@ struct KalmanCovariances{
104105
rethrow()
105106
end
106107
end
107-
invQ̂_He = repeatdiag(invQ̂, He)
108-
invR̂_He = repeatdiag(invR̂, He)
109-
invQ̂_He = Hermitian(invQ̂_He, :L)
110-
invR̂_He = Hermitian(invR̂_He, :L)
108+
invR̂_He = Hermitian(repeatdiag(invR̂, He), :L)
111109
return new{NT, Q̂C, R̂C}(P̂_0, P̂, Q̂, R̂, invP̄, invQ̂_He, invR̂_He)
112110
end
113111
end
@@ -125,7 +123,7 @@ end
125123
"""
126124
validate_kfcov(model, i_ym, nint_u, nint_ym, Q̂, R̂, P̂_0=nothing)
127125
128-
Validate sizes and Hermitianity of process `Q̂`` and sensor `R̂` noises covariance matrices.
126+
Validate sizes and Hermitianity of process `Q̂` and sensor `R̂` noises covariance matrices.
129127
130128
Also validate initial estimate covariance `P̂_0`, if provided.
131129
"""

0 commit comments

Comments
 (0)