Skip to content

Commit 27648cc

Browse files
authored
Merge pull request #135 from JuliaControl/doc_addition
Doc: nonlinear custom constraint example on the motor in the manual
2 parents f0c54b5 + 1fb831d commit 27648cc

File tree

2 files changed

+117
-46
lines changed

2 files changed

+117
-46
lines changed

docs/src/manual/nonlinmpc.md

Lines changed: 115 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@
44
Pages = ["nonlinmpc.md"]
55
```
66

7-
```@setup 1
8-
using Logging; errlogger = ConsoleLogger(stderr, Error);
9-
old_logger = global_logger(); global_logger(errlogger);
10-
```
11-
127
## Nonlinear Model
138

149
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}
4540
constructor assumes by default that the state function `f` is continuous in time, that is,
4641
an ordinary differential equation system (like here):
4742

48-
```@example 1
43+
```@example man_nonlin
4944
using ModelPredictiveControl
5045
function f(x, u, _ , p)
5146
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
6964
differential equations by default. It is good practice to first simulate `model` using
7065
[`sim!`](@ref) as a quick sanity check:
7166

72-
```@example 1
67+
```@example man_nonlin
7368
using Plots
7469
u = [0.5]
7570
N = 35
@@ -88,7 +83,7 @@ method.
8883

8984
An [`UnscentedKalmanFilter`](@ref) estimates the plant state :
9085

91-
```@example 1
86+
```@example man_nonlin
9287
α=0.01; σQ=[0.1, 1.0]; σR=[5.0]; nint_u=[1]; σQint_u=[0.1]
9388
estim = UnscentedKalmanFilter(model; α, σQ, σR, nint_u, σQint_u)
9489
```
@@ -100,7 +95,7 @@ Also, the argument `nint_u` explicitly adds one integrating state at the model i
10095
motor torque ``τ``, with an associated standard deviation `σQint_u` of 0.1 N m. The
10196
estimator tuning is tested on a plant with a 25 % larger friction coefficient ``K``:
10297

103-
```@example 1
98+
```@example man_nonlin
10499
p_plant = copy(p_model)
105100
p_plant[3] = 1.25*p_model[3]
106101
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.
117112
As the motor torque is limited to -1.5 to 1.5 N m, we incorporate the input constraints in
118113
a [`NonLinMPC`](@ref):
119114

120-
```@example 1
115+
```@example man_nonlin
121116
Hp, Hc, Mwt, Nwt = 20, 2, [0.5], [2.5]
122117
nmpc = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt=Inf)
123118
umin, umax = [-1.5], [+1.5]
@@ -127,7 +122,7 @@ nmpc = setconstraint!(nmpc; umin, umax)
127122
The option `Cwt=Inf` disables the slack variable `ϵ` for constraint softening. We test `mpc`
128123
performance on `plant` by imposing an angular setpoint of 180° (inverted position):
129124

130-
```@example 1
125+
```@example man_nonlin
131126
using JuMP; unset_time_limit_sec(nmpc.optim) # hide
132127
res_ry = sim!(nmpc, N, [180.0], plant=plant, x_0=[0, 0], x̂_0=[0, 0, 0])
133128
plot(res_ry)
@@ -140,7 +135,7 @@ The controller seems robust enough to variations on ``K`` coefficient. Starting
140135
inverted position, the closed-loop response to a step disturbances of 10° is also
141136
satisfactory:
142137

143-
```@example 1
138+
```@example man_nonlin
144139
res_yd = sim!(nmpc, N, [180.0], plant=plant, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10])
145140
plot(res_yd)
146141
savefig("plot4_NonLinMPC.svg"); nothing # hide
@@ -181,7 +176,7 @@ angle ``θ`` is measured here). As the arguments of [`NonLinMPC`](@ref) economic
181176
`JE` do not include the states, the speed is now defined as an unmeasured output to design a
182177
Kalman Filter similar to the previous one (``\mathbf{y^m} = θ`` and ``\mathbf{y^u} = ω``):
183178

184-
```@example 1
179+
```@example man_nonlin
185180
h2(x, _ , _ ) = [180/π*x[1], x[2]]
186181
nu, nx, ny = 1, 2, 2
187182
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
193188
output vector of `plant` argument corresponds to the model output vector in `mpc` argument.
194189
We can now define the ``J_E`` function and the `empc` controller:
195190

196-
```@example 1
191+
```@example man_nonlin
197192
function JE(Ue, Ŷe, _ , p)
198193
Ts = p
199194
τ, ω = 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
209204
speed ``ω`` is not requested to track a setpoint. The closed-loop response to a 180°
210205
setpoint is similar:
211206

