Skip to content

Commit d3e0a2f

Browse files
authored
Merge pull request #54 from JuliaControl/adapt_linmpc
Added: adaptation of controller/estimator based on `LinModel`
2 parents 58e355e + de99b51 commit d3e0a2f

29 files changed

+1501
-845
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.20.2"
4+
version = "0.21.0"
55

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

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ for more detailed examples.
8585
- [x] input setpoint tracking
8686
- [x] terminal costs
8787
- [x] economic costs (economic model predictive control)
88+
- [ ] adaptive linear model predictive controller
89+
- [x] manual model modification
90+
- [x] automatic successive linearization of a nonlinear model
91+
- [ ] objective function weights modification
8892
- [x] explicit predictive controller for problems without constraint
89-
- [x] soft and hard constraints on:
93+
- [x] online-tunable soft and hard constraints on:
9094
- [x] output predictions
9195
- [x] manipulated inputs
9296
- [x] manipulated inputs increments
@@ -126,7 +130,7 @@ for more detailed examples.
126130
- [x] moving horizon estimator in two formulations:
127131
- [x] linear plant models (quadratic optimization)
128132
- [x] nonlinear plant models (nonlinear optimization)
129-
- [x] moving horizon estimator soft and hard constraints on:
133+
- [x] moving horizon estimator online-tunable soft and hard constraints on:
130134
- [x] state estimates
131135
- [x] process noise estimates
132136
- [x] sensor noise estimates

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ makedocs(
4949
deploydocs(
5050
repo = "github.com/JuliaControl/ModelPredictiveControl.jl.git",
5151
devbranch = "main",
52+
push_preview = true
5253
)

docs/src/internals/state_estim.md

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ ModelPredictiveControl.f̂!
1111
ModelPredictiveControl.ĥ!
1212
```
1313

14-
## Constraint Relaxation
15-
16-
```@docs
17-
18-
```
19-
2014
## Estimator Construction
2115

2216
```@docs
@@ -52,6 +46,12 @@ ModelPredictiveControl.evalŷ
5246
ModelPredictiveControl.remove_op!
5347
```
5448

49+
## Init Estimate
50+
51+
```@docs
52+
ModelPredictiveControl.init_estimate!
53+
```
54+
5555
## Update Estimate
5656

5757
!!! info
@@ -62,12 +62,3 @@ ModelPredictiveControl.remove_op!
6262
```@docs
6363
ModelPredictiveControl.update_estimate!
6464
```
65-
66-
## Init Estimate
67-
68-
!!! info
69-
Same as above: the arguments should be called `u0`, `ym0` and `d0`, strickly speaking.
70-
71-
```@docs
72-
ModelPredictiveControl.init_estimate!
73-
```

docs/src/manual/nonlinmpc.md

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,19 @@ As the motor torque is limited to -1.5 to 1.5 N m, we incorporate the input cons
108108
a [`NonLinMPC`](@ref):
109109

110110
```@example 1
111-
nmpc = NonLinMPC(estim, Hp=20, Hc=2, Mwt=[0.5], Nwt=[2.5], Cwt=Inf)
112-
nmpc = setconstraint!(nmpc, umin=[-1.5], umax=[+1.5])
111+
Hp, Hc, Mwt, Nwt = 20, 2, [0.5], [2.5]
112+
nmpc = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt=Inf)
113+
umin, umax = [-1.5], [+1.5]
114+
nmpc = setconstraint!(nmpc; umin, umax)
113115
```
114116

115-
The option `Cwt=Inf` disables the slack variable `ϵ` for constraint softening. We test `mpc` performance on `plant` by imposing an angular setpoint of 180° (inverted position):
117+
The option `Cwt=Inf` disables the slack variable `ϵ` for constraint softening. We test `mpc`
118+
performance on `plant` by imposing an angular setpoint of 180° (inverted position):
116119

117120
```@example 1
118121
using Logging; disable_logging(Warn) # hide
119122
using JuMP; unset_time_limit_sec(nmpc.optim) # hide
120-
res_ry = sim!(nmpc, N, [180.0], plant=plant, x0=[0, 0], x̂0=[0, 0, 0])
123+
res_ry = sim!(nmpc, N, [180.0], plant=plant, x_0=[0, 0], x̂_0=[0, 0, 0])
121124
plot(res_ry)
122125
savefig(ans, "plot3_NonLinMPC.svg"); nothing # hide
123126
```
@@ -129,7 +132,7 @@ inverted position, the closed-loop response to a step disturbances of 10° is al
129132
satisfactory:
130133

131134
```@example 1
132-
res_yd = sim!(nmpc, N, [180.0], plant=plant, x0=[π, 0], x̂0=[π, 0, 0], y_step=[10])
135+
res_yd = sim!(nmpc, N, [180.0], plant=plant, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10])
133136
plot(res_yd)
134137
savefig(ans, "plot4_NonLinMPC.svg"); nothing # hide
135138
```
@@ -184,8 +187,8 @@ function JE(UE, ŶE, _ )
184187
τ, ω = UE[1:end-1], ŶE[2:2:end-1]
185188
return Ts*sum(τ.*ω)
186189
end
187-
empc = NonLinMPC(estim2, Hp=20, Hc=2, Mwt=[0.5, 0], Nwt=[2.5], Cwt=Inf, Ewt=3.5e3, JE=JE)
188-
empc = setconstraint!(empc, umin=[-1.5], umax=[+1.5])
190+
empc = NonLinMPC(estim2; Hp, Hc, Nwt, Mwt=[0.5, 0], Cwt=Inf, Ewt=3.5e3, JE=JE)
191+
empc = setconstraint!(empc; umin, umax)
189192
```
190193

