Skip to content

Commit 58e4f1e

Browse files
authored
Merge pull request #102 from JuliaControl/cleanup_&_mpc_lessalloc
Warning `preparestate!`, debug `InternalModel`, reduce alloc `PredictiveController`
2 parents 54bee46 + cf1d7a2 commit 58e4f1e

20 files changed

+224
-104
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.24.0"
4+
version = "0.24.1"
55

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

docs/src/internals/predictive_control.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ ModelPredictiveControl.linconstraint!(::PredictiveController, ::LinModel)
3434

3535
```@docs
3636
ModelPredictiveControl.optim_objective!(::PredictiveController)
37+
ModelPredictiveControl.getinput
3738
```

docs/src/internals/state_estim.md

-6
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@ ModelPredictiveControl.linconstraint!(::MovingHorizonEstimator, ::LinModel)
4040
ModelPredictiveControl.optim_objective!(::MovingHorizonEstimator)
4141
```
4242

43-
## Evaluate Estimated Output
44-
45-
```@docs
46-
ModelPredictiveControl.evalŷ
47-
```
48-
4943
## Remove Operating Points
5044

5145
```@docs

src/controller/construct.jl

+3-3
Original file line numberDiff line numberDiff line change
@@ -603,15 +603,15 @@ Init the quadratic programming Hessian `H̃` for MPC.
603603
604604
The matrix appear in the quadratic general form:
605605
```math
606-
J = \min_{\mathbf{ΔŨ}} \frac{1}{2}\mathbf{(ΔŨ)'H̃(ΔŨ)} + \mathbf{q̃'(ΔŨ)} + p
606+
J = \min_{\mathbf{ΔŨ}} \frac{1}{2}\mathbf{(ΔŨ)'H̃(ΔŨ)} + \mathbf{q̃'(ΔŨ)} + r
607607
```
608608
The Hessian matrix is constant if the model and weights are linear and time invariant (LTI):
609609
```math
610610
\mathbf{H̃} = 2 ( \mathbf{Ẽ}'\mathbf{M}_{H_p}\mathbf{Ẽ} + \mathbf{Ñ}_{H_c}
611611
+ \mathbf{S̃}'\mathbf{L}_{H_p}\mathbf{S̃} )
612612
```
613-
The vector ``\mathbf{q̃}`` and scalar ``p`` need recalculation each control period ``k``, see
614-
[`initpred!`](@ref). ``p`` does not impact the minima position. It is thus useless at
613+
The vector ``\mathbf{q̃}`` and scalar ``r`` need recalculation each control period ``k``, see
614+
[`initpred!`](@ref). ``r`` does not impact the minima position. It is thus useless at
615615
optimization but required to evaluate the minimal ``J`` value.
616616
"""
617617
function init_quadprog(::LinModel, Ẽ, S̃, M_Hp, Ñ_Hc, L_Hp)

src/controller/execute.jl

+38-19
Original file line numberDiff line numberDiff line change
@@ -42,33 +42,34 @@ See also [`LinMPC`](@ref), [`ExplicitMPC`](@ref), [`NonLinMPC`](@ref).
4242
```jldoctest
4343
julia> mpc = LinMPC(LinModel(tf(5, [2, 1]), 3), Nwt=[0], Hp=1000, Hc=1);
4444
45-
julia> ry = [5]; u = moveinput!(mpc, ry); round.(u, digits=3)
45+
julia> preparestate!(mpc, [0]); ry = [5];
46+
47+
julia> u = moveinput!(mpc, ry); round.(u, digits=3)
4648
1-element Vector{Float64}:
4749
1.0
4850
```
4951
"""
5052
function moveinput!(
5153
mpc::PredictiveController,
5254
ry::Vector = mpc.estim.model.yop,
53-
d ::Vector = mpc.estim.buffer.empty;
54-
Dhat ::Vector = repeat(d, mpc.Hp),
55-
Rhaty::Vector = repeat(ry, mpc.Hp),
55+
d ::Vector = mpc.buffer.empty;
56+
Dhat ::Vector = repeat!(mpc.buffer.D̂, d, mpc.Hp),
57+
Rhaty::Vector = repeat!(mpc.buffer.R̂y, ry, mpc.Hp),
5658
Rhatu::Vector = mpc.Uop,
5759
= Dhat,
5860
R̂y = Rhaty,
5961
R̂u = Rhatu
6062
)
63+
if mpc.estim.direct && !mpc.estim.corrected[]
64+
@warn "preparestate! should be called before moveinput! with current estimators"
65+
end
6166
validate_args(mpc, ry, d, D̂, R̂y, R̂u)
6267
initpred!(mpc, mpc.estim.model, d, D̂, R̂y, R̂u)
6368
linconstraint!(mpc, mpc.estim.model)
6469
ΔŨ = optim_objective!(mpc)
65-
Δu = ΔŨ[1:mpc.estim.model.nu] # receding horizon principle: only Δu(k) is used (1st one)
66-
u = mpc.estim.lastu0 + mpc.estim.model.uop + Δu
67-
return u
70+
return getinput(mpc, ΔŨ)
6871
end
6972