212-
```@example 1
207+
```@example man_nonlin
213208
unset_time_limit_sec(empc.optim) # hide
214209
res2_ry = sim!(empc, N, [180, 0], plant=plant2, x_0=[0, 0], x̂_0=[0, 0, 0])
215210
plot(res2_ry, ploty=[1])
@@ -220,7 +215,7 @@ savefig("plot5_NonLinMPC.svg"); nothing # hide
220215

221216
and the energy consumption is slightly lower:
222217

223-
```@example 1
218+
```@example man_nonlin
224219
function calcW(res)
225220
τ, ω = res.U_data[1, 1:end-1], res.X_data[2, 1:end-1]
226221
return Ts*sum(τ.*ω)
@@ -230,7 +225,7 @@ Dict(:W_nmpc => calcW(res_ry), :W_empc => calcW(res2_ry))
230225

231226
Also, for a 10° step disturbance:
232227

233-
```@example 1
228+
```@example man_nonlin
234229
res2_yd = sim!(empc, N, [180; 0]; plant=plant2, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10, 0])
235230
plot(res2_yd, ploty=[1])
236231
savefig("plot6_NonLinMPC.svg"); nothing # hide
@@ -241,13 +236,93 @@ savefig("plot6_NonLinMPC.svg"); nothing # hide
241236
the new controller is able to recuperate a little more energy from the pendulum (i.e.
242237
negative work):
243238

244-
```@example 1
239+
```@example man_nonlin
245240
Dict(:W_nmpc => calcW(res_yd), :W_empc => calcW(res2_yd))
246241
```
247242

248243
Of course, this gain is only exploitable if the motor electronic includes some kind of
249244
regenerative circuitry.
250245

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+
251326
## Model Linearization
252327

253328
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
257332
function allows automatic linearization of [`NonLinModel`](@ref) based on [`ForwardDiff.jl`](https://juliadiff.org/ForwardDiff.jl/stable/).
258333
We first linearize `model` at the point ``θ = π`` rad and ``ω = τ = 0`` (inverted position):
259334

260-
```@example 1
335+
```@example man_nonlin
261336
linmodel = linearize(model, x=[π, 0], u=[0])
262337
```
263338

264339
A [`SteadyKalmanFilter`](@ref) and a [`LinMPC`](@ref) are designed from `linmodel`:
265340

266-
```@example 1
341+
```@example man_nonlin
267342
268343
skf = SteadyKalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
269344
mpc = LinMPC(skf; Hp, Hc, Mwt, Nwt, Cwt=Inf)
@@ -272,13 +347,13 @@ mpc = setconstraint!(mpc, umin=[-1.5], umax=[+1.5])
272347

273348
The linear controller satisfactorily rejects the 10° step disturbance:
274349

275-
```@example 1
350+
```@example man_nonlin
276351
res_lin = sim!(mpc, N, [180.0]; plant, x_0=[π, 0], y_step=[10])
277352
plot(res_lin)
278-
savefig("plot7_NonLinMPC.svg"); nothing # hide
353+
savefig("plot9_NonLinMPC.svg"); nothing # hide
279354
```
280355

281-
![plot7_NonLinMPC](plot7_NonLinMPC.svg)
356+
![plot9_NonLinMPC](plot9_NonLinMPC.svg)
282357

283358
Solving the optimization problem of `mpc` with [`DAQP`](https://darnstrom.github.io/daqp/)
284359
optimizer instead of the default `OSQP` solver can improve the performance here. It is
@@ -290,7 +365,7 @@ than one):
290365
embedded quadratic programming using recursive LDLᵀ updates. IEEE Trans. Autom. Contr.,
291366
67(8). <https://doi.org/doi:10.1109/TAC.2022.3176430>.
292367

293-
```@example 1
368+
```@example man_nonlin
294369
using LinearAlgebra; poles = eigvals(linmodel.A)
295370
```
296371

@@ -302,7 +377,7 @@ using Pkg; Pkg.add("DAQP")
302377

303378
Constructing a [`LinMPC`](@ref) with `DAQP`:
304379

305-
```@example 1
380+
```@example man_nonlin
306381
using JuMP, DAQP
307382
daqp = Model(DAQP.Optimizer, add_bridges=false)
308383
mpc2 = LinMPC(skf; Hp, Hc, Mwt, Nwt, Cwt=Inf, optim=daqp)
@@ -311,27 +386,27 @@ mpc2 = setconstraint!(mpc2; umin, umax)
311386

312387
does slightly improve the rejection of the step disturbance:
313388