191194
The keyword argument `Ewt` weights the economic costs relative to the other terms in the
@@ -196,7 +199,7 @@ setpoint is similar:
196199

197200
```@example 1
198201
unset_time_limit_sec(empc.optim) # hide
199-
res2_ry = sim!(empc, N, [180, 0], plant=plant2, x0=[0, 0], x̂0=[0, 0, 0])
202+
res2_ry = sim!(empc, N, [180, 0], plant=plant2, x_0=[0, 0], x̂_0=[0, 0, 0])
200203
plot(res2_ry)
201204
savefig(ans, "plot5_NonLinMPC.svg"); nothing # hide
202205
```
@@ -216,7 +219,7 @@ Dict(:W_nmpc => calcW(res_ry), :W_empc => calcW(res2_ry))
216219
Also, for a 10° step disturbance:
217220

218221
```@example 1
219-
res2_yd = sim!(empc, N, [180; 0]; plant=plant2, x0=[π, 0], x̂0=[π, 0, 0], y_step=[10, 0])
222+
res2_yd = sim!(empc, N, [180; 0]; plant=plant2, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10, 0])
220223
plot(res2_yd)
221224
savefig(ans, "plot6_NonLinMPC.svg"); nothing # hide
222225
```
@@ -232,7 +235,7 @@ Dict(:W_nmpc => calcW(res_yd), :W_empc => calcW(res2_yd))
232235
Of course, this gain is only exploitable if the motor electronic includes some kind of
233236
regenerative circuitry.
234237

235-
## Linearizing the Model
238+
## Model Linearization
236239

237240
Nonlinear MPC is more computationally expensive than [`LinMPC`](@ref). Solving the problem
238241
should always be faster than the sampling time ``T_s = 0.1`` s for real-time operation. This
@@ -248,15 +251,15 @@ linmodel = linearize(model, x=[π, 0], u=[0])
248251
A [`SteadyKalmanFilter`](@ref) and a [`LinMPC`](@ref) are designed from `linmodel`:
249252

250253
```@example 1
251-
kf = SteadyKalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
252-
mpc = LinMPC(kf, Hp=20, Hc=2, Mwt=[0.5], Nwt=[2.5], Cwt=Inf)
254+
skf = SteadyKalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
255+
mpc = LinMPC(skf; Hp, Hc, Mwt, Nwt, Cwt=Inf)
253256
mpc = setconstraint!(mpc, umin=[-1.5], umax=[+1.5])
254257
```
255258

256259
The linear controller has difficulties to reject the 10° step disturbance:
257260

258261
```@example 1
259-
res_lin = sim!(mpc, N, [180.0]; plant, x0=[π, 0], y_step=[10])
262+
res_lin = sim!(mpc, N, [180.0]; plant, x_0=[π, 0], y_step=[10])
260263
plot(res_lin)
261264
savefig(ans, "plot7_NonLinMPC.svg"); nothing # hide
262265
```
@@ -287,22 +290,98 @@ Constructing a [`LinMPC`](@ref) with `DAQP`:
287290
```@example 1
288291
using JuMP, DAQP
289292
daqp = Model(DAQP.Optimizer, add_bridges=false)
290-
mpc2 = LinMPC(kf, Hp=20, Hc=2, Mwt=[0.5], Nwt=[2.5], Cwt=Inf, optim=daqp)
291-
mpc2 = setconstraint!(mpc2, umin=[-1.5], umax=[+1.5])
293+
mpc2 = LinMPC(skf; Hp, Hc, Mwt, Nwt, Cwt=Inf, optim=daqp)
294+
mpc2 = setconstraint!(mpc2; umin, umax)
292295
```
293296

294297
does improve the rejection of the step disturbance:
295298

296299
```@example 1
297-
res_lin2 = sim!(mpc2, N, [180.0]; plant, x0=[π, 0], y_step=[10])
300+
res_lin2 = sim!(mpc2, N, [180.0]; plant, x_0=[π, 0], y_step=[10])
298301
plot(res_lin2)
299302
savefig(ans, "plot8_NonLinMPC.svg"); nothing # hide
300303
```
301304

302305
![plot8_NonLinMPC](plot8_NonLinMPC.svg)
303306

