Skip to content

Commit 85e4ac8

Browse files
committed
added : linearize function
- automatic linearization based on `ForwardDiff` - unit test for linearization - doc: example for the pendulum in the manual
1 parent 0baa02f commit 85e4ac8

File tree

4 files changed

+99
-25
lines changed

4 files changed

+99
-25
lines changed

docs/src/manual/linmpc.md

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -154,26 +154,17 @@ savefig(ans, "plot1_LinMPC.svg"); nothing # hide
154154

155155
![plot1_LinMPC](plot1_LinMPC.svg)
156156

157-
For some situations, when [`LinMPC`](@ref) matrices are small/medium and dense, [`DAQP`](https://darnstrom.github.io/daqp/)
158-
optimizer may be more efficient. To install it, run:
159-
160-
```text
161-
using Pkg; Pkg.add("DAQP")
162-
```
163-
164-
Also, compared to the default setting, adding the integrating states at the model inputs may
165-
improve the closed-loop performance. Load disturbances are indeed very frequent in many
166-
real-life control problems. Constructing a [`LinMPC`](@ref) with `DAQP` and input integrators:
157+
Compared to the default setting, adding the integrating states at the model inputs may
158+
improve the closed-loop performance. Load disturbances are indeed very common in many
159+
real-life control problems. Constructing a [`LinMPC`](@ref) with input integrators:
167160

168161
```@example 1
169-
using JuMP, DAQP
170-
daqp = Model(DAQP.Optimizer, add_bridges=false)
171-
mpc2 = LinMPC(model, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1], optim=daqp, nint_u=[1, 1])
162+
mpc2 = LinMPC(model, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1], nint_u=[1, 1])
172163
mpc2 = setconstraint!(mpc2, ymin=[45, -Inf])
173164
```
174165

175-
leads to similar computational times, but it does accelerate the rejection of the load
176-
disturbance and eliminates the level constraint violation:
166+
does accelerate the rejection of the load disturbance and eliminates the level constraint
167+
violation:
177168

178169
```@example 1
179170
setstate!(model, zeros(model.nx))

docs/src/manual/nonlinmpc.md

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,91 @@ satisfactory:
130130

131131
```@example 1
132132
res_yd = sim!(nmpc, N, [180.0], plant=plant, x0=[π, 0], x̂0=[π, 0, 0], y_step=[10])
133+
using BenchmarkTools
134+
@btime sim!(nmpc, N, [180.0], plant=plant, x0=[π, 0], x̂0=[π, 0, 0], y_step=[10])
133135
plot(res_yd)
134136
savefig(ans, "plot4_NonLinMPC.svg"); nothing # hide
135137
```
136138

137139
![plot4_NonLinMPC](plot4_NonLinMPC.svg)
138140

141+
## Linearizing the Model
142+
143+
Nonlinear MPC are more computationally expensive than [`LinMPC`](@ref). Solving the problem
144+
should always be faster than the sampling time ``T_s = 0.1`` s for real-time operation. For
145+
electronic and mechanical systems like here, this requirement is sometimes harder to achieve
146+
because of their fast dynamics. To ease the design and comparison with [`LinMPC`](@ref), the
147+
[`linearize`](@ref) function allows automatic linearization of [`NonLinModel`](@ref) based
148+
on [`ForwardDiff.jl`](https://juliadiff.org/ForwardDiff.jl/stable/). We first linearize
149+
`model` at the operating points ``\mathbf{x} = [\begin{smallmatrix} π\\0 \end{smallmatrix}]``
150+
and ``\mathbf{u} = 0`` (inverted position):
151+
152+
```@example 1
153+
linmodel = linearize(model, x=[π, 0], u=[0])
154+
```
155+
156+
It is worth mentionning that the Euler method in `model` object is not the best choice for
157+
linearization since its accuracy is low (i.e. approximation of a bad approximation). A
158+
[`SteadyKalmanFilter`](@ref) and a [`LinMPC`](@ref) is designed from `linmodel`:
159+
160+
```@example 1
161+
kf = SteadyKalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)
162+
mpc = LinMPC(kf, Hp=20, Hc=2, Mwt=[0.5], Nwt=[2.5])
163+
mpc = setconstraint!(mpc, umin=[-1.5], umax=[+1.5])
164+
```
165+
166+
The linear controller has difficulties to reject the 10° step disturbance:
167+
168+
```@example 1
169+
res_lin = sim!(mpc, N, [180.0]; plant, x0=[π, 0], y_step=[10])
170+
plot(res_lin)
171+
savefig(ans, "plot5_NonLinMPC.svg"); nothing # hide
172+
```
173+
174+
![plot5_NonLinMPC](plot5_NonLinMPC.svg)
175+
176+
Some improvements can be achieved by solving the optimization problem of `mpc` with [`DAQP`](https://darnstrom.github.io/daqp/)
177+
optimizer instead of the default `OSQP` solver. It is indeed documented that `DAQP` can
178+
perform better on small/medium dense matrices and unstable poles, which is obviously the
179+
case here (the absolute value of unstable poles are greater than one).:
180+
181+
```@example 1
182+
using LinearAlgebra; poles = eigvals(linmodel.A)
183+
```
184+
185+
To install it, run:
186+
187+
```text
188+
using Pkg; Pkg.add("DAQP")
189+
```
190+
191+
Constructing a [`LinMPC`](@ref) with `DAQP`:
192+
193+
```@example 1
194+
using JuMP, DAQP
195+
daqp = Model(DAQP.Optimizer, add_bridges=false)
196+
mpc2 = LinMPC(kf, Hp=20, Hc=2, Mwt=[0.5], Nwt=[2.5], optim=daqp)
197+
mpc2 = setconstraint!(mpc, umin=[-1.5], umax=[+1.5])
198+
```
199+
200+
does improve the rejection of the step disturbance:
201+
202+
```@example 1
203+
res_lin2 = sim!(mpc2, N, [180.0]; plant, x0=[π, 0], y_step=[10])
204+
using BenchmarkTools
205+
@btime sim!(mpc2, N, [180.0]; plant, x0=[π, 0], y_step=[10])
206+
plot(res_lin2)
207+
savefig(ans, "plot6_NonLinMPC.svg"); nothing # hide
208+
```
209+
210+
![plot6_NonLinMPC](plot6_NonLinMPC.svg)
211+
212+
The performance is still lower than the nonlinear controller, as expected, but computations
213+
are about 2000 times faster (0.00002 s versus 0.04 s per time steps on average). Note that
214+
`linmodel` is only valid for angular position near 180°. Multiple linearized models and
215+
controllers are required for large deviations from this operating point. This is known as
216+
gain scheduling.
217+
139218
## Economic Model Predictive Controller
140219

141220
Economic MPC can achieve the same objective but with lower economical costs. For this case
@@ -198,10 +277,10 @@ setpoint is:
198277
unset_time_limit_sec(empc.optim) # hide
199278
res2_ry = sim!(empc, N, [180, 0], plant=plant2, x0=[0, 0], x̂0=[0, 0, 0])
200279
plot(res2_ry)
201-
savefig(ans, "plot5_NonLinMPC.svg"); nothing # hide
280+
savefig(ans, "plot7_NonLinMPC.svg"); nothing # hide
202281
```
203282

