Skip to content

Commit a3f2d5d

Browse files
authored
Merge 67d924d into 092cf30
2 parents 092cf30 + 67d924d commit a3f2d5d

20 files changed

+608
-337
lines changed

docs/src/internals/predictive_control.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ ModelPredictiveControl.init_matconstraint_mpc
2626
## Update Quadratic Optimization
2727

2828
```@docs
29-
ModelPredictiveControl.initpred!(::PredictiveController, ::LinModel, ::Any, ::Any, ::Any, ::Any, ::Any)
29+
ModelPredictiveControl.initpred!(::PredictiveController, ::LinModel, ::Any, ::Any, ::Any, ::Any)
3030
ModelPredictiveControl.linconstraint!(::PredictiveController, ::LinModel)
3131
```
3232

docs/src/internals/state_estim.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ ModelPredictiveControl.remove_op!
5252
ModelPredictiveControl.init_estimate!
5353
```
5454

55-
## Update Estimate
55+
## Correct and Update Estimate
5656

5757
!!! info
5858
All these methods assume that the `u0`, `y0m` and `d0` arguments are deviation vectors
@@ -63,5 +63,6 @@ ModelPredictiveControl.init_estimate!
6363
``\mathbf{x̂_0}``, respectively.
6464

6565
```@docs
66+
ModelPredictiveControl.correct_estimate!
6667
ModelPredictiveControl.update_estimate!
6768
```

docs/src/manual/linmpc.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,18 @@ plant simulator to test the design. Its sampling time is 2 s thus the control pe
6262
## Linear Model Predictive Controller
6363

6464
A linear model predictive controller (MPC) will control both the water level ``y_L`` and
65-
temperature ``y_T`` in the tank. The tank level should also never fall below 45:
65+
temperature ``y_T`` in the tank. The tank level should also never fall below 48:
6666

6767
```math
68-
y_L ≥ 45
68+
y_L ≥ 48
6969
```
7070

7171
We design our [`LinMPC`](@ref) controllers by including the linear level constraint with
7272
[`setconstraint!`](@ref) (`±Inf` values should be used when there is no bound):
7373

7474
```@example 1
7575
mpc = LinMPC(model, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1])
76-
mpc = setconstraint!(mpc, ymin=[45, -Inf])
76+
mpc = setconstraint!(mpc, ymin=[48, -Inf])
7777
```
7878

7979
in which `Hp` and `Hc` keyword arguments are respectively the predictive and control
@@ -116,10 +116,11 @@ function test_mpc(mpc, model)
116116
i == 101 && (ry = [54, 30])
117117
i == 151 && (ul = -20)
118118
y = model() # simulated measurements
119+
preparestate!(mpc, y) # prepare mpc state estimate for current iteration
119120
u = mpc(ry) # or equivalently : u = moveinput!(mpc, ry)
120121
u_data[:,i], y_data[:,i], ry_data[:,i] = u, y, ry
121-
updatestate!(mpc, u, y) # update mpc state estimate
122-
updatestate!(model, u + [0; ul]) # update simulator with the load disturbance
122+
updatestate!(mpc, u, y) # update mpc state estimate for next iteration
123+
updatestate!(model, u + [0; ul]) # update simulator with load disturbance
123124
end
124125
return u_data, y_data, ry_data
125126
end
@@ -131,10 +132,10 @@ nothing # hide
131132
The [`LinMPC`](@ref) objects are also callable as an alternative syntax for
132133
[`moveinput!`](@ref). It is worth mentioning that additional information like the optimal
133134
output predictions ``\mathbf{Ŷ}`` can be retrieved by calling [`getinfo`](@ref) after
134-
solving the problem. Also, calling [`updatestate!`](@ref) on the `mpc` object updates its
135-
internal state for the *NEXT* control period (this is by design, see
136-
[Functions: State Estimators](@ref) for justifications). That is why the call is done at the
137-
end of the `for` loop. The same logic applies for `model`.
135+
solving the problem. Also, calling [`preparestate!`](@ref) on the `mpc` object prepares the
136+
estimates for the current control period, and [`updatestate!`](@ref) updates them for the
137+
next one (the same logic applies for `model`). This is why [`preparestate!`](@ref) is called
138+
before the controller, and [`updatestate!`](@ref), after.
138139

139140
Lastly, we plot the closed-loop test with the `Plots` package:
140141

@@ -143,7 +144,7 @@ using Plots
143144
function plot_data(t_data, u_data, y_data, ry_data)
144145
p1 = plot(t_data, y_data[1,:], label="meas.", ylabel="level")
145146
plot!(p1, t_data, ry_data[1,:], label="setpoint", linestyle=:dash, linetype=:steppost)
146-
plot!(p1, t_data, fill(45,size(t_data)), label="min", linestyle=:dot, linewidth=1.5)
147+
plot!(p1, t_data, fill(48,size(t_data)), label="min", linestyle=:dot, linewidth=1.5)
147148
p2 = plot(t_data, y_data[2,:], label="meas.", legend=:topleft, ylabel="temp.")
148149
plot!(p2, t_data, ry_data[2,:],label="setpoint", linestyle=:dash, linetype=:steppost)
149150
p3 = plot(t_data,u_data[1,:],label="cold", linetype=:steppost, ylabel="flow rate")
@@ -162,11 +163,11 @@ real-life control problems. Constructing a [`LinMPC`](@ref) with input integrato
162163

163164
```@example 1
164165
mpc2 = LinMPC(model, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1], nint_u=[1, 1])
165-
mpc2 = setconstraint!(mpc2, ymin=[45, -Inf])
166+
mpc2 = setconstraint!(mpc2, ymin=[48, -Inf])
166167
```
167168

168-
does accelerate the rejection of the load disturbance and eliminates the level constraint
169-
violation:
169+
does accelerate the rejection of the load disturbance and almost eliminates the level
170+
constraint violation:
170171

171172
```@example 1
172173
setstate!(model, zeros(model.nx))
@@ -246,7 +247,7 @@ A [`LinMPC`](@ref) controller is constructed on this model:
246247