70-
71-
7273
@doc raw"""
7374
getinfo(mpc::PredictiveController) -> info
7475
@@ -102,7 +103,7 @@ available for [`NonLinMPC`](@ref).
102103
```jldoctest
103104
julia> mpc = LinMPC(LinModel(tf(5, [2, 1]), 3), Nwt=[0], Hp=1, Hc=1);
104105
105-
julia> u = moveinput!(mpc, [10]);
106+
julia> preparestate!(mpc, [0]); u = moveinput!(mpc, [10]);
106107
107108
julia> round.(getinfo(mpc)[:Ŷ], digits=3)
108109
1-element Vector{Float64}:
@@ -164,7 +165,7 @@ end
164165
@doc raw"""
165166
initpred!(mpc::PredictiveController, model::LinModel, d, D̂, R̂y, R̂u) -> nothing
166167
167-
Init linear model prediction matrices `F, q̃, p` and current estimated output `ŷ`.
168+
Init linear model prediction matrices `F, q̃, r` and current estimated output `ŷ`.
168169
169170
See [`init_predmat`](@ref) and [`init_quadprog`](@ref) for the definition of the matrices.
170171
They are computed with these equations using in-place operations:
@@ -176,15 +177,15 @@ They are computed with these equations using in-place operations:
176177
\mathbf{C_u} &= \mathbf{T} \mathbf{u_0}(k-1) - (\mathbf{R̂_u - U_{op}}) \\
177178
\mathbf{q̃} &= 2[(\mathbf{M}_{H_p} \mathbf{Ẽ})' \mathbf{C_y}
178179
+ (\mathbf{L}_{H_p} \mathbf{S̃})' \mathbf{C_u}] \\
179-
p &= \mathbf{C_y}' \mathbf{M}_{H_p} \mathbf{C_y}
180+
r &= \mathbf{C_y}' \mathbf{M}_{H_p} \mathbf{C_y}
180181
+ \mathbf{C_u}' \mathbf{L}_{H_p} \mathbf{C_u}
181182
\end{aligned}
182183
```
183184
"""
184185
function initpred!(mpc::PredictiveController, model::LinModel, d, D̂, R̂y, R̂u)
185186
mul!(mpc.T_lastu0, mpc.T, mpc.estim.lastu0)
186-
ŷ, F, q̃, p = mpc.ŷ, mpc.F, mpc.q̃, mpc.p
187-
ŷ .= evalŷ(mpc.estim, d)
187+
ŷ, F, q̃, r = mpc.ŷ, mpc.F, mpc.q̃, mpc.r
188+
ŷ .= evaloutput(mpc.estim, d)
188189
predictstoch!(mpc, mpc.estim) # init mpc.F with Ŷs for InternalModel
189190
F .+= mpc.B
190191
mul!(F, mpc.K, mpc.estim.x̂0, 1, 1)
@@ -201,13 +202,13 @@ function initpred!(mpc::PredictiveController, model::LinModel, d, D̂, R̂y, R̂
201202
Cy = F .- mpc.R̂y0
202203
M_Hp_Ẽ = mpc.M_Hp*mpc.
203204
mul!(q̃, M_Hp_Ẽ', Cy)
204-
p .= dot(Cy, mpc.M_Hp, Cy)
205+
r .= dot(Cy, mpc.M_Hp, Cy)
205206
if ~mpc.noR̂u
206207
mpc.R̂u0 .= R̂u .- mpc.Uop
207208
Cu = mpc.T_lastu0 .- mpc.R̂u0
208209
L_Hp_S̃ = mpc.L_Hp*mpc.
209210
mul!(q̃, L_Hp_S̃', Cu, 1, 1)
210-
p .+= dot(Cu, mpc.L_Hp, Cu)
211+
r .+= dot(Cu, mpc.L_Hp, Cu)
211212
end
212213
lmul!(2, q̃)
213214
return nothing
@@ -220,7 +221,7 @@ Init `ŷ, F, d0, D̂0, D̂E, R̂y0, R̂u0` vectors when model is not a [`LinMod
220221
"""
221222
function initpred!(mpc::PredictiveController, model::SimModel, d, D̂, R̂y, R̂u)
222223
mul!(mpc.T_lastu0, mpc.T, mpc.estim.lastu0)
223-
mpc.ŷ .= evalŷ(mpc.estim, d)
224+
mpc.ŷ .= evaloutput(mpc.estim, d)
224225
predictstoch!(mpc, mpc.estim) # init mpc.F with Ŷs for InternalModel
225226
if model.nd 0
226227
mpc.d0 .= d .- model.dop
@@ -367,9 +368,9 @@ at specific input increments `ΔŨ` and predictions `Ŷ0` values. It mutates t
367368
function obj_nonlinprog!(
368369
U0, Ȳ, _ , mpc::PredictiveController, model::LinModel, Ŷ0, ΔŨ::AbstractVector{NT}
369370
) where NT <: Real
370-
J = obj_quadprog(ΔŨ, mpc.H̃, mpc.q̃) + mpc.p[]
371+
J = obj_quadprog(ΔŨ, mpc.H̃, mpc.q̃) + mpc.r[]
371372
if !iszero(mpc.E)
372-
ny, Hp, ŷ, D̂E = model.ny, mpc.Hp, mpc.ŷ, mpc.D̂E
373+
ŷ, D̂E = mpc.ŷ, mpc.D̂E
373374
U = U0
374375
U .+= mpc.Uop
375376
uend = @views U[(end-model.nu+1):end]
@@ -501,6 +502,24 @@ function preparestate!(mpc::PredictiveController, ym, d=mpc.estim.buffer.empty)
501502
return preparestate!(mpc.estim, ym, d)
502503
end
503504

505+
@doc raw"""
506+
getinput(mpc::PredictiveController, ΔŨ) -> u
507+
508+
Get current manipulated input `u` from a [`PredictiveController`](@ref) solution `ΔŨ`.
509+
510+
The first manipulated input ``\mathbf{u}(k)`` is extracted from the input increments vector
511+
``\mathbf{ΔŨ}`` and applied on the plant (from the receding horizon principle).
512+
"""
513+
function getinput(mpc, ΔŨ)
514+
Δu = mpc.buffer.u
515+
for i in 1:mpc.estim.model.nu
516+
Δu[i] = ΔŨ[i]
517+
end
518+
u = Δu
519+
u .+= mpc.estim.lastu0 .+ mpc.estim.model.uop
520+
return u
521+
end
522+
504523
"""
505524
updatestate!(mpc::PredictiveController, u, ym, d=[]) -> x̂next
506525

src/controller/explicitmpc.jl

+6-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
2424
B::Vector{NT}
2525
::Hermitian{NT, Matrix{NT}}
2626
::Vector{NT}
27-
p::Vector{NT}
27+
r::Vector{NT}
2828
H̃_chol::Cholesky{NT, Matrix{NT}}
2929
Ks::Matrix{NT}
3030
Ps::Matrix{NT}
@@ -34,6 +34,7 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
3434
Uop::Vector{NT}
3535
Yop::Vector{NT}
3636
Dop::Vector{NT}
37+
buffer::PredictiveControllerBuffer{NT}
3738
function ExplicitMPC{NT, SE}(
3839
estim::SE, Hp, Hc, M_Hp, N_Hc, L_Hp
3940
) where {NT<:Real, SE<:StateEstimator}
@@ -57,14 +58,15 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
5758
S̃, Ñ_Hc, Ẽ = S, N_Hc, E # no slack variable ϵ for ExplicitMPC
5859
= init_quadprog(model, Ẽ, S̃, M_Hp, Ñ_Hc, L_Hp)
5960
# dummy vals (updated just before optimization):
60-
q̃, p = zeros(NT, size(H̃, 1)), zeros(NT, 1)
61+
q̃, r = zeros(NT, size(H̃, 1)), zeros(NT, 1)
6162
H̃_chol = cholesky(H̃)
6263
Ks, Ps = init_stochpred(estim, Hp)
6364
# dummy vals (updated just before optimization):
6465
d0, D̂0, D̂E = zeros(NT, nd), zeros(NT, nd*Hp), zeros(NT, nd + nd*Hp)
6566
Uop, Yop, Dop = repeat(model.uop, Hp), repeat(model.yop, Hp), repeat(model.dop, Hp)
6667
nΔŨ = size(Ẽ, 2)
6768
ΔŨ = zeros(NT, nΔŨ)
69+
buffer = PredictiveControllerBuffer{NT}(nu, ny, nd, Hp)
6870
mpc = new{NT, SE}(
6971
estim,
7072
ΔŨ, ŷ,
@@ -73,11 +75,12 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
7375
R̂u0, R̂y0, noR̂u,
7476
S̃, T, T_lastu0,
7577
Ẽ, F, G, J, K, V, B,
76-
H̃, q̃, p,
78+
H̃, q̃, r,
7779
H̃_chol,
7880
Ks, Ps,
7981
d0, D̂0, D̂E,
8082
Uop, Yop, Dop,
83+
buffer
8184
)
8285
return mpc
8386
end

src/controller/linmpc.jl

+6-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ struct LinMPC{
3434
B::Vector{NT}
3535
::Hermitian{NT, Matrix{NT}}
3636
::Vector{NT}
37-
p::Vector{NT}
37+
r::Vector{NT}
3838
Ks::Matrix{NT}
3939
Ps::Matrix{NT}
4040
d0::Vector{NT}
@@ -43,6 +43,7 @@ struct LinMPC{
4343
Uop::Vector{NT}
4444
Yop::Vector{NT}
4545
Dop::Vector{NT}
46+
buffer::PredictiveControllerBuffer{NT}
4647
function LinMPC{NT, SE, JM}(
4748
estim::SE, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt, optim::JM
4849
) where {NT<:Real, SE<:StateEstimator, JM<:JuMP.GenericModel}
@@ -67,13 +68,14 @@ struct LinMPC{
6768
)
6869
= init_quadprog(model, Ẽ, S̃, M_Hp, Ñ_Hc, L_Hp)
6970
# dummy vals (updated just before optimization):
70-
q̃, p = zeros(NT, size(H̃, 1)), zeros(NT, 1)
71+
q̃, r = zeros(NT, size(H̃, 1)), zeros(NT, 1)
7172
Ks, Ps = init_stochpred(estim, Hp)
7273
# dummy vals (updated just before optimization):
7374
d0, D̂0, D̂E = zeros(NT, nd), zeros(NT, nd*Hp), zeros(NT, nd + nd*Hp)
7475
Uop, Yop, Dop = repeat(model.uop, Hp), repeat(model.yop, Hp), repeat(model.dop, Hp)
7576
nΔŨ = size(Ẽ, 2)
7677
ΔŨ = zeros(NT, nΔŨ)
78+
buffer = PredictiveControllerBuffer{NT}(nu, ny, nd, Hp)
7779
mpc = new{NT, SE, JM}(
7880
estim, optim, con,
7981
ΔŨ, ŷ,
@@ -82,10 +84,11 @@ struct LinMPC{
8284
R̂u0, R̂y0, noR̂u,
8385
S̃, T, T_lastu0,
8486
Ẽ, F, G, J, K, V, B,
85-
H̃, q̃, p,
87+
H̃, q̃, r,
8688
Ks, Ps,
8789
d0, D̂0, D̂E,
8890
Uop, Yop, Dop,
91+
buffer
8992
)
9093
init_optimization!(mpc, model, optim)
9194
return mpc

src/controller/nonlinmpc.jl

+6-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ struct NonLinMPC{
3636
B::Vector{NT}
3737
::Hermitian{NT, Matrix{NT}}
3838
::Vector{NT}
39-
p::Vector{NT}
39+
r::Vector{NT}
4040
Ks::Matrix{NT}
4141
Ps::Matrix{NT}
4242
d0::Vector{NT}
@@ -45,6 +45,7 @@ struct NonLinMPC{
4545
Uop::Vector{NT}
4646
Yop::Vector{NT}
4747
Dop::Vector{NT}
48+
buffer::PredictiveControllerBuffer{NT}
4849
function NonLinMPC{NT, SE, JM, JEFunc}(
4950
estim::SE, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt, Ewt, JE::JEFunc, optim::JM
5051
) where {NT<:Real, SE<:StateEstimator, JM<:JuMP.GenericModel, JEFunc<:Function}
@@ -68,13 +69,14 @@ struct NonLinMPC{
6869
)
6970
= init_quadprog(model, Ẽ, S̃, M_Hp, Ñ_Hc, L_Hp)
7071
# dummy vals (updated just before optimization):
71-
q̃, p = zeros(NT, size(H̃, 1)), zeros(NT, 1)
72+
q̃, r = zeros(NT, size(H̃, 1)), zeros(NT, 1)
7273
Ks, Ps = init_stochpred(estim, Hp)
7374
# dummy vals (updated just before optimization):
7475
d0, D̂0, D̂E = zeros(NT, nd), zeros(NT, nd*Hp), zeros(NT, nd + nd*Hp)
7576
Uop, Yop, Dop = repeat(model.uop, Hp), repeat(model.yop, Hp), repeat(model.dop, Hp)
7677
nΔŨ = size(Ẽ, 2)
7778
ΔŨ = zeros(NT, nΔŨ)
79+
buffer = PredictiveControllerBuffer{NT}(nu, ny, nd, Hp)
7880
mpc = new{NT, SE, JM, JEFunc}(
7981
estim, optim, con,
8082
ΔŨ, ŷ,
@@ -83,10 +85,11 @@ struct NonLinMPC{
8385
R̂u0, R̂y0, noR̂u,
8486
S̃, T, T_lastu0,
8587
Ẽ, F, G, J, K, V, B,
86-
H̃, q̃, p,
88+
H̃, q̃, r,
8789
Ks, Ps,
8890
d0, D̂0, D̂E,
8991
Uop, Yop, Dop,
92+
buffer
9093
)
9194
init_optimization!(mpc, model, optim)
9295
return mpc

src/estimator/execute.jl

+12-7
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ end
8787
Init `estim.x̂0` states from current inputs `u`, measured outputs `ym` and disturbances `d`.
8888
8989
The method tries to find a good steady-state for the initial estimate ``\mathbf{x̂}``. It
90-
removes the operating points with [`remove_op!`](@ref) and call [`init_estimate!`](@ref):
90+
stores `u - estim.model.uop` at `estim.lastu0` and removes the operating points with
91+
[`remove_op!`](@ref), and call [`init_estimate!`](@ref):
9192
9293
- If `estim.model` is a [`LinModel`](@ref), it finds the steady-state of the augmented model
9394
using `u` and `d` arguments, and uses the `ym` argument to enforce that
@@ -99,16 +100,14 @@ If applicable, it also sets the error covariance `estim.P̂` to `estim.P̂_0`.
99100
100101
# Examples
101102
```jldoctest
102-
julia> estim = SteadyKalmanFilter(LinModel(tf(3, [10, 1]), 0.5), nint_ym=[2]);
103+
julia> estim = SteadyKalmanFilter(LinModel(tf(3, [10, 1]), 0.5), nint_ym=[2], direct=false);
103104
104105
julia> u = [1]; y = [3 - 0.1]; x̂ = round.(initstate!(estim, u, y), digits=3)
105106
3-element Vector{Float64}:
106107
5.0
107108
0.0
108109
-0.1
109110
110-
julia> preparestate!(estim, y);
111-
112111
julia> x̂ ≈ updatestate!(estim, u, y)
113112
true
114113
@@ -171,19 +170,25 @@ init_estimate!(::StateEstimator, ::SimModel, _ , _ , _ ) = nothing
171170
172171
Evaluate `StateEstimator` outputs `ŷ` from `estim.x̂0` states and disturbances `d`.
173172
174-
It returns `estim` output at the current time step ``\mathbf{ŷ}(k)``. Calling a
175-
[`StateEstimator`](@ref) object calls this `evaloutput` method.
173+
It returns `estim` output at the current time step ``\mathbf{ŷ}(k)``. If `estim.direct` is
174+
`true`, the method [`preparestate!`](@ref) should be called beforehand to correct the state
175+
estimate.
176+
177+
Calling a [`StateEstimator`](@ref) object calls this `evaloutput` method.
176178
177179
# Examples
178180
```jldoctest
179-
julia> kf = SteadyKalmanFilter(setop!(LinModel(tf(2, [10, 1]), 5), yop=[20]));
181+
julia> kf = SteadyKalmanFilter(setop!(LinModel(tf(2, [10, 1]), 5), yop=[20]), direct=false);
180182
181183
julia> ŷ = evaloutput(kf)
182184
1-element Vector{Float64}:
183185
20.0
184186
```
185187
"""
186188
function evaloutput(estim::StateEstimator{NT}, d=estim.buffer.empty) where NT <: Real
189+
if estim.direct && !estim.corrected[]
190+
@warn "preparestate! should be called before evaloutput with current estimators"
191+
end
187192
validate_args(estim.model, d)
188193
ŷ0, d0 = estim.buffer.ŷ, estim.buffer.d
189194
d0 .= d .- estim.model.dop

0 commit comments

Comments
 (0)