204-
![plot5_NonLinMPC](plot5_NonLinMPC.svg)
283+
![plot7_NonLinMPC](plot7_NonLinMPC.svg)
205284

206285
and the energy consumption is slightly lower:
207286

@@ -218,10 +297,10 @@ Also, for a 10° step disturbance:
218297
```@example 1
219298
res2_yd = sim!(empc, N, [180; 0]; plant=plant2, x0=[π, 0], x̂0=[π, 0, 0], y_step=[10, 0])
220299
plot(res2_yd)
221-
savefig(ans, "plot6_NonLinMPC.svg"); nothing # hide
300+
savefig(ans, "plot8_NonLinMPC.svg"); nothing # hide
222301
```
223302

224-
![plot6_NonLinMPC](plot6_NonLinMPC.svg)
303+
![plot8_NonLinMPC](plot8_NonLinMPC.svg)
225304

226305
the new controller is able to recuperate more energy from the pendulum (i.e. negative work):
227306

src/estimator/kalman.jl

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,7 @@ identical to [`UnscentedKalmanFilter`](@ref). The Jacobians of the augmented mod
632632
automatic differentiation.
633633
634634
!!! warning
635-
See Extended Help if you get an error like:
635+
See the Extended Help of [`linearize`](@ref) function if you get an error like:
636636
`MethodError: no method matching (::var"##")(::Vector{ForwardDiff.Dual})`.
637637
638638
# Arguments
@@ -652,11 +652,6 @@ ExtendedKalmanFilter estimator with a sample time Ts = 5.0 s, NonLinModel and:
652652
0 unmeasured outputs yu
653653
0 measured disturbances d
654654
```
655-
656-
# Extended Help
657-
Automatic differentiation (AD) allows exact Jacobians. The [`NonLinModel`](@ref) `f` and `h`
658-
functions must be compatible with this feature though. See [Automatic differentiation](https://jump.dev/JuMP.jl/stable/manual/nlp/#Automatic-differentiation)
659-
for common mistakes when writing these functions.
660655
"""
661656
function ExtendedKalmanFilter(
662657
model::SM;

src/model/linearization.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ manipulated input ``\mathbf{u}`` and measured disturbance ``\mathbf{d}``, respec
1717
Jacobians of ``\mathbf{f}`` and ``\mathbf{h}`` functions are automatically computed with
1818
[`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl).
1919
20+
!!! warning
21+
See Extended Help if you get an error like:
22+
`MethodError: no method matching (::var"##")(::Vector{ForwardDiff.Dual})`.
23+
2024
## Examples
2125
```jldoctest
2226
julia> model = NonLinModel((x,u,_)->x.^3 + u, (x,_)->x, 0.1, 1, 1, 1);
@@ -27,6 +31,11 @@ julia> linmodel.A
2731
1×1 Matrix{Float64}:
2832
300.0
2933
```
34+
35+
## Extended Help
36+
Automatic differentiation (AD) allows exact Jacobians. The [`NonLinModel`](@ref) `f` and `h`
37+
functions must be compatible with this feature though. See [Automatic differentiation](https://jump.dev/JuMP.jl/stable/manual/nlp/#Automatic-differentiation)
38+
for common mistakes when writing these functions.
3039
"""
3140
function linearize(model::NonLinModel; x=model.x, u=model.uop, d=model.dop)
3241
nu, nx, ny, nd = model.nu, model.nx, model.ny, model.nd

0 commit comments

Comments
 (0)