Skip to content

Commit 2a553ed

Browse files
authored
Merge pull request #105 from JuliaControl/doc_cov
Preparation for v1.0.0 release
2 parents 4dae85d + 9cc320b commit 2a553ed

28 files changed

+577
-285
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ LocalPreferences.toml
55
.vscode/
66
*.cov
77
docs/Manifest.toml
8+
lcov.info

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.24.1"
4+
version = "1.0.0"
55

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

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ using Pkg; Pkg.add("ModelPredictiveControl")
2424
To construct model predictive controllers (MPCs), we must first specify a plant model that
2525
is typically extracted from input-output data using [system identification](https://github.com/baggepinnen/ControlSystemIdentification.jl).
2626
The model here is linear with one input, two outputs and a large time delay in the first
27-
channel:
27+
channel (a transfer function matrix, with $s$ as the Laplace variable):
2828

2929
```math
3030
\mathbf{G}(s) = \frac{\mathbf{y}(s)}{\mathbf{u}(s)} =

docs/Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
55
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
66
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
77
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
8+
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
89
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
910

1011
[compat]
1112
ControlSystemsBase = "1"
13+
DAQP = "0.5"
1214
Documenter = "1"
15+
JuMP = "1"
1316
LinearAlgebra = "1.6"
1417
Logging = "1.6"
18+
Plots = "1"

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ makedocs(
2727
"Examples" => [
2828
"Linear Design" => "manual/linmpc.md",
2929
"Nonlinear Design" => "manual/nonlinmpc.md",
30+
"ModelingToolkit" => "manual/mtk.md",
3031
],
3132
],
3233
"Functions" => [

docs/src/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Pages = [
2929
"manual/installation.md",
3030
"manual/linmpc.md",
3131
"manual/nonlinmpc.md",
32+
"manual/mtk.md"
3233
]
3334
```
3435

docs/src/manual/mtk.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# [Manual: ModelingToolkit Integration](@id man_mtk)
2+
3+
```@contents
4+
Pages = ["mtk.md"]
5+
```
6+
7+
```@setup 1
8+
using Logging; errlogger = ConsoleLogger(stderr, Error);
9+
old_logger = global_logger(); global_logger(errlogger);
10+
```
11+
12+
## Pendulum Model
13+
14+
This example integrates the simple pendulum model of the [last section](@ref man_nonlin) in the
15+
[`ModelingToolkit.jl`](https://docs.sciml.ai/ModelingToolkit/stable/) (MTK) framework and
16+
extracts appropriate `f!` and `h!` functions to construct a [`NonLinModel`](@ref). An
17+
[`NonLinMPC`](@ref) is designed from this model and simulated to reproduce the results of
18+
the last section.
19+
20+
!!! danger "Disclaimer"
21+
This simple example is not an official interface to `ModelingToolkit.jl`. It is provided
22+
as a basic starting template to combine both packages. There is no guarantee that it
23+
will work for all corner cases.
24+
25+
We first construct and instantiate the pendulum model:
26+
27+
```@example 1
28+
using ModelPredictiveControl, ModelingToolkit
29+
using ModelingToolkit: D_nounits as D, t_nounits as t, varmap_to_vars
30+
@mtkmodel Pendulum begin
31+
@parameters begin
32+
g = 9.8
33+
L = 0.4
34+
K = 1.2
35+
m = 0.3
36+
end
37+
@variables begin
38+
θ(t) # state
39+
ω(t) # state
40+
τ(t) # input
41+
y(t) # output
42+
end
43+
@equations begin
44+
D(θ) ~ ω
45+
D(ω) ~ -g/L*sin(θ) - K/m*ω + τ/m/L^2
46+
y ~ θ * 180 / π
47+
end
48+
end
49+
@named mtk_model = Pendulum()
50+
mtk_model = complete(mtk_model)
51+
```
52+
53+
We than convert the MTK model to an [input-output system](https://docs.sciml.ai/ModelingToolkit/stable/basics/InputOutput/):
54+
55+
```@example 1
56+
function generate_f_h(model, inputs, outputs)
57+
(_, f_ip), dvs, psym, io_sys = ModelingToolkit.generate_control_function(
58+
model, inputs, split=false; outputs
59+
)
60+
if any(ModelingToolkit.is_alg_equation, equations(io_sys))
61+
error("Systems with algebraic equations are not supported")
62+
end
63+
h_ = ModelingToolkit.build_explicit_observed_function(io_sys, outputs; inputs = inputs)
64+
nx = length(dvs)
65+
vx = string.(dvs)
66+
par = varmap_to_vars(defaults(io_sys), psym)
67+
function f!(ẋ, x, u, _ , _ )
68+
f_ip(ẋ, x, u, par, 1)
69+
nothing
70+
end
71+
function h!(y, x, _ , _ )
72+
y .= h_(x, 1, par, 1)
73+
nothing
74+
end
75+
return f!, h!, nx, vx
76+
end
77+
inputs, outputs = [mtk_model.τ], [mtk_model.y]
78+
f!, h!, nx, vx = generate_f_h(mtk_model, inputs, outputs)
79+
nu, ny, Ts = length(inputs), length(outputs), 0.1
80+
vu, vy = ["\$τ\$ (Nm)"], ["\$θ\$ (°)"]
81+
nothing # hide
82+
```
83+
84+
A [`NonLinModel`](@ref) can now be constructed:
85+
86+
```@example 1
87+
model = setname!(NonLinModel(f!, h!, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
88+
```
89+
90+
We also instantiate a plant model with a 25 % larger friction coefficient ``K``:
91+
92+
```@example 1
93+
mtk_model.K = defaults(mtk_model)[mtk_model.K] * 1.25
94+
f_plant, h_plant, _, _ = generate_f_h(mtk_model, inputs, outputs)
95+
plant = setname!(NonLinModel(f_plant, h_plant, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
96+
```
97+
98+
## Controller Design
99+
100+
We can than reproduce the Kalman filter and the controller design of the [last section](@ref man_nonlin):
101+
102+
```@example 1
103+
α=0.01; σQ=[0.1, 1.0]; σR=[5.0]; nint_u=[1]; σQint_u=[0.1]
104+
estim = UnscentedKalmanFilter(model; α, σQ, σR, nint_u, σQint_u)
105+
Hp, Hc, Mwt, Nwt = 20, 2, [0.5], [2.5]
106+
nmpc = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt=Inf)
107+
umin, umax = [-1.5], [+1.5]
108+
nmpc = setconstraint!(nmpc; umin, umax)
109+
```
110+
111+
The 180° setpoint response is identical:
112+
113+
```@example 1
114+
using Plots
115+
N = 35
116+
using JuMP; unset_time_limit_sec(nmpc.optim) # hide
117+
res_ry = sim!(nmpc, N, [180.0], plant=plant, x_0=[0, 0], x̂_0=[0, 0, 0])
118+
plot(res_ry)
119+
savefig("plot1_MTK.svg"); nothing # hide
120+
```
121+
122+
![plot1_MTK](plot1_MTK.svg)
123+
124+
and also the output disturbance rejection:
125+
126+
```@example 1
127+
res_yd = sim!(nmpc, N, [180.0], plant=plant, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10])
128+
plot(res_yd)
129+
savefig("plot2_MTK.svg"); nothing # hide
130+
```
131+
132+
![plot2_MTK](plot2_MTK.svg)
133+
134+
## Acknowledgement
135+
136+
Authored by `1-Bart-1` and `baggepinnen`, thanks for the contribution.
137+
138+
```@setup 1
139+
global_logger(old_logger);
140+
```

docs/src/manual/nonlinmpc.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,34 +40,34 @@ The plant model is nonlinear:
4040

4141
in which ``g`` is the gravitational acceleration in m/s², ``L``, the pendulum length in m,
4242
``K``, the friction coefficient at the pivot point in kg/s, and ``m``, the mass attached at
43-
the end of the pendulum in kg. The [`NonLinModel`](@ref) constructor assumes by default
44-
that the state function `f` is continuous in time, that is, an ordinary differential
45-
equation system (like here):
43+
the end of the pendulum in kg, all bundled in the parameter vector ``\mathbf{p} =
44+
[\begin{smallmatrix} g & L & K & m \end{smallmatrix}]'``. The [`NonLinModel`](@ref)
45+
constructor assumes by default that the state function `f` is continuous in time, that is,
46+
an ordinary differential equation system (like here):
4647

4748
```@example 1
4849
using ModelPredictiveControl
49-
function pendulum(par, x, u)
50-
g, L, K, m = par # [m/s²], [m], [kg/s], [kg]
50+
function f(x, u, _ , p)
51+
g, L, K, m = p # [m/s²], [m], [kg/s], [kg]
5152
θ, ω = x[1], x[2] # [rad], [rad/s]
5253
τ = u[1] # [Nm]
5354
dθ = ω
5455
dω = -g/L*sin(θ) - K/m*ω + τ/m/L^2
5556
return [dθ, dω]
5657
end
57-
# declared constants, to avoid type-instability in the f function, for speed:
58-
const par = (9.8, 0.4, 1.2, 0.3)
59-
f(x, u, _ ) = pendulum(par, x, u)
60-
h(x, _ ) = [180/π*x[1]] # [°]
58+
h(x, _ , _ ) = [180/π*x[1]] # [°]
59+
p_model = [9.8, 0.4, 1.2, 0.3]
6160
nu, nx, ny, Ts = 1, 2, 1, 0.1
6261
vu, vx, vy = ["\$τ\$ (Nm)"], ["\$θ\$ (rad)", "\$ω\$ (rad/s)"], ["\$θ\$ (°)"]
63-
model = setname!(NonLinModel(f, h, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
62+
model = setname!(NonLinModel(f, h, Ts, nu, nx, ny; p=p_model); u=vu, x=vx, y=vy)
6463
```
6564

6665
The output function ``\mathbf{h}`` converts the ``θ`` angle to degrees. Note that special
6766
characters like ``θ`` can be typed in the Julia REPL or VS Code by typing `\theta` and
68-
pressing the `<TAB>` key. The tuple `par` is constant here to improve the [performance](https://docs.julialang.org/en/v1/manual/performance-tips/#Avoid-untyped-global-variables).
69-
A 4th order [`RungeKutta`](@ref) method solves the differential equations by default. It is
70-
good practice to first simulate `model` using [`sim!`](@ref) as a quick sanity check:
67+
pressing the `<TAB>` key. Note that the parameter `p` can be of any type but use a mutable
68+
type like a vector of you want to modify it later. A 4th order [`RungeKutta`](@ref) method
69+
solves the differential equations by default. It is good practice to first simulate `model`
70+
using [`sim!`](@ref) as a quick sanity check:
7171

7272
```@example 1
7373
using Plots
@@ -101,9 +101,9 @@ motor torque ``τ``, with an associated standard deviation `σQint_u` of 0.1 N m
101101
estimator tuning is tested on a plant with a 25 % larger friction coefficient ``K``:
102102

103103
```@example 1
104-
const par_plant = (par[1], par[2], 1.25*par[3], par[4])
105-
f_plant(x, u, _ ) = pendulum(par_plant, x, u)
106-
plant = setname!(NonLinModel(f_plant, h, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
104+
p_plant = copy(p_model)
105+
p_plant[3] = 1.25*p_model[3]
106+
plant = setname!(NonLinModel(f, h, Ts, nu, nx, ny; p=p_plant); u=vu, x=vx, y=vy)
107107
res = sim!(estim, N, [0.5], plant=plant, y_noise=[0.5])
108108
plot(res, plotu=false, plotxwithx̂=true)
109109
savefig("plot2_NonLinMPC.svg"); nothing # hide
@@ -182,10 +182,10 @@ angle ``θ`` is measured here). As the arguments of [`NonLinMPC`](@ref) economic
182182
Kalman Filter similar to the previous one (``\mathbf{y^m} = θ`` and ``\mathbf{y^u} = ω``):
183183

184184
```@example 1
185-
h2(x, _ ) = [180/π*x[1], x[2]]
185+
h2(x, _ , _ ) = [180/π*x[1], x[2]]
186186
nu, nx, ny = 1, 2, 2
187-
model2 = setname!(NonLinModel(f , h2, Ts, nu, nx, ny), u=vu, x=vx, y=[vy; vx[2]])
188-
plant2 = setname!(NonLinModel(f_plant, h2, Ts, nu, nx, ny), u=vu, x=vx, y=[vy; vx[2]])
187+
model2 = setname!(NonLinModel(f, h2, Ts, nu, nx, ny; p=p_model), u=vu, x=vx, y=[vy; vx[2]])
188+
plant2 = setname!(NonLinModel(f, h2, Ts, nu, nx, ny; p=p_plant), u=vu, x=vx, y=[vy; vx[2]])
189189
estim2 = UnscentedKalmanFilter(model2; σQ, σR, nint_u, σQint_u, i_ym=[1])
190190
```
191191

@@ -194,11 +194,12 @@ output vector of `plant` argument corresponds to the model output vector in `mpc
194194
We can now define the ``J_E`` function and the `empc` controller:
195195

196196
```@example 1
197-
function JE(UE, ŶE, _ )
197+
function JE(UE, ŶE, _ , p)
198+
Ts = p
198199
τ, ω = UE[1:end-1], ŶE[2:2:end-1]
199200
return Ts*sum(τ.*ω)
200201
end
201-
empc = NonLinMPC(estim2; Hp, Hc, Nwt, Mwt=[0.5, 0], Cwt=Inf, Ewt=3.5e3, JE=JE)
202+
empc = NonLinMPC(estim2; Hp, Hc, Nwt, Mwt=[0.5, 0], Cwt=Inf, Ewt=3.5e3, JE=JE, p=Ts)
202203
empc = setconstraint!(empc; umin, umax)
203204
```
204205

src/controller/execute.jl

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ The function should be called after calling [`moveinput!`](@ref). It returns the
8989
- `:Ŷs` or *`:Yhats`* : predicted stochastic output over ``H_p`` of [`InternalModel`](@ref), ``\mathbf{Ŷ_s}``
9090
- `:R̂y` or *`:Rhaty`* : predicted output setpoint over ``H_p``, ``\mathbf{R̂_y}``
9191
- `:R̂u` or *`:Rhatu`* : predicted manipulated input setpoint over ``H_p``, ``\mathbf{R̂_u}``
92-
- `:x̂end` or *`:xhatend`* : optimal terminal states, ``\mathbf{x̂}_{k-1}(k+H_p)``
92+
- `:x̂end` or *`:xhatend`* : optimal terminal states, ``\mathbf{x̂}_i(k+H_p)``
9393
- `:J` : objective value optimum, ``J``
9494
- `:U` : optimal manipulated inputs over ``H_p``, ``\mathbf{U}``
9595
- `:u` : current optimal manipulated input, ``\mathbf{u}(k)``
@@ -369,19 +369,7 @@ function obj_nonlinprog!(
369369
U0, Ȳ, _ , mpc::PredictiveController, model::LinModel, Ŷ0, ΔŨ::AbstractVector{NT}
370370
) where NT <: Real
371371
J = obj_quadprog(ΔŨ, mpc.H̃, mpc.q̃) + mpc.r[]
372-
if !iszero(mpc.E)
373-
ŷ, D̂E = mpc.ŷ, mpc.D̂E
374-
U = U0
375-
U .+= mpc.Uop
376-
uend = @views U[(end-model.nu+1):end]
377-
=
378-
Ŷ .= Ŷ0 .+ mpc.Yop
379-
UE = [U; uend]
380-
ŶE = [ŷ; Ŷ]
381-
E_JE = mpc.E*mpc.JE(UE, ŶE, D̂E)
382-
else
383-
E_JE = 0.0
384-
end
372+
E_JE = obj_econ!(U0, Ȳ, mpc, model, Ŷ0, ΔŨ)
385373
return J + E_JE
386374
end
387375

@@ -413,22 +401,13 @@ function obj_nonlinprog!(
413401
JR̂u = 0.0
414402
end
415403
# --- economic term ---
416-
if !iszero(mpc.E)
417-
ny, Hp, ŷ, D̂E = model.ny, mpc.Hp, mpc.ŷ, mpc.D̂E
418-
U = U0
419-
U .+= mpc.Uop
420-
uend = @views U[(end-model.nu+1):end]
421-
=
422-
Ŷ .= Ŷ0 .+ mpc.Yop
423-
UE = [U; uend]
424-
ŶE = [ŷ; Ŷ]
425-
E_JE = mpc.E*mpc.JE(UE, ŶE, D̂E)
426-
else
427-
E_JE = 0.0
428-
end
404+
E_JE = obj_econ!(U0, Ȳ, mpc, model, Ŷ0, ΔŨ)
429405
return JR̂y + JΔŨ + JR̂u + E_JE
430406
end
431407

408+
"By default, the economic term is zero."
409+
obj_econ!( _ , _ , ::PredictiveController, ::SimModel, _ , _ ) = 0.0
410+
432411
@doc raw"""
433412
optim_objective!(mpc::PredictiveController) -> ΔŨ
434413

src/controller/explicitmpc.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ function ExplicitMPC(
167167
return ExplicitMPC{NT, SE}(estim, Hp, Hc, M_Hp, N_Hc, L_Hp)
168168
end
169169

170-
setconstraint!(::ExplicitMPC,kwargs...) = error("ExplicitMPC does not support constraints.")
170+
setconstraint!(::ExplicitMPC; kwargs...) = error("ExplicitMPC does not support constraints.")
171171

172172
function Base.show(io::IO, mpc::ExplicitMPC)
173173
Hp, Hc = mpc.Hp, mpc.Hc

0 commit comments

Comments
 (0)