314-
```@example 1
389+
```@example man_nonlin
315390
res_lin2 = sim!(mpc2, N, [180.0]; plant, x_0=[π, 0], y_step=[10])
316391
plot(res_lin2)
317-
savefig("plot8_NonLinMPC.svg"); nothing # hide
392+
savefig("plot10_NonLinMPC.svg"); nothing # hide
318393
```
319394

320-
![plot8_NonLinMPC](plot8_NonLinMPC.svg)
395+
![plot10_NonLinMPC](plot10_NonLinMPC.svg)
321396

322397
The closed-loop performance is still lower than the nonlinear controller, as expected, but
323398
computations are about 210 times faster (0.000071 s versus 0.015 s per time steps, on
324399
average). However, remember that `linmodel` is only valid for angular positions near 180°.
325400
For example, the 180° setpoint response from 0° is unsatisfactory since the predictions are
326401
poor in the first quadrant:
327402

328-
```@example 1
403+
```@example man_nonlin
329404
res_lin3 = sim!(mpc2, N, [180.0]; plant, x_0=[0, 0])
330405
plot(res_lin3)
331-
savefig("plot9_NonLinMPC.svg"); nothing # hide
406+
savefig("plot11_NonLinMPC.svg"); nothing # hide
332407
```
333408

334-
![plot9_NonLinMPC](plot9_NonLinMPC.svg)
409+
![plot11_NonLinMPC](plot11_NonLinMPC.svg)
335410

336411
Multiple linearized model and controller objects are required for large deviations from this
337412
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
345420
[`setmodel!`](@ref) so we need to use the time-varying [`KalmanFilter`](@ref), and we
346421
initialize it with a linearization at ``θ = ω = τ = 0``:
347422

348-
```@example 1
423+
```@example man_nonlin
349424
linmodel = linearize(model, x=[0, 0], u=[0])
350425
kf = KalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
351426
mpc3 = LinMPC(kf; Hp, Hc, Mwt, Nwt, Cwt=Inf, optim=daqp)
@@ -354,7 +429,7 @@ mpc3 = setconstraint!(mpc3; umin, umax)
354429

355430
We create a function that simulates the plant and the adaptive controller:
356431

357-
```@example 1
432+
```@example man_nonlin
358433
function sim_adapt!(mpc, nonlinmodel, N, ry, plant, x_0, x̂_0, y_step=[0])
359434
U_data, Y_data, Ry_data = zeros(plant.nu, N), zeros(plant.ny, N), zeros(plant.ny, N)
360435
setstate!(plant, x_0)
@@ -382,29 +457,25 @@ that is, when both ``\mathbf{u}(k)`` and ``\mathbf{x̂}_{k}(k)`` are available a
382457
operating point. The [`SimResult`](@ref) object is for plotting purposes only. The adaptive
383458
[`LinMPC`](@ref) performances are similar to the nonlinear MPC, both for the 180° setpoint:
384459

385-
```@example 1
460+
```@example man_nonlin
386461
x_0 = [0, 0]; x̂_0 = [0, 0, 0]; ry = [180]
387462
res_slin = sim_adapt!(mpc3, model, N, ry, plant, x_0, x̂_0)
388463
plot(res_slin)
389-
savefig("plot10_NonLinMPC.svg"); nothing # hide
464+
savefig("plot12_NonLinMPC.svg"); nothing # hide
390465
```
391466

392-
![plot10_NonLinMPC](plot10_NonLinMPC.svg)
467+
![plot12_NonLinMPC](plot12_NonLinMPC.svg)
393468

394469
and the 10° step disturbance:
395470

396-
```@example 1
471+
```@example man_nonlin
397472
x_0 = [π, 0]; x̂_0 = [π, 0, 0]; y_step = [10]
398473
res_slin = sim_adapt!(mpc3, model, N, ry, plant, x_0, x̂_0, y_step)
399474
plot(res_slin)
400-
savefig("plot11_NonLinMPC.svg"); nothing # hide
475+
savefig("plot13_NonLinMPC.svg"); nothing # hide
401476
```
402477

403-
![plot11_NonLinMPC](plot11_NonLinMPC.svg)
478+
![plot13_NonLinMPC](plot13_NonLinMPC.svg)
404479

405480
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!

src/controller/construct.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -781,8 +781,8 @@ constraints:
781781
\mathbf{A_{U_{max}}}
782782
\end{bmatrix} \mathbf{ΔŨ} ≤
783783
\begin{bmatrix}
784-
- \mathbf{(U_{min}) + T} \mathbf{u}(k-1) \\
785-
+ \mathbf{(U_{max}) - T} \mathbf{u}(k-1)
784+
- \mathbf{U_{min} + T} \mathbf{u}(k-1) \\
785+
+ \mathbf{U_{max} - T} \mathbf{u}(k-1)
786786
\end{bmatrix}
787787
```
788788
in which ``\mathbf{U_{min}}`` and ``\mathbf{U_{max}}`` vectors respectively contains

0 commit comments

Comments
 (0)