4
4
Pages = ["nonlinmpc.md"]
5
5
```
6
6
7
- ``` @setup 1
8
- using Logging; errlogger = ConsoleLogger(stderr, Error);
9
- old_logger = global_logger(); global_logger(errlogger);
10
- ```
11
-
12
7
## Nonlinear Model
13
8
14
9
In this example, the goal is to control the angular position `` θ `` of a pendulum
@@ -45,7 +40,7 @@ the end of the pendulum in kg, all bundled in the parameter vector ``\mathbf{p}
45
40
constructor assumes by default that the state function ` f ` is continuous in time, that is,
46
41
an ordinary differential equation system (like here):
47
42
48
- ``` @example 1
43
+ ``` @example man_nonlin
49
44
using ModelPredictiveControl
50
45
function f(x, u, _ , p)
51
46
g, L, K, m = p # [m/s²], [m], [kg/s], [kg]
@@ -69,7 +64,7 @@ vector of you want to modify it later. A 4th order [`RungeKutta`](@ref) method s
69
64
differential equations by default. It is good practice to first simulate ` model ` using
70
65
[ ` sim! ` ] ( @ref ) as a quick sanity check:
71
66
72
- ``` @example 1
67
+ ``` @example man_nonlin
73
68
using Plots
74
69
u = [0.5]
75
70
N = 35
@@ -88,7 +83,7 @@ method.
88
83
89
84
An [ ` UnscentedKalmanFilter ` ] ( @ref ) estimates the plant state :
90
85
91
- ``` @example 1
86
+ ``` @example man_nonlin
92
87
α=0.01; σQ=[0.1, 1.0]; σR=[5.0]; nint_u=[1]; σQint_u=[0.1]
93
88
estim = UnscentedKalmanFilter(model; α, σQ, σR, nint_u, σQint_u)
94
89
```
@@ -100,7 +95,7 @@ Also, the argument `nint_u` explicitly adds one integrating state at the model i
100
95
motor torque `` τ `` , with an associated standard deviation ` σQint_u ` of 0.1 N m. The
101
96
estimator tuning is tested on a plant with a 25 % larger friction coefficient `` K `` :
102
97
103
- ``` @example 1
98
+ ``` @example man_nonlin
104
99
p_plant = copy(p_model)
105
100
p_plant[3] = 1.25*p_model[3]
106
101
plant = setname!(NonLinModel(f, h, Ts, nu, nx, ny; p=p_plant); u=vu, x=vx, y=vy)
@@ -117,7 +112,7 @@ static errors. The Kalman filter performance seems sufficient for control.
117
112
As the motor torque is limited to -1.5 to 1.5 N m, we incorporate the input constraints in
118
113
a [ ` NonLinMPC ` ] ( @ref ) :
119
114
120
- ``` @example 1
115
+ ``` @example man_nonlin
121
116
Hp, Hc, Mwt, Nwt = 20, 2, [0.5], [2.5]
122
117
nmpc = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt=Inf)
123
118
umin, umax = [-1.5], [+1.5]
@@ -127,7 +122,7 @@ nmpc = setconstraint!(nmpc; umin, umax)
127
122
The option ` Cwt=Inf ` disables the slack variable ` ϵ ` for constraint softening. We test ` mpc `
128
123
performance on ` plant ` by imposing an angular setpoint of 180° (inverted position):
129
124
130
- ``` @example 1
125
+ ``` @example man_nonlin
131
126
using JuMP; unset_time_limit_sec(nmpc.optim) # hide
132
127
res_ry = sim!(nmpc, N, [180.0], plant=plant, x_0=[0, 0], x̂_0=[0, 0, 0])
133
128
plot(res_ry)
@@ -140,7 +135,7 @@ The controller seems robust enough to variations on ``K`` coefficient. Starting
140
135
inverted position, the closed-loop response to a step disturbances of 10° is also
141
136
satisfactory:
142
137
143
- ``` @example 1
138
+ ``` @example man_nonlin
144
139
res_yd = sim!(nmpc, N, [180.0], plant=plant, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10])
145
140
plot(res_yd)
146
141
savefig("plot4_NonLinMPC.svg"); nothing # hide
@@ -181,7 +176,7 @@ angle ``θ`` is measured here). As the arguments of [`NonLinMPC`](@ref) economic
181
176
` JE ` do not include the states, the speed is now defined as an unmeasured output to design a
182
177
Kalman Filter similar to the previous one (`` \mathbf{y^m} = θ `` and `` \mathbf{y^u} = ω `` ):
183
178
184
- ``` @example 1
179
+ ``` @example man_nonlin
185
180
h2(x, _ , _ ) = [180/π*x[1], x[2]]
186
181
nu, nx, ny = 1, 2, 2
187
182
model2 = setname!(NonLinModel(f, h2, Ts, nu, nx, ny; p=p_model), u=vu, x=vx, y=[vy; vx[2]])
@@ -193,7 +188,7 @@ The `plant2` object based on `h2` is also required since [`sim!`](@ref) expects
193
188
output vector of ` plant ` argument corresponds to the model output vector in ` mpc ` argument.
194
189
We can now define the `` J_E `` function and the ` empc ` controller:
195
190
196
- ``` @example 1
191
+ ``` @example man_nonlin
197
192
function JE(Ue, Ŷe, _ , p)
198
193
Ts = p
199
194
τ, ω = Ue[1:end-1], Ŷe[2:2:end-1]
@@ -209,7 +204,7 @@ lead to a static error on the angle setpoint. The second element of `Mwt` is zer
209
204
speed `` ω `` is not requested to track a setpoint. The closed-loop response to a 180°
210
205
setpoint is similar:
211
206
212
- ``` @example 1
207
+ ``` @example man_nonlin
213
208
unset_time_limit_sec(empc.optim) # hide
214
209
res2_ry = sim!(empc, N, [180, 0], plant=plant2, x_0=[0, 0], x̂_0=[0, 0, 0])
215
210
plot(res2_ry, ploty=[1])
@@ -220,7 +215,7 @@ savefig("plot5_NonLinMPC.svg"); nothing # hide
220
215
221
216
and the energy consumption is slightly lower:
222
217
223
- ``` @example 1
218
+ ``` @example man_nonlin
224
219
function calcW(res)
225
220
τ, ω = res.U_data[1, 1:end-1], res.X_data[2, 1:end-1]
226
221
return Ts*sum(τ.*ω)
@@ -230,7 +225,7 @@ Dict(:W_nmpc => calcW(res_ry), :W_empc => calcW(res2_ry))
230
225
231
226
Also, for a 10° step disturbance:
232
227
233
- ``` @example 1
228
+ ``` @example man_nonlin
234
229
res2_yd = sim!(empc, N, [180; 0]; plant=plant2, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10, 0])
235
230
plot(res2_yd, ploty=[1])
236
231
savefig("plot6_NonLinMPC.svg"); nothing # hide
@@ -241,13 +236,93 @@ savefig("plot6_NonLinMPC.svg"); nothing # hide
241
236
the new controller is able to recuperate a little more energy from the pendulum (i.e.
242
237
negative work):
243
238
244
- ``` @example 1
239
+ ``` @example man_nonlin
245
240
Dict(:W_nmpc => calcW(res_yd), :W_empc => calcW(res2_yd))
246
241
```
247
242
248
243
Of course, this gain is only exploitable if the motor electronic includes some kind of
249
244
regenerative circuitry.
250
245
246
+ ## Custom Nonlinear Inequality Constraints
247
+
248
+ Instead of limits on the torque, suppose that the motor can deliver a maximum of 3 watt:
249
+
250
+ ``` math
251
+ P(t) = τ(t) ω(t) ≤ P_\mathrm{max}
252
+ ```
253
+
254
+ with `` P_\mathrm{max} = 3 `` W. This inequality describes nonlinear constraints that can
255
+ be implemented using the custom `` \mathbf{g_c} `` function of [ ` NonLinMPC ` ] ( @ref ) . The
256
+ constructor expects a function in this form:
257
+
258
+ ``` math
259
+ \mathbf{g_c}(\mathbf{U_e}, \mathbf{Ŷ_e}, \mathbf{D̂_e}, \mathbf{p}, ϵ) ≤ \mathbf{0}
260
+ ```
261
+
262
+ in which `` ϵ `` is the slack variable (scalar), an necessary feature to ensure feasibility
263
+ when the nonlinear inequality constraints are active. There is also an additional ` LHS `
264
+ argument ("left-hand side" of the inequality above) for the in-place version:
265
+
266
+ ``` @example man_nonlin
267
+ function gc!(LHS, Ue, Ŷe, _, p, ϵ)
268
+ Pmax, Hp = p
269
+ i_τ, i_ω = 1, 2
270
+ for i in eachindex(LHS)
271
+ τ, ω = Ue[i_τ], Ŷe[i_ω]
272
+ P = τ*ω
273
+ LHS[i] = P - Pmax - ϵ
274
+ i_τ += 1
275
+ i_ω += 2
276
+ end
277
+ return nothing
278
+ end
279
+ nothing # hide
280
+ ```
281
+
282
+ Here we implemented the custom nonlinear constraint function in the mutating form to reduce
283
+ the computational burden (see [ ` NonLinMPC ` ] ( @ref ) Extended Help for details). Note that
284
+ similar mutating forms are also possible for the ` f ` and ` h ` functions of [ ` NonLinModel ` ] ( @ref ) .
285
+ We construct the controller by enabling relaxation with the ` Cwt ` argument, and also by
286
+ specifying the number of custom inequality constraints ` nc ` :
287
+
288
+ ``` @example man_nonlin
289
+ Cwt, Pmax, nc = 1e5, 3, Hp+1
290
+ p_nmpc2 = [Pmax, Hp]
291
+ nmpc2 = NonLinMPC(estim2; Hp, Hc, Nwt=Nwt, Mwt=[0.5, 0], Cwt, gc!, nc, p=p_nmpc2)
292
+ using JuMP; unset_time_limit_sec(nmpc2.optim) # hide
293
+ nmpc2 # hide
294
+ ```
295
+
296
+ In addition to the 180° setpoint response:
297
+
298
+ ``` @example man_nonlin
299
+ using Logging # hide
300
+ res3_ry = with_logger(ConsoleLogger(stderr, Error)) do # hide
301
+ res3_ry = sim!(nmpc2, N, [180; 0]; plant=plant2, x_0=[0, 0], x̂_0=[0, 0, 0]);
302
+ end # hide
303
+ nothing # hide
304
+ ```
305
+
306
+ a plot for the power `` P(t) `` is included below:
307
+
308
+ ``` @example man_nonlin
309
+ function plotWithPower(res, Pmax)
310
+ t, τ, ω = res.T_data, res.U_data[1, :], res.X_data[2, :]
311
+ P, Pmax = τ.*ω, fill(Pmax, size(t))
312
+ plt1 = plot(res, ploty=[1])
313
+ plt2 = plot(t, P, label=raw"$P$", ylabel=raw"$P$ (W)", xlabel="Time (s)", legend=:right)
314
+ plot!(plt2, t, Pmax, label=raw"$P_\mathrm{max}$", linestyle=:dot, linewidth=1.5)
315
+ return plot(plt1, plt2, layout=(2,1))
316
+ end
317
+ plotWithPower(res3_ry, Pmax)
318
+ savefig("plot7_NonLinMPC.svg"); nothing # hide
319
+ ```
320
+
321
+ ![ plot7_NonLinMPC] ( plot7_NonLinMPC.svg )
322
+
323
+ The slight constraint violation is caused here by the modeling error on the friction
324
+ coefficient `` K `` .
325
+
251
326
## Model Linearization
252
327
253
328
Nonlinear MPC is more computationally expensive than [ ` LinMPC ` ] ( @ref ) . Solving the problem
@@ -257,13 +332,13 @@ fast dynamics. To ease the design and comparison with [`LinMPC`](@ref), the [`li
257
332
function allows automatic linearization of [ ` NonLinModel ` ] ( @ref ) based on [ ` ForwardDiff.jl ` ] ( https://juliadiff.org/ForwardDiff.jl/stable/ ) .
258
333
We first linearize ` model ` at the point `` θ = π `` rad and `` ω = τ = 0 `` (inverted position):
259
334
260
- ``` @example 1
335
+ ``` @example man_nonlin
261
336
linmodel = linearize(model, x=[π, 0], u=[0])
262
337
```
263
338
264
339
A [ ` SteadyKalmanFilter ` ] ( @ref ) and a [ ` LinMPC ` ] ( @ref ) are designed from ` linmodel ` :
265
340
266
- ``` @example 1
341
+ ``` @example man_nonlin
267
342
268
343
skf = SteadyKalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
269
344
mpc = LinMPC(skf; Hp, Hc, Mwt, Nwt, Cwt=Inf)
@@ -272,13 +347,13 @@ mpc = setconstraint!(mpc, umin=[-1.5], umax=[+1.5])
272
347
273
348
The linear controller satisfactorily rejects the 10° step disturbance:
274
349
275
- ``` @example 1
350
+ ``` @example man_nonlin
276
351
res_lin = sim!(mpc, N, [180.0]; plant, x_0=[π, 0], y_step=[10])
277
352
plot(res_lin)
278
- savefig("plot7_NonLinMPC .svg"); nothing # hide
353
+ savefig("plot9_NonLinMPC .svg"); nothing # hide
279
354
```
280
355
281
- ![ plot7_NonLinMPC ] ( plot7_NonLinMPC .svg)
356
+ ![ plot9_NonLinMPC ] ( plot9_NonLinMPC .svg)
282
357
283
358
Solving the optimization problem of ` mpc ` with [ ` DAQP ` ] ( https://darnstrom.github.io/daqp/ )
284
359
optimizer instead of the default ` OSQP ` solver can improve the performance here. It is
@@ -290,7 +365,7 @@ than one):
290
365
embedded quadratic programming using recursive LDLᵀ updates. IEEE Trans. Autom. Contr.,
291
366
67(8). < https://doi.org/doi:10.1109/TAC.2022.3176430 > .
292
367
293
- ``` @example 1
368
+ ``` @example man_nonlin
294
369
using LinearAlgebra; poles = eigvals(linmodel.A)
295
370
```
296
371
@@ -302,7 +377,7 @@ using Pkg; Pkg.add("DAQP")
302
377
303
378
Constructing a [ ` LinMPC ` ] ( @ref ) with ` DAQP ` :
304
379
305
- ``` @example 1
380
+ ``` @example man_nonlin
306
381
using JuMP, DAQP
307
382
daqp = Model(DAQP.Optimizer, add_bridges=false)
308
383
mpc2 = LinMPC(skf; Hp, Hc, Mwt, Nwt, Cwt=Inf, optim=daqp)
@@ -311,27 +386,27 @@ mpc2 = setconstraint!(mpc2; umin, umax)
311
386
312
387
does slightly improve the rejection of the step disturbance:
313
388
314
- ``` @example 1
389
+ ``` @example man_nonlin
315
390
res_lin2 = sim!(mpc2, N, [180.0]; plant, x_0=[π, 0], y_step=[10])
316
391
plot(res_lin2)
317
- savefig("plot8_NonLinMPC .svg"); nothing # hide
392
+ savefig("plot10_NonLinMPC .svg"); nothing # hide
318
393
```
319
394
320
- ![ plot8_NonLinMPC ] ( plot8_NonLinMPC .svg)
395
+ ![ plot10_NonLinMPC ] ( plot10_NonLinMPC .svg)
321
396
322
397
The closed-loop performance is still lower than the nonlinear controller, as expected, but
323
398
computations are about 210 times faster (0.000071 s versus 0.015 s per time steps, on
324
399
average). However, remember that ` linmodel ` is only valid for angular positions near 180°.
325
400
For example, the 180° setpoint response from 0° is unsatisfactory since the predictions are
326
401
poor in the first quadrant:
327
402
328
- ``` @example 1
403
+ ``` @example man_nonlin
329
404
res_lin3 = sim!(mpc2, N, [180.0]; plant, x_0=[0, 0])
330
405
plot(res_lin3)
331
- savefig("plot9_NonLinMPC .svg"); nothing # hide
406
+ savefig("plot11_NonLinMPC .svg"); nothing # hide
332
407
```
333
408
334
- ![ plot9_NonLinMPC ] ( plot9_NonLinMPC .svg)
409
+ ![ plot11_NonLinMPC ] ( plot11_NonLinMPC .svg)
335
410
336
411
Multiple linearized model and controller objects are required for large deviations from this
337
412
operating point. This is known as gain scheduling. Another approach is adapting the model of
@@ -345,7 +420,7 @@ be designed with minimal efforts. The [`SteadyKalmanFilter`](@ref) does not supp
345
420
[ ` setmodel! ` ] ( @ref ) so we need to use the time-varying [ ` KalmanFilter ` ] ( @ref ) , and we
346
421
initialize it with a linearization at `` θ = ω = τ = 0 `` :
347
422
348
- ``` @example 1
423
+ ``` @example man_nonlin
349
424
linmodel = linearize(model, x=[0, 0], u=[0])
350
425
kf = KalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
351
426
mpc3 = LinMPC(kf; Hp, Hc, Mwt, Nwt, Cwt=Inf, optim=daqp)
@@ -354,7 +429,7 @@ mpc3 = setconstraint!(mpc3; umin, umax)
354
429
355
430
We create a function that simulates the plant and the adaptive controller:
356
431
357
- ``` @example 1
432
+ ``` @example man_nonlin
358
433
function sim_adapt!(mpc, nonlinmodel, N, ry, plant, x_0, x̂_0, y_step=[0])
359
434
U_data, Y_data, Ry_data = zeros(plant.nu, N), zeros(plant.ny, N), zeros(plant.ny, N)
360
435
setstate!(plant, x_0)
@@ -382,29 +457,25 @@ that is, when both ``\mathbf{u}(k)`` and ``\mathbf{x̂}_{k}(k)`` are available a
382
457
operating point. The [ ` SimResult ` ] ( @ref ) object is for plotting purposes only. The adaptive
383
458
[ ` LinMPC ` ] ( @ref ) performances are similar to the nonlinear MPC, both for the 180° setpoint:
384
459
385
- ``` @example 1
460
+ ``` @example man_nonlin
386
461
x_0 = [0, 0]; x̂_0 = [0, 0, 0]; ry = [180]
387
462
res_slin = sim_adapt!(mpc3, model, N, ry, plant, x_0, x̂_0)
388
463
plot(res_slin)
389
- savefig("plot10_NonLinMPC .svg"); nothing # hide
464
+ savefig("plot12_NonLinMPC .svg"); nothing # hide
390
465
```
391
466
392
- ![ plot10_NonLinMPC ] ( plot10_NonLinMPC .svg)
467
+ ![ plot12_NonLinMPC ] ( plot12_NonLinMPC .svg)
393
468
394
469
and the 10° step disturbance:
395
470
396
- ``` @example 1
471
+ ``` @example man_nonlin
397
472
x_0 = [π, 0]; x̂_0 = [π, 0, 0]; y_step = [10]
398
473
res_slin = sim_adapt!(mpc3, model, N, ry, plant, x_0, x̂_0, y_step)
399
474
plot(res_slin)
400
- savefig("plot11_NonLinMPC .svg"); nothing # hide
475
+ savefig("plot13_NonLinMPC .svg"); nothing # hide
401
476
```
402
477
403
- ![ plot11_NonLinMPC ] ( plot11_NonLinMPC .svg)
478
+ ![ plot13_NonLinMPC ] ( plot13_NonLinMPC .svg)
404
479
405
480
The computations of the successive linearization MPC are about 75 times faster than the
406
- nonlinear MPC on average, an impressive gain for similar closed-loop performances!
407
-
408
- ``` @setup 1
409
- global_logger(old_logger);
410
- ```
481
+ nonlinear MPC on average, an impressive gain for similar closed-loop performances!
0 commit comments