304307
The closed-loop performance is still lower than the nonlinear controller, as expected, but
305-
computations are about 2000 times faster (0.00002 s versus 0.04 s per time steps, on
306-
average). Note that `linmodel` is only valid for angular positions near 180°. Multiple
307-
linearized models and controllers are required for large deviations from this operating
308-
point. This is known as gain scheduling.
308+
computations are about 210 times faster (0.000071 s versus 0.015 s per time steps, on
309+
average). However, remember that `linmodel` is only valid for angular positions near 180°.
310+
For example, the 180° setpoint response from 0° is unsatisfactory since the predictions are
311+
poor in the first quadrant:
312+
313+
```@example 1
314+
res_lin3 = sim!(mpc2, N, [180.0]; plant, x_0=[0, 0])
315+
plot(res_lin3)
316+
savefig(ans, "plot9_NonLinMPC.svg"); nothing # hide
317+
```
318+
319+
![plot9_NonLinMPC](plot9_NonLinMPC.svg)
320+
321+
Multiple linearized model and controller objects are required for large deviations from this
322+
operating point. This is known as gain scheduling. Another approach is adapting the model of
323+
the [`LinMPC`](@ref) instance based on repeated online linearization.
324+
325+
## Adapting the Model via Successive Linearization
326+
327+
The [`setmodel!`](@ref) method allows online adaptation of a linear plant model. Combined
328+
with the automatic linearization of [`linearize`](@ref), a successive linearization MPC can
329+
be designed with minimal efforts. The [`SteadyKalmanFilter`](@ref) does not support
330+
[`setmodel!`](@ref), so we need to use the time-varying [`KalmanFilter`](@ref) instead:
331+
332+
```@example 1
333+
kf = KalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
334+
mpc3 = LinMPC(kf; Hp, Hc, Mwt, Nwt, Cwt=Inf, optim=daqp)
335+
mpc3 = setconstraint!(mpc3; umin, umax)
336+
```
337+
338+
We create a function that simulates the plant and the adaptive controller:
339+
340+
```@example 1
341+
function test_slmpc(nonlinmodel, mpc, ry, plant; x_0=plant.xop, y_step=0)
342+
N = 35
343+
U_data, Y_data, Ry_data = zeros(plant.nu, N), zeros(plant.ny, N), zeros(plant.ny, N)
344+
setstate!(plant, x_0)
345+
u, y = [0.0], plant()
346+
x̂ = initstate!(mpc, u, y)
347+
for i = 1:N
348+
y = plant() .+ y_step
349+
u = moveinput!(mpc, ry)
350+
linmodel = linearize(nonlinmodel; u, x=x̂[1:2])
351+
setmodel!(mpc, linmodel)
352+
U_data[:,i], Y_data[:,i], Ry_data[:,i] = u, y, ry
353+
x̂ = updatestate!(mpc, u, y) # update mpc state estimate
354+
updatestate!(plant, u) # update plant simulator
355+
end
356+
res = SimResult(mpc, U_data, Y_data; Ry_data)
357+
return res
358+
end
359+
nothing # hide
360+
```
361+
362+
The [`setmodel!`](@ref) method must be called after solving the optimization problem with
363+
[`moveinput!`](@ref), and before updating the state estimate with [`updatestate!`](@ref).
364+
The [`SimResult`](@ref) object is for plotting purposes only. The adaptive [`LinMPC`](@ref)
365+
performances are similar to the nonlinear MPC, both for the 180° setpoint:
366+
367+
```@example 1
368+
res_slin = test_slmpc(model, mpc3, [180], plant, x_0=[0, 0])
369+
plot(res_slin)
370+
savefig(ans, "plot10_NonLinMPC.svg"); nothing # hide
371+
```
372+
373+
![plot10_NonLinMPC](plot10_NonLinMPC.svg)
374+
375+
and the 10° step disturbance:
376+
377+
```@example 1
378+
res_slin = test_slmpc(model, mpc3, [180], plant, x_0=[π, 0], y_step=[10])
379+
plot(res_slin)
380+
savefig(ans, "plot11_NonLinMPC.svg"); nothing # hide
381+
```
382+
383+
![plot11_NonLinMPC](plot11_NonLinMPC.svg)
384+
385+
The computations of the successive linearization MPC are about 125 times faster than the
386+
nonlinear MPC (0.00012 s per time steps versus 0.015 s per time steps, on average), an
387+
impressive gain for similar closed-loop performances!

docs/src/public/sim_model.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ setop!
5353

5454
```@docs
5555
linearize
56+
linearize!
5657
```

src/ModelPredictiveControl.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import OSQP, Ipopt
2323

2424
export SimModel, LinModel, NonLinModel
2525
export DiffSolver, RungeKutta
26-
export setop!, setstate!, setmodel!, updatestate!, evaloutput, linearize
26+
export setop!, setstate!, setmodel!, updatestate!, evaloutput, linearize, linearize!
2727
export StateEstimator, InternalModel, Luenberger
2828
export SteadyKalmanFilter, KalmanFilter, UnscentedKalmanFilter, ExtendedKalmanFilter
2929
export MovingHorizonEstimator

0 commit comments

Comments
 (0)