247248
```@example 1
248249
mpc_d = LinMPC(model_d, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1])
249-
mpc_d = setconstraint!(mpc_d, ymin=[45, -Inf])
250+
mpc_d = setconstraint!(mpc_d, ymin=[48, -Inf])
250251
```
251252

252253
A new test function that feeds the measured disturbance ``\mathbf{d}`` to the controller is
@@ -264,6 +265,7 @@ function test_mpc_d(mpc_d, model)
264265
i == 151 && (ul = -20)
265266
d = ul .+ dop # simulated measured disturbance
266267
y = model() # simulated measurements
268+
preparestate!(mpc_d, y, d) # prepare estimate with the measured disturbance d
267269
u = mpc_d(ry, d) # also feed the measured disturbance d to the controller
268270
u_data[:,i], y_data[:,i], ry_data[:,i] = u, y, ry
269271
updatestate!(mpc_d, u, y, d) # update estimate with the measured disturbance d

docs/src/manual/nonlinmpc.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ method.
8989
An [`UnscentedKalmanFilter`](@ref) estimates the plant state :
9090

9191
```@example 1
92-
α=0.01; σQ=[0.1, 0.5]; σR=[0.5]; nint_u=[1]; σQint_u=[0.1]
92+
α=0.01; σQ=[0.1, 1.0]; σR=[5.0]; nint_u=[1]; σQint_u=[0.1]
9393
estim = UnscentedKalmanFilter(model; α, σQ, σR, nint_u, σQint_u)
9494
```
9595

96-
The vectors `σQ` and σR `σR` are the standard deviations of the process and sensor noises,
96+
The vectors `σQ` and `σR` are the standard deviations of the process and sensor noises,
9797
respectively. The value for the velocity ``ω`` is higher here (`σQ` second value) since
9898
``\dot{ω}(t)`` equation includes an uncertain parameter: the friction coefficient ``K``.
9999
Also, the argument `nint_u` explicitly adds one integrating state at the model input, the
@@ -237,7 +237,8 @@ savefig("plot6_NonLinMPC.svg"); nothing # hide
237237

238238
![plot6_NonLinMPC](plot6_NonLinMPC.svg)
239239

240-
the new controller is able to recuperate more energy from the pendulum (i.e. negative work):
240+
the new controller is able to recuperate a little more energy from the pendulum (i.e.
241+
negative work):
241242

242243
```@example 1
243244
Dict(:W_nmpc => calcW(res_yd), :W_empc => calcW(res2_yd))
@@ -262,12 +263,13 @@ linmodel = linearize(model, x=[π, 0], u=[0])
262263
A [`SteadyKalmanFilter`](@ref) and a [`LinMPC`](@ref) are designed from `linmodel`:
263264

264265
```@example 1
266+
265267
skf = SteadyKalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
266268
mpc = LinMPC(skf; Hp, Hc, Mwt, Nwt, Cwt=Inf)
267269
mpc = setconstraint!(mpc, umin=[-1.5], umax=[+1.5])
268270
```
269271

270-
The linear controller has difficulties to reject the 10° step disturbance:
272+
The linear controller satisfactorily rejects the 10° step disturbance:
271273

272274
```@example 1
273275
res_lin = sim!(mpc, N, [180.0]; plant, x_0=[π, 0], y_step=[10])
@@ -278,9 +280,10 @@ savefig("plot7_NonLinMPC.svg"); nothing # hide
278280
![plot7_NonLinMPC](plot7_NonLinMPC.svg)
279281

280282
Solving the optimization problem of `mpc` with [`DAQP`](https://darnstrom.github.io/daqp/)
281-
optimizer instead of the default `OSQP` solver can help here. It is indeed documented that
282-
`DAQP` can perform better on small/medium dense matrices and unstable poles[^1], which is
283-
obviously the case here (absolute value of unstable poles are greater than one):
283+
optimizer instead of the default `OSQP` solver can improve the performance here. It is
284+
indeed documented that `DAQP` can perform better on small/medium dense matrices and unstable
285+
poles[^1], which is obviously the case here (absolute value of unstable poles are greater
286+
than one):
284287

285288
[^1]: Arnström, D., Bemporad, A., and Axehill, D. (2022). A dual active-set solver for
286289
embedded quadratic programming using recursive LDLᵀ updates. IEEE Trans. Autom. Contr.,
@@ -305,7 +308,7 @@ mpc2 = LinMPC(skf; Hp, Hc, Mwt, Nwt, Cwt=Inf, optim=daqp)
305308
mpc2 = setconstraint!(mpc2; umin, umax)
306309
```
307310

308-
does improve the rejection of the step disturbance:
311+
does slightly improve the rejection of the step disturbance:
309312

310313
```@example 1
311314
res_lin2 = sim!(mpc2, N, [180.0]; plant, x_0=[π, 0], y_step=[10])
@@ -359,12 +362,13 @@ function sim_adapt!(mpc, nonlinmodel, N, ry, plant, x_0, x̂_0, y_step=[0])
359362
x̂ = x̂_0
360363
for i = 1:N
361364
y = plant() + y_step
365+
x̂ = preparestate!(mpc, y)
362366
u = moveinput!(mpc, ry)
363367
linmodel = linearize(nonlinmodel; u, x=x̂[1:2])
364368
setmodel!(mpc, linmodel)
365369
U_data[:,i], Y_data[:,i], Ry_data[:,i] = u, y, ry
366-
x̂ = updatestate!(mpc, u, y) # update mpc state estimate
367-
updatestate!(plant, u) # update plant simulator
370+
updatestate!(mpc, u, y) # update mpc state estimate
371+
updatestate!(plant, u) # update plant simulator
368372
end
369373
res = SimResult(mpc, U_data, Y_data; Ry_data)
370374
return res

docs/src/public/generic_func.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ setconstraint!
1919
evaloutput
2020
```
2121

22+
## Prepare State x
23+
24+
```@docs
25+
preparestate!
26+
```
27+
2228
## Update State x
2329

2430
```@docs

docs/src/public/state_estim.md

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,15 @@ error with closed-loop control (offset-free tracking).
1717
integral action is not necessarily desired. The options `nint_u=0` and `nint_ym=0`
1818
disable it.
1919

20-
The estimators are all implemented in the predictor form (a.k.a. observer form), that is,
21-
they all estimates at each discrete time ``k`` the states of the next period
22-
``\mathbf{x̂}_k(k+1)``[^1]. In contrast, the filter form that estimates ``\mathbf{x̂}_k(k)``
23-
is sometimes slightly more accurate.
24-
25-
[^1]: also denoted ``\mathbf{x̂}_{k+1|k}`` [elsewhere](https://en.wikipedia.org/wiki/Kalman_filter).
26-
27-
The predictor form comes in handy for control applications since the estimations come after
28-
the controller computations, without introducing any additional delays. Moreover, the
29-
[`moveinput!`](@ref) method of the predictive controllers does not automatically update the
30-
estimates with [`updatestate!`](@ref). This allows applying the calculated inputs on the
31-
real plant before starting the potentially expensive estimator computations (see
32-
[Manual](@ref man_lin) for examples).
20+
The estimators are all implemented in the current form (a.k.a. as filter form) by default
21+
to improve accuracy and robustness, that is, they all estimates at each discrete time ``k``
22+
the states of the current period ``\mathbf{x̂}_k(k)``[^1] (using the newest measurements, see
23+
[Manual](@ref man_lin) for examples). The predictor form (a.k.a. delayed form) is also
24+
available with the option `direct=false`. This allow moving the estimator computations after
25+
solving the MPC problem with [`moveinput!`](@ref), for when the estimations are expensive
26+
(for instance, with the [`MovingHorizonEstimator`](@ref)).
27+
28+
[^1]: also denoted ``\mathbf{x̂}_{k|k}`` [elsewhere](https://en.wikipedia.org/wiki/Kalman_filter).
3329

3430
!!! info
3531
All the estimators support measured ``\mathbf{y^m}`` and unmeasured ``\mathbf{y^u}``

src/ModelPredictiveControl.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import OSQP, Ipopt
2525
export SimModel, LinModel, NonLinModel
2626
export DiffSolver, RungeKutta
2727
export setop!, setname!
28-
export setstate!, setmodel!, updatestate!, evaloutput, linearize, linearize!
28+
export setstate!, setmodel!, preparestate!, updatestate!, evaloutput, linearize, linearize!
2929
export StateEstimator, InternalModel, Luenberger
3030
export SteadyKalmanFilter, KalmanFilter, UnscentedKalmanFilter, ExtendedKalmanFilter
3131
export MovingHorizonEstimator

src/controller/execute.jl

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ See also [`LinMPC`](@ref), [`ExplicitMPC`](@ref), [`NonLinMPC`](@ref).
3636
in the future by default or ``\mathbf{r̂_y}(k+j)=\mathbf{r_y}(k)`` for ``j=1`` to ``H_p``.
3737
- `R̂u=mpc.Uop` or *`Rhatu`* : predicted manipulated input setpoints, constant in the future
3838
by default or ``\mathbf{r̂_u}(k+j)=\mathbf{u_{op}}`` for ``j=0`` to ``H_p-1``.
39-
- `ym=nothing` : current measured outputs ``\mathbf{y^m}(k)``, only required if `mpc.estim`
40-
is an [`InternalModel`](@ref).
4139
4240
# Examples
4341
```jldoctest
@@ -55,13 +53,12 @@ function moveinput!(
5553
Dhat ::Vector = repeat(d, mpc.Hp),
5654
Rhaty::Vector = repeat(ry, mpc.Hp),
5755
Rhatu::Vector = mpc.Uop,
58-
ym::Union{Vector, Nothing} = nothing,
5956
= Dhat,
6057
R̂y = Rhaty,
6158
R̂u = Rhatu
6259
)
6360
validate_args(mpc, ry, d, D̂, R̂y, R̂u)
64-
initpred!(mpc, mpc.estim.model, d, ym, D̂, R̂y, R̂u)
61+
initpred!(mpc, mpc.estim.model, d, D̂, R̂y, R̂u)
6562
linconstraint!(mpc, mpc.estim.model)
6663
ΔŨ = optim_objective!(mpc)
6764
Δu = ΔŨ[1:mpc.estim.model.nu] # receding horizon principle: only Δu(k) is used (1st one)
@@ -122,7 +119,7 @@ function getinfo(mpc::PredictiveController{NT}) where NT<:Real
122119
U0 = mpc.*mpc.ΔŨ + mpc.T_lastu0
123120
J = obj_nonlinprog!(U0, Ȳ, Ū, mpc, model, Ŷ0, mpc.ΔŨ)
124121
oldF = copy(mpc.F)
125-
predictstoch!(mpc, mpc.estim, mpc.d0 + model.dop, mpc.ŷ[mpc.estim.i_ym])
122+
predictstoch!(mpc, mpc.estim)
126123
Ŷs .= mpc.F # predictstoch! init mpc.F with Ŷs value if estim is an InternalModel
127124
mpc.F .= oldF # restore old F value
128125
info[:ΔU] = mpc.ΔŨ[1:mpc.Hc*model.nu]
@@ -164,7 +161,7 @@ end
164161

165162

166163
@doc raw"""
167-
initpred!(mpc::PredictiveController, model::LinModel, d, ym, D̂, R̂y, R̂u) -> nothing
164+
initpred!(mpc::PredictiveController, model::LinModel, d, D̂, R̂y, R̂u) -> nothing
168165
169166
Init linear model prediction matrices `F, q̃, p` and current estimated output `ŷ`.
170167
@@ -183,11 +180,11 @@ They are computed with these equations using in-place operations:
183180
\end{aligned}
184181
```
185182
"""
186-
function initpred!(mpc::PredictiveController, model::LinModel, d, ym, D̂, R̂y, R̂u)
183+
function initpred!(mpc::PredictiveController, model::LinModel, d, D̂, R̂y, R̂u)
187184
mul!(mpc.T_lastu0, mpc.T, mpc.estim.lastu0)
188185
ŷ, F, q̃, p = mpc.ŷ, mpc.F, mpc.q̃, mpc.p
189-
ŷ .= evalŷ(mpc.estim, ym, d)
190-
predictstoch!(mpc, mpc.estim, d, ym) # init mpc.F with Ŷs for InternalModel
186+
ŷ .= evalŷ(mpc.estim, d)
187+
predictstoch!(mpc, mpc.estim) # init mpc.F with Ŷs for InternalModel
191188
F .+= mpc.B
192189
mul!(F, mpc.K, mpc.estim.x̂0, 1, 1)
193190
mul!(F, mpc.V, mpc.estim.lastu0, 1, 1)
@@ -216,14 +213,14 @@ function initpred!(mpc::PredictiveController, model::LinModel, d, ym, D̂, R̂y,
216213
end
217214

218215
@doc raw"""
219-
initpred!(mpc::PredictiveController, model::SimModel, d, ym, D̂, R̂y, R̂u)
216+
initpred!(mpc::PredictiveController, model::SimModel, d, D̂, R̂y, R̂u)
220217
221218
Init `ŷ, F, d0, D̂0, D̂E, R̂y0, R̂u0` vectors when model is not a [`LinModel`](@ref).
222219
"""
223-
function initpred!(mpc::PredictiveController, model::SimModel, d, ym, D̂, R̂y, R̂u)
220+
function initpred!(mpc::PredictiveController, model::SimModel, d, D̂, R̂y, R̂u)
224221
mul!(mpc.T_lastu0, mpc.T, mpc.estim.lastu0)
225-
mpc.ŷ .= evalŷ(mpc.estim, ym, d)
226-
predictstoch!(mpc, mpc.estim, d, ym) # init mpc.F with Ŷs for InternalModel
222+
mpc.ŷ .= evalŷ(mpc.estim, d)
223+
predictstoch!(mpc, mpc.estim) # init mpc.F with Ŷs for InternalModel
227224
if model.nd 0
228225
mpc.d0 .= d .- model.dop
229226
mpc.D̂0 .=.- mpc.Dop
@@ -238,28 +235,23 @@ function initpred!(mpc::PredictiveController, model::SimModel, d, ym, D̂, R̂y,
238235
end
239236

240237
@doc raw"""
241-
predictstoch!(mpc::PredictiveController, estim::InternalModel, x̂s, d, ym)
238+
predictstoch!(mpc::PredictiveController, estim::InternalModel)
242239
243240
Init `mpc.F` vector with ``\mathbf{F = Ŷ_s}`` when `estim` is an [`InternalModel`](@ref).
244241
"""
245-
function predictstoch!(
246-
mpc::PredictiveController{NT}, estim::InternalModel, d, ym
247-
) where {NT<:Real}
248-
isnothing(ym) && error("Predictive controllers with InternalModel need the measured "*
249-
"outputs ym in keyword argument to compute control actions u")
242+
function predictstoch!(mpc::PredictiveController{NT}, estim::InternalModel) where {NT<:Real}
250243
F, ny = mpc.F, estim.model.ny
251-
ŷd = similar(estim.model.yop)
252-
h!(ŷd, estim.model, estim.x̂d, d - estim.model.dop)
253-
ŷd .+= estim.model.yop
254-
ŷs = zeros(NT, estim.model.ny)
255-
ŷs[estim.i_ym] .= @views ym .- ŷd[estim.i_ym] # ŷs=0 for unmeasured outputs
244+
ŷ0d = similar(estim.model.yop)
245+
h!(ŷ0d, estim.model, estim.x̂d, estim.d0)
246+
ŷs = zeros(NT, ny)
247+
ŷs[estim.i_ym] .= @views estim.y0m .- ŷ0d[estim.i_ym] # ŷs=0 for unmeasured outputs
256248
Ŷs = F
257249
mul!(Ŷs, mpc.Ks, estim.x̂s)
258250
mul!(Ŷs, mpc.Ps, ŷs, 1, 1)
259251
return nothing
260252
end
261253
"Separate stochastic predictions are not needed if `estim` is not [`InternalModel`](@ref)."
262-
predictstoch!(mpc::PredictiveController, ::StateEstimator, _ , _ ) = (mpc.F .= 0; nothing)
254+
predictstoch!(mpc::PredictiveController, ::StateEstimator) = (mpc.F .= 0; nothing)
263255

264256
@doc raw"""
265257
linconstraint!(mpc::PredictiveController, model::LinModel)
@@ -505,7 +497,16 @@ end
505497
set_objective_linear_coef!(::PredictiveController, _ ) = nothing
506498

507499
"""
508-
updatestate!(mpc::PredictiveController, u, ym, d=[]) -> x̂
500+
preparestate!(mpc::PredictiveController, ym, d=[]) -> x̂
501+
502+
Call [`preparestate!`](@ref) on `mpc.estim` [`StateEstimator`](@ref).
503+
"""
504+
function preparestate!(mpc::PredictiveController, ym, d=mpc.estim.buffer.empty)
505+
return preparestate!(mpc.estim, ym, d)
506+
end
507+
508+
"""
509+
updatestate!(mpc::PredictiveController, u, ym, d=[]) -> x̂next
509510
510511
Call [`updatestate!`](@ref) on `mpc.estim` [`StateEstimator`](@ref).
511512
"""
@@ -523,7 +524,7 @@ setstate!(mpc::PredictiveController, x̂) = (setstate!(mpc.estim, x̂); return m
523524

524525

525526
@doc raw"""
526-
setmodel!(mpc::PredictiveController, model=mpc.estim.model, <keyword arguments>) -> mpc
527+
setmodel!(mpc::PredictiveController, model=mpc.estim.model; <keyword arguments>) -> mpc
527528
528529
Set `model` and objective function weights of `mpc` [`PredictiveController`](@ref).
529530

0 commit comments

Comments
 (0)