Skip to content

Commit 9038e0a

Browse files
authored
Merge pull request #25 from JuliaControl/terminal_cost
Added: terminal cost support
2 parents 24598de + 57d789a commit 9038e0a

File tree

7 files changed

+83
-50
lines changed

7 files changed

+83
-50
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 = "0.16.1"
4+
version = "0.17.0"
55

66
[deps]
77
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"

src/controller/construct.jl

+10-7
Original file line numberDiff line numberDiff line change
@@ -672,18 +672,18 @@ returns the augmented constraints ``\mathbf{ΔŨ_{min}}`` and ``\mathbf{ΔŨ_{
672672
```
673673
"""
674674
function relaxΔU(::SimModel{NT}, C, C_Δumin, C_Δumax, ΔUmin, ΔUmax, N_Hc) where {NT<:Real}
675-
diag_N_Hc = diag(N_Hc)
675+
nΔU = size(N_Hc, 1)
676676
if !isinf(C) # ΔŨ = [ΔU; ϵ]
677677
# 0 ≤ ϵ ≤ ∞
678678
ΔŨmin, ΔŨmax = [ΔUmin; NT[0.0]], [ΔUmax; NT[Inf]]
679679
A_ϵ = [zeros(NT, 1, length(ΔUmin)) NT[1.0]]
680680
A_ΔŨmin, A_ΔŨmax = -[I C_Δumin; A_ϵ], [I -C_Δumax; A_ϵ]
681-
Ñ_Hc = Diagonal{NT}([diag_N_Hc; C])
681+
Ñ_Hc = Hermitian([N_Hc zeros(NT, nΔU, 1);zeros(NT, 1, nΔU) C])
682682
else # ΔŨ = ΔU (only hard constraints)
683683
ΔŨmin, ΔŨmax = ΔUmin, ΔUmax
684-
I_Hc = Matrix{NT}(I, size(N_Hc))
684+
I_Hc = Matrix{NT}(I, nΔU, nΔU)
685685
A_ΔŨmin, A_ΔŨmax = -I_Hc, I_Hc
686-
Ñ_Hc = Diagonal{NT}(diag_N_Hc)
686+
Ñ_Hc = N_Hc
687687
end
688688
return A_ΔŨmin, A_ΔŨmax, ΔŨmin, ΔŨmax, Ñ_Hc
689689
end
@@ -817,9 +817,12 @@ function validate_weights(model, Hp, Hc, M_Hp, N_Hc, L_Hp, C, E=nothing)
817817
size(M_Hp) (nM,nM) && throw(ArgumentError("M_Hp size $(size(M_Hp)) ≠ (ny*Hp, ny*Hp) ($nM,$nM)"))
818818
size(N_Hc) (nN,nN) && throw(ArgumentError("N_Hc size $(size(N_Hc)) ≠ (nu*Hc, nu*Hc) ($nN,$nN)"))
819819
size(L_Hp) (nL,nL) && throw(ArgumentError("L_Hp size $(size(L_Hp)) ≠ (nu*Hp, nu*Hp) ($nL,$nL)"))
820-
(!isdiag(M_Hp) || any(diag(M_Hp).<0)) && throw(ArgumentError("M_Hp should be a positive semidefinite diagonal matrix"))
821-
(!isdiag(N_Hc) || any(diag(N_Hc).<0)) && throw(ArgumentError("N_Hc should be a positive semidefinite diagonal matrix"))
822-
(!isdiag(L_Hp) || any(diag(L_Hp).<0)) && throw(ArgumentError("L_Hp should be a positive semidefinite diagonal matrix"))
820+
(isdiag(M_Hp) && any(diag(M_Hp) .< 0)) && throw(ArgumentError("Mwt values should be nonnegative"))
821+
(isdiag(N_Hc) && any(diag(N_Hc) .< 0)) && throw(ArgumentError("Nwt values should be nonnegative"))
822+
(isdiag(L_Hp) && any(diag(L_Hp) .< 0)) && throw(ArgumentError("Lwt values should be nonnegative"))
823+
!ishermitian(M_Hp) && throw(ArgumentError("M_Hp should be hermitian"))
824+
!ishermitian(N_Hc) && throw(ArgumentError("N_Hc should be hermitian"))
825+
!ishermitian(L_Hp) && throw(ArgumentError("L_Hp should be hermitian"))
823826
size(C) () && throw(ArgumentError("Cwt should be a real scalar"))
824827
C < 0 && throw(ArgumentError("Cwt weight should be ≥ 0"))
825828
!isnothing(E) && size(E) () && throw(ArgumentError("Ewt should be a real scalar"))

src/controller/execute.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ function initpred!(mpc::PredictiveController, model::LinModel, d, ym, D̂, R̂y,
180180
mpc.R̂u .= R̂u
181181
C_u = mpc.T_lastu .- mpc.R̂u
182182
mpc..+= lmul!(2, (mpc.L_Hp*mpc.S̃)'*C_u)
183-
mpc.p .+= dot(C_y, mpc.L_Hp, C_u)
183+
mpc.p .+= dot(C_u, mpc.L_Hp, C_u)
184184
end
185185
return nothing
186186
end

src/controller/explicitmpc.jl

+11-10
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
44
::Vector{NT}
55
Hp::Int
66
Hc::Int
7-
M_Hp::Diagonal{NT, Vector{NT}}
8-
Ñ_Hc::Diagonal{NT, Vector{NT}}
9-
L_Hp::Diagonal{NT, Vector{NT}}
7+
M_Hp::Hermitian{NT, Matrix{NT}}
8+
Ñ_Hc::Hermitian{NT, Matrix{NT}}
9+
L_Hp::Hermitian{NT, Matrix{NT}}
1010
C::NT
1111
E::NT
1212
R̂u::Vector{NT}
@@ -41,7 +41,8 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
4141
Cwt = Inf # no slack variable ϵ for ExplicitMPC
4242
Ewt = 0 # economic costs not supported for ExplicitMPC
4343
validate_weights(model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt)
44-
M_Hp, N_Hc, L_Hp = Diagonal{NT}(M_Hp), Diagonal{NT}(N_Hc), Diagonal{NT}(L_Hp) # debug julia 1.6
44+
# Matrix() call is needed to convert `Diagonal` to normal `Matrix`
45+
M_Hp, N_Hc, L_Hp = Hermitian(Matrix(M_Hp)), Hermitian(Matrix(N_Hc)), Hermitian(Matrix(L_Hp))
4546
# dummy vals (updated just before optimization):
4647
R̂y, R̂u, T_lastu = zeros(NT, ny*Hp), zeros(NT, nu*Hp), zeros(NT, nu*Hp)
4748
noR̂u = iszero(L_Hp)
@@ -118,9 +119,9 @@ function ExplicitMPC(
118119
Mwt = fill(DEFAULT_MWT, model.ny),
119120
Nwt = fill(DEFAULT_NWT, model.nu),
120121
Lwt = fill(DEFAULT_LWT, model.nu),
121-
M_Hp = Diagonal(repeat(Mwt, Hp)),
122-
N_Hc = Diagonal(repeat(Nwt, Hc)),
123-
L_Hp = Diagonal(repeat(Lwt, Hp)),
122+
M_Hp = diagm(repeat(Mwt, Hp)),
123+
N_Hc = diagm(repeat(Nwt, Hc)),
124+
L_Hp = diagm(repeat(Lwt, Hp)),
124125
kwargs...
125126
)
126127
estim = SteadyKalmanFilter(model; kwargs...)
@@ -156,9 +157,9 @@ function ExplicitMPC(
156157
Mwt = fill(DEFAULT_MWT, estim.model.ny),
157158
Nwt = fill(DEFAULT_NWT, estim.model.nu),
158159
Lwt = fill(DEFAULT_LWT, estim.model.nu),
159-
M_Hp = Diagonal(repeat(Mwt, Hp)),
160-
N_Hc = Diagonal(repeat(Nwt, Hc)),
161-
L_Hp = Diagonal(repeat(Lwt, Hp)),
160+
M_Hp = diagm(repeat(Mwt, Hp)),
161+
N_Hc = diagm(repeat(Nwt, Hc)),
162+
L_Hp = diagm(repeat(Lwt, Hp)),
162163
) where {NT<:Real, SE<:StateEstimator{NT}}
163164
isa(estim.model, LinModel) || error("estim.model type must be a LinModel")
164165
nk = estimate_delays(estim.model)

src/controller/linmpc.jl

+17-15
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ struct LinMPC{
1414
::Vector{NT}
1515
Hp::Int
1616
Hc::Int
17-
M_Hp::Diagonal{NT, Vector{NT}}
18-
Ñ_Hc::Diagonal{NT, Vector{NT}}
19-
L_Hp::Diagonal{NT, Vector{NT}}
17+
M_Hp::Hermitian{NT, Matrix{NT}}
18+
Ñ_Hc::Hermitian{NT, Matrix{NT}}
19+
L_Hp::Hermitian{NT, Matrix{NT}}
2020
C::NT
2121
E::NT
2222
R̂u::Vector{NT}
@@ -49,7 +49,8 @@ struct LinMPC{
4949
= copy(model.yop) # dummy vals (updated just before optimization)
5050
Ewt = 0 # economic costs not supported for LinMPC
5151
validate_weights(model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt)
52-
M_Hp, N_Hc, L_Hp = Diagonal{NT}(M_Hp), Diagonal{NT}(N_Hc), Diagonal{NT}(L_Hp) # debug julia 1.6
52+
# Matrix() call is needed to convert `Diagonal` to normal `Matrix`
53+
M_Hp, N_Hc, L_Hp = Hermitian(Matrix(M_Hp)), Hermitian(Matrix(N_Hc)), Hermitian(Matrix(L_Hp))
5354
# dummy vals (updated just before optimization):
5455
R̂y, R̂u, T_lastu = zeros(NT, ny*Hp), zeros(NT, nu*Hp), zeros(NT, nu*Hp)
5556
noR̂u = iszero(L_Hp)
@@ -102,8 +103,9 @@ in which the weight matrices are repeated ``H_p`` or ``H_c`` times by default:
102103
\mathbf{L}_{H_p} &= \text{diag}\mathbf{(L,L,...,L)}
103104
\end{aligned}
104105
```
105-
Time-varying weights over the horizons are also supported. The ``\mathbf{ΔU}`` includes the
106-
input increments ``\mathbf{Δu}(k+j) = \mathbf{u}(k+j) - \mathbf{u}(k+j-1)`` from ``j=0`` to
106+
Time-varying and non-diagonal weights are also supported. Modify the last block in
107+
``\mathbf{M}_{H_p}`` to specify a terminal weight. The ``\mathbf{ΔU}`` includes the input
108+
increments ``\mathbf{Δu}(k+j) = \mathbf{u}(k+j) - \mathbf{u}(k+j-1)`` from ``j=0`` to
107109
``H_c-1``, the ``\mathbf{Ŷ}`` vector, the output predictions ``\mathbf{ŷ}(k+j)`` from
108110
``j=1`` to ``H_p``, and the ``\mathbf{U}`` vector, the manipulated inputs ``\mathbf{u}(k+j)``
109111
from ``j=0`` to ``H_p-1``. The slack variable ``ϵ`` relaxes the constraints, as described
@@ -119,9 +121,9 @@ arguments.
119121
- `Mwt=fill(1.0,model.ny)` : main diagonal of ``\mathbf{M}`` weight matrix (vector).
120122
- `Nwt=fill(0.1,model.nu)` : main diagonal of ``\mathbf{N}`` weight matrix (vector).
121123
- `Lwt=fill(0.0,model.nu)` : main diagonal of ``\mathbf{L}`` weight matrix (vector).
122-
- `M_Hp=Diagonal(repeat(Mwt),Hp)` : diagonal weight matrix ``\mathbf{M}_{H_p}``.
123-
- `N_Hc=Diagonal(repeat(Nwt),Hc)` : diagonal weight matrix ``\mathbf{N}_{H_c}``.
124-
- `L_Hp=Diagonal(repeat(Lwt),Hp)` : diagonal weight matrix ``\mathbf{L}_{H_p}``.
124+
- `M_Hp=diagm(repeat(Mwt,Hp))` : positive semidefinite symmetric matrix ``\mathbf{M}_{H_p}``.
125+
- `N_Hc=diagm(repeat(Nwt,Hc))` : positive semidefinite symmetric matrix ``\mathbf{N}_{H_c}``.
126+
- `L_Hp=diagm(repeat(Lwt,Hp))` : positive semidefinite symmetric matrix ``\mathbf{L}_{H_p}``.
125127
- `Cwt=1e5` : slack variable weight ``C`` (scalar), use `Cwt=Inf` for hard constraints only.
126128
- `optim=JuMP.Model(OSQP.MathOptInterfaceOSQP.Optimizer)` : quadratic optimizer used in
127129
the predictive controller, provided as a [`JuMP.Model`](https://jump.dev/JuMP.jl/stable/api/JuMP/#JuMP.Model)
@@ -174,9 +176,9 @@ function LinMPC(
174176
Mwt = fill(DEFAULT_MWT, model.ny),
175177
Nwt = fill(DEFAULT_NWT, model.nu),
176178
Lwt = fill(DEFAULT_LWT, model.nu),
177-
M_Hp = Diagonal(repeat(Mwt, Hp)),
178-
N_Hc = Diagonal(repeat(Nwt, Hc)),
179-
L_Hp = Diagonal(repeat(Lwt, Hp)),
179+
M_Hp = diagm(repeat(Mwt, Hp)),
180+
N_Hc = diagm(repeat(Nwt, Hc)),
181+
L_Hp = diagm(repeat(Lwt, Hp)),
180182
Cwt = DEFAULT_CWT,
181183
optim::JuMP.GenericModel = JuMP.Model(DEFAULT_LINMPC_OPTIMIZER, add_bridges=false),
182184
kwargs...
@@ -215,9 +217,9 @@ function LinMPC(
215217
Mwt = fill(DEFAULT_MWT, estim.model.ny),
216218
Nwt = fill(DEFAULT_NWT, estim.model.nu),
217219
Lwt = fill(DEFAULT_LWT, estim.model.nu),
218-
M_Hp = Diagonal(repeat(Mwt, Hp)),
219-
N_Hc = Diagonal(repeat(Nwt, Hc)),
220-
L_Hp = Diagonal(repeat(Lwt, Hp)),
220+
M_Hp = diagm(repeat(Mwt, Hp)),
221+
N_Hc = diagm(repeat(Nwt, Hc)),
222+
L_Hp = diagm(repeat(Lwt, Hp)),
221223
Cwt = DEFAULT_CWT,
222224
optim::JM = JuMP.Model(DEFAULT_LINMPC_OPTIMIZER, add_bridges=false),
223225
) where {NT<:Real, SE<:StateEstimator{NT}, JM<:JuMP.GenericModel}

src/controller/nonlinmpc.jl

+17-16
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ struct NonLinMPC{
1515
::Vector{NT}
1616
Hp::Int
1717
Hc::Int
18-
M_Hp::Diagonal{NT, Vector{NT}}
19-
Ñ_Hc::Diagonal{NT, Vector{NT}}
20-
L_Hp::Diagonal{NT, Vector{NT}}
18+
M_Hp::Hermitian{NT, Matrix{NT}}
19+
Ñ_Hc::Hermitian{NT, Matrix{NT}}
20+
L_Hp::Hermitian{NT, Matrix{NT}}
2121
C::NT
2222
E::NT
2323
JE::JEfunc
@@ -50,7 +50,8 @@ struct NonLinMPC{
5050
nu, ny, nd = model.nu, model.ny, model.nd
5151
= copy(model.yop) # dummy vals (updated just before optimization)
5252
validate_weights(model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt, Ewt)
53-
M_Hp, N_Hc, L_Hp = Diagonal{NT}(M_Hp), Diagonal{NT}(N_Hc), Diagonal{NT}(L_Hp) # debug julia 1.6
53+
# Matrix() call is needed to convert `Diagonal` to normal `Matrix`
54+
M_Hp, N_Hc, L_Hp = Hermitian(Matrix(M_Hp)), Hermitian(Matrix(N_Hc)), Hermitian(Matrix(L_Hp))
5455
# dummy vals (updated just before optimization):
5556
R̂y, R̂u, T_lastu = zeros(NT, ny*Hp), zeros(NT, nu*Hp), zeros(NT, nu*Hp)
5657
noR̂u = iszero(L_Hp)
@@ -130,9 +131,9 @@ This method uses the default state estimator :
130131
- `Mwt=fill(1.0,model.ny)` : main diagonal of ``\mathbf{M}`` weight matrix (vector).
131132
- `Nwt=fill(0.1,model.nu)` : main diagonal of ``\mathbf{N}`` weight matrix (vector).
132133
- `Lwt=fill(0.0,model.nu)` : main diagonal of ``\mathbf{L}`` weight matrix (vector).
133-
- `M_Hp=Diagonal(repeat(Mwt),Hp)` : diagonal weight matrix ``\mathbf{M}_{H_p}``.
134-
- `N_Hc=Diagonal(repeat(Nwt),Hc)` : diagonal weight matrix ``\mathbf{N}_{H_c}``.
135-
- `L_Hp=Diagonal(repeat(Lwt),Hp)` : diagonal weight matrix ``\mathbf{L}_{H_p}``.
134+
- `M_Hp=diagm(repeat(Mwt,Hp))` : positive semidefinite symmetric matrix ``\mathbf{M}_{H_p}``.
135+
- `N_Hc=diagm(repeat(Nwt,Hc))` : positive semidefinite symmetric matrix ``\mathbf{N}_{H_c}``.
136+
- `L_Hp=diagm(repeat(Lwt,Hp))` : positive semidefinite symmetric matrix ``\mathbf{L}_{H_p}``.
136137
- `Cwt=1e5` : slack variable weight ``C`` (scalar), use `Cwt=Inf` for hard constraints only.
137138
- `Ewt=0.0` : economic costs weight ``E`` (scalar).
138139
- `JE=(_,_,_)->0.0` : economic function ``J_E(\mathbf{U}_E, \mathbf{Ŷ}_E, \mathbf{D̂}_E)``.
@@ -179,9 +180,9 @@ function NonLinMPC(
179180
Mwt = fill(DEFAULT_MWT, model.ny),
180181
Nwt = fill(DEFAULT_NWT, model.nu),
181182
Lwt = fill(DEFAULT_LWT, model.nu),
182-
M_Hp = Diagonal(repeat(Mwt, Hp)),
183-
N_Hc = Diagonal(repeat(Nwt, Hc)),
184-
L_Hp = Diagonal(repeat(Lwt, Hp)),
183+
M_Hp = diagm(repeat(Mwt, Hp)),
184+
N_Hc = diagm(repeat(Nwt, Hc)),
185+
L_Hp = diagm(repeat(Lwt, Hp)),
185186
Cwt = DEFAULT_CWT,
186187
Ewt = DEFAULT_EWT,
187188
JE::Function = (_,_,_) -> 0.0,
@@ -199,9 +200,9 @@ function NonLinMPC(
199200
Mwt = fill(DEFAULT_MWT, model.ny),
200201
Nwt = fill(DEFAULT_NWT, model.nu),
201202
Lwt = fill(DEFAULT_LWT, model.nu),
202-
M_Hp = Diagonal(repeat(Mwt, Hp)),
203-
N_Hc = Diagonal(repeat(Nwt, Hc)),
204-
L_Hp = Diagonal(repeat(Lwt, Hp)),
203+
M_Hp = diagm(repeat(Mwt, Hp)),
204+
N_Hc = diagm(repeat(Nwt, Hc)),
205+
L_Hp = diagm(repeat(Lwt, Hp)),
205206
Cwt = DEFAULT_CWT,
206207
Ewt = DEFAULT_EWT,
207208
JE::Function = (_,_,_) -> 0.0,
@@ -242,9 +243,9 @@ function NonLinMPC(
242243
Mwt = fill(DEFAULT_MWT, estim.model.ny),
243244
Nwt = fill(DEFAULT_NWT, estim.model.nu),
244245
Lwt = fill(DEFAULT_LWT, estim.model.nu),
245-
M_Hp = Diagonal(repeat(Mwt, Hp)),
246-
N_Hc = Diagonal(repeat(Nwt, Hc)),
247-
L_Hp = Diagonal(repeat(Lwt, Hp)),
246+
M_Hp = diagm(repeat(Mwt, Hp)),
247+
N_Hc = diagm(repeat(Nwt, Hc)),
248+
L_Hp = diagm(repeat(Lwt, Hp)),
248249
Cwt = DEFAULT_CWT,
249250
Ewt = DEFAULT_EWT,
250251
JE::JEFunc = (_,_,_) -> 0.0,

test/test_predictive_control.jl

+26
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,32 @@ end
232232
@test info[:x̂end][1] 0 atol=1e-1
233233
end
234234

235+
@testset "LinMPC terminal cost" begin
236+
model = LinModel(ss([0.5 -0.4;0.6 0.5], [1 0;0 1], [1 0; 0 1], 0, 1))
237+
K = lqr(Discrete, model.A, model.Bu, I, 0.5I)
238+
M_end, _ = ControlSystemsBase.ared(model.A, model.Bu, I, 0.5I)
239+
M_Hp = [I(4) zeros(4,2); zeros(2,4) M_end]
240+
mpc = LinMPC(model; Hp=3, Hc=3, M_Hp, Nwt=[0; 0], Lwt=[0.5, 0.5], nint_ym=0)
241+
X_mpc = zeros(2,20)
242+
setstate!(mpc,[1,1])
243+
setstate!(model, [1,1])
244+
for i=1:20
245+
y = model()
246+
u = moveinput!(mpc, [0, 0])
247+
X_mpc[:,i] = model.x
248+
updatestate!(mpc, u, y)
249+
updatestate!(model, u)
250+
end
251+
X_lqr = zeros(2,20)
252+
x=[1,1]
253+
for i=1:20
254+
u = -K*x
255+
X_lqr[:,i] = x
256+
x = model.A*x + model.Bu*u
257+
end
258+
@test all(isapprox.(X_mpc, X_lqr, atol=1e-3))
259+
end
260+
235261
@testset "ExplicitMPC construction" begin
236262
model = LinModel(sys, Ts, i_d=[3])
237263
mpc1 = ExplicitMPC(model, Hp=15)

0 commit comments

Comments
 (0)