Skip to content

Commit d10a731

Browse files
authored
Merge pull request #90 from JuliaControl/current_form
added: support for the current form for all the state estimators
2 parents 092cf30 + 15f270a commit d10a731

22 files changed

+837
-416
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
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.22.1"
4+
version = "0.23.0"
55

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

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ ModelPredictiveControl.remove_op!
5252
ModelPredictiveControl.init_estimate!
5353
```
5454

55+
## Correct Estimate
56+
57+
```@docs
58+
ModelPredictiveControl.correct_estimate!
59+
```
60+
5561
## Update Estimate
5662

5763
!!! info

docs/src/manual/linmpc.md

Lines changed: 21 additions & 15 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))
@@ -202,7 +203,7 @@ mpc_mhe = LinMPC(estim, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1])
202203
mpc_mhe = setconstraint!(mpc_mhe, ymin=[45, -Inf])
203204
```
204205

205-
The rejection is slightly improved:
206+
The rejection is not improved here:
206207

207208
```@example 1
208209
setstate!(model, zeros(model.nx))
@@ -215,6 +216,10 @@ savefig("plot3_LinMPC.svg"); nothing # hide
215216

216217
![plot3_LinMPC](plot3_LinMPC.svg)
217218

219+
This is because the more performant `direct=true` version of the [`MovingHorizonEstimator`](@ref)
220+
is not not implemented yet. The rejection will be improved with the `direct=true` version
221+
(coming soon).
222+
218223
## Adding Feedforward Compensation
219224

220225
Suppose that the load disturbance ``u_l`` of the last section is in fact caused by a
@@ -246,7 +251,7 @@ A [`LinMPC`](@ref) controller is constructed on this model:
246251

247252
```@example 1
248253
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])
254+
mpc_d = setconstraint!(mpc_d, ymin=[48, -Inf])
250255
```
251256

252257
A new test function that feeds the measured disturbance ``\mathbf{d}`` to the controller is
@@ -264,6 +269,7 @@ function test_mpc_d(mpc_d, model)
264269
i == 151 && (ul = -20)
265270
d = ul .+ dop # simulated measured disturbance
266271
y = model() # simulated measurements
272+
preparestate!(mpc_d, y, d) # prepare estimate with the measured disturbance d
267273
u = mpc_d(ry, d) # also feed the measured disturbance d to the controller
268274
u_data[:,i], y_data[:,i], ry_data[:,i] = u, y, ry
269275
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/construct.jl

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ The predictive controllers support both soft and hard constraints, defined by:
4444
\mathbf{u_{min} - c_{u_{min}}} ϵ ≤&&\ \mathbf{u}(k+j) &≤ \mathbf{u_{max} + c_{u_{max}}} ϵ &&\qquad j = 0, 1 ,..., H_p - 1 \\
4545
\mathbf{Δu_{min} - c_{Δu_{min}}} ϵ ≤&&\ \mathbf{Δu}(k+j) &≤ \mathbf{Δu_{max} + c_{Δu_{max}}} ϵ &&\qquad j = 0, 1 ,..., H_c - 1 \\
4646
\mathbf{y_{min} - c_{y_{min}}} ϵ ≤&&\ \mathbf{ŷ}(k+j) &≤ \mathbf{y_{max} + c_{y_{max}}} ϵ &&\qquad j = 1, 2 ,..., H_p \\
47-
\mathbf{x̂_{min} - c_{x̂_{min}}} ϵ ≤&&\ \mathbf{x̂}_{k-1}(k+j) &≤ \mathbf{x̂_{max} + c_{x̂_{max}}} ϵ &&\qquad j = H_p
47+
\mathbf{x̂_{min} - c_{x̂_{min}}} ϵ ≤&&\ \mathbf{x̂}_{i}(k+j) &≤ \mathbf{x̂_{max} + c_{x̂_{max}}} ϵ &&\qquad j = H_p
4848
\end{alignat*}
4949
```
5050
and also ``ϵ ≥ 0``. The last line is the terminal constraints applied on the states at the
@@ -94,8 +94,10 @@ LinMPC controller with a sample time Ts = 4.0 s, OSQP optimizer, SteadyKalmanFil
9494
Terminal constraints provide closed-loop stability guarantees on the nominal plant model.
9595
They can render an unfeasible problem however. In practice, a sufficiently large
9696
prediction horizon ``H_p`` without terminal constraints is typically enough for
97-
stability. Note that terminal constraints are applied on the augmented state vector
98-
``\mathbf{x̂}`` (see [`SteadyKalmanFilter`](@ref) for details on augmentation).
97+
stability. If `mpc.estim.direct==true`, the state is estimated at ``i = k`` (the current
98+
time step), otherwise at ``i = k - 1``. Note that terminal constraints are applied on the
99+
augmented state vector ``\mathbf{x̂}`` (see [`SteadyKalmanFilter`](@ref) for details on
100+
augmentation).
99101
100102
For variable constraints, the bounds can be modified after calling [`moveinput!`](@ref),
101103
that is, at runtime, but not the softness parameters ``\mathbf{c}``. It is not possible
@@ -433,16 +435,16 @@ The model predictions are evaluated from the deviation vectors (see [`setop!`](@
433435
&= \mathbf{E ΔU} + \mathbf{F}
434436
\end{aligned}
435437
```
436-
in which ``\mathbf{x̂_0}(k) = \mathbf{x̂}_{k-1}(k) - \mathbf{x̂_{op}}`` and
437-
``\mathbf{x̂}_{k-1}(k)`` is the state estimated at the last control period. The predicted
438-
outputs ``\mathbf{Ŷ_0}`` and measured disturbances ``\mathbf{D̂_0}`` respectively include
439-
``\mathbf{ŷ_0}(k+j)`` and ``\mathbf{d̂_0}(k+j)`` values with ``j=1`` to ``H_p``, and input
440-
increments ``\mathbf{ΔU}``, ``\mathbf{Δu}(k+j)`` from ``j=0`` to ``H_c-1``. The vector
441-
``\mathbf{B}`` contains the contribution for non-zero state ``\mathbf{x̂_{op}}`` and state
442-
update ``\mathbf{f̂_{op}}`` operating points (for linearization at non-equilibrium point, see
443-
[`linearize`](@ref)). The stochastic predictions ``\mathbf{Ŷ_s=0}`` if `estim` is not a
444-
[`InternalModel`](@ref), see [`init_stochpred`](@ref). The method also computes similar
445-
matrices for the predicted terminal states at ``k+H_p``:
438+
in which ``\mathbf{x̂_0}(k) = \mathbf{x̂}_{i}(k) - \mathbf{x̂_{op}}``, with ``i = k`` if
439+
`estim.direct==true`, otherwise ``i = k - 1``. The predicted outputs ``\mathbf{Ŷ_0}`` and
440+
measured disturbances ``\mathbf{D̂_0}`` respectively include ``\mathbf{ŷ_0}(k+j)`` and
441+
``\mathbf{d̂_0}(k+j)`` values with ``j=1`` to ``H_p``, and input increments ``\mathbf{ΔU}``,
442+
``\mathbf{Δu}(k+j)`` from ``j=0`` to ``H_c-1``. The vector ``\mathbf{B}`` contains the
443+
contribution for non-zero state ``\mathbf{x̂_{op}}`` and state update ``\mathbf{f̂_{op}}``
444+
operating points (for linearization at non-equilibrium point, see [`linearize`](@ref)). The
445+
stochastic predictions ``\mathbf{Ŷ_s=0}`` if `estim` is not a [`InternalModel`](@ref), see
446+
[`init_stochpred`](@ref). The method also computes similar matrices for the predicted
447+
terminal states at ``k+H_p``:
446448
```math
447449
\begin{aligned}
448450
\mathbf{x̂_0}(k+H_p) &= \mathbf{e_x̂ ΔU} + \mathbf{g_x̂ d_0}(k) + \mathbf{j_x̂ D̂_0}

0 commit comments

Comments
 (0)