Skip to content

Commit 2477c4f

Browse files
authored
Merge pull request #307 from JuliaControl/extension
🚀 added: C code generation via `LinearMPC.jl`
2 parents e210402 + 8b5e6b3 commit 2477c4f

13 files changed

Lines changed: 358 additions & 15 deletions

File tree

Project.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ModelPredictiveControl"
22
uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c"
3-
version = "1.14.4"
3+
version = "1.15.0"
44
authors = ["Francis Gagnon"]
55

66
[deps]
@@ -22,6 +22,12 @@ SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5"
2222
SparseMatrixColorings = "0a514795-09f3-496d-8182-132a7b665d35"
2323
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
2424

25+
[weakdeps]
26+
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"
27+
28+
[extensions]
29+
LinearMPCext = "LinearMPC"
30+
2531
[compat]
2632
ControlSystemsBase = "1.18.2"
2733
DAQP = "0.6, 0.7.1"
@@ -32,6 +38,7 @@ ForwardDiff = "0.10, 1"
3238
Ipopt = "1"
3339
JuMP = "1.21"
3440
LinearAlgebra = "1.10"
41+
LinearMPC = "0.8.0"
3542
Logging = "1.10"
3643
MathOptInterface = "1.46"
3744
OSQP = "0.8"
@@ -52,10 +59,11 @@ julia = "1.10"
5259
DAQP = "c47d62df-3981-49c8-9651-128b1cd08617"
5360
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
5461
FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41"
62+
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"
5563
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
5664
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
5765
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
5866
TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe"
5967

6068
[targets]
61-
test = ["Test", "TestItems", "TestItemRunner", "Documenter", "Plots", "DAQP", "FiniteDiff"]
69+
test = ["Test", "TestItems", "TestItemRunner", "Documenter", "Plots", "DAQP", "FiniteDiff", "LinearMPC"]

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ for more detailed examples.
9191
- 📝 **Transcription**: Direct single/multiple shooting and trapezoidal collocation.
9292
- 🩺 **Troubleshooting**: Detailed diagnostic information about optimum.
9393
- ⏱️ **Real-Time**: Optimized for low memory allocations with soft real-time utilities.
94+
- 📟️ **Embedded**: Lightweight C code generation via `LinearMPC.jl`
9495

9596
### 🔭 State Estimation Features
9697

docs/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
55
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
66
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
8+
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"
89
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
910
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
1011
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
@@ -15,6 +16,7 @@ DAQP = "0.6, 0.7.1"
1516
Documenter = "1"
1617
JuMP = "1"
1718
LinearAlgebra = "1.10"
19+
LinearMPC = "0.8.0"
1820
Logging = "1.10"
1921
ModelingToolkit = "10"
2022
Plots = "1"

docs/make.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ push!(LOAD_PATH,"../src/")
55

66
using Documenter, DocumenterInterLinks
77
using ModelPredictiveControl
8+
import LinearMPC
89

910
links = InterLinks(
1011
"Julia" => "https://docs.julialang.org/en/v1/objects.inv",
@@ -14,6 +15,7 @@ links = InterLinks(
1415
"DifferentiationInterface" => "https://juliadiff.org/DifferentiationInterface.jl/DifferentiationInterface/stable/objects.inv",
1516
"ForwardDiff" => "https://juliadiff.org/ForwardDiff.jl/stable/objects.inv",
1617
"LowLevelParticleFilters" => "https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/objects.inv",
18+
"LinearMPC" => "https://darnstrom.github.io/LinearMPC.jl/stable/objects.inv",
1719
)
1820

1921
DocMeta.setdocmeta!(

docs/src/manual/linmpc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ For the CSTR, we will bound the innovation term ``\mathbf{y}(k) - \mathbf{ŷ}(k
201201
estim = MovingHorizonEstimator(model, He=10, nint_u=[1, 1], σQint_u = [1, 2])
202202
estim = setconstraint!(estim, v̂min=[-1, -0.5], v̂max=[+1, +0.5])
203203
mpc_mhe = LinMPC(estim, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1])
204-
mpc_mhe = setconstraint!(mpc_mhe, ymin=[45, -Inf])
204+
mpc_mhe = setconstraint!(mpc_mhe, ymin=[48, -Inf])
205205
```
206206

207207
The rejection is indeed improved:

docs/src/public/predictive_control.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ PredictiveController
7272
LinMPC
7373
```
7474

75+
### Conversion to LinearMPC.jl (code generation)
76+
77+
```@docs
78+
LinearMPC.MPC
79+
```
80+
7581
## ExplicitMPC
7682

7783
```@docs
@@ -114,4 +120,4 @@ MultipleShooting
114120

115121
```@docs
116122
TrapezoidalCollocation
117-
```
123+
```

ext/LinearMPCext.jl

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
module LinearMPCext
2+
3+
using ModelPredictiveControl
4+
using LinearAlgebra, SparseArrays
5+
using JuMP
6+
7+
import LinearMPC
8+
import ModelPredictiveControl: isblockdiag
9+
10+
function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC)
11+
model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights
12+
nu, ny, nx̂ = model.nu, model.ny, estim.nx̂
13+
Hp, Hc = mpc.Hp, mpc.Hc
14+
nΔU = Hc * nu
15+
validate_compatibility(mpc)
16+
# --- Model parameters ---
17+
F, G, Gd = estim.Â, estim.B̂u, estim.B̂d
18+
C, Dd = estim.Ĉ, estim.D̂d
19+
Np = Hp
20+
Nc = Hc
21+
newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc)
22+
# --- Operating points ---
23+
uoff = model.uop
24+
doff = model.dop
25+
yoff = model.yop
26+
xoff = estim.x̂op
27+
foff = estim.f̂op
28+
LinearMPC.set_offset!(newmpc; uo=uoff, ho=yoff, doff=doff, xo=xoff, fo=foff)
29+
# --- State observer parameters ---
30+
Q, R = estim.cov.Q̂, estim.cov.
31+
LinearMPC.set_state_observer!(newmpc; C=estim.Ĉm, Q, R)
32+
# --- Objective function weights ---
33+
Q = weights.M_Hp[1:ny, 1:ny]
34+
Qf = weights.M_Hp[end-ny+1:end, end-ny+1:end]
35+
Rr = weights.Ñ_Hc[1:nu, 1:nu]
36+
R = weights.L_Hp[1:nu, 1:nu]
37+
LinearMPC.set_objective!(newmpc; Q, Rr, R, Qf)
38+
# --- Custom move blocking ---
39+
LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged
40+
# ---- Constraint softening ---
41+
only_hard = weights.isinf_C
42+
if !only_hard
43+
issoft(C) = any(x -> x > 0, C)
44+
C_u = -mpc.con.A_Umin[:, end]
45+
C_Δu = -mpc.con.A_ΔŨmin[1:nΔU, end]
46+
C_y = -mpc.con.A_Ymin[:, end]
47+
c_x̂ = -mpc.con.A_x̂min[:, end]
48+
if sum(mpc.con.i_b) > 1 # ignore the slack variable ϵ bound
49+
if issoft(C_u) || issoft(C_Δu) || issoft(C_y) || issoft(C_x̂)
50+
@warn "The LinearMPC conversion is approximate for the soft constraints.\n"*
51+
"You may need to adjust the soft_weight field of the "*
52+
"LinearMPC.MPC object to replicate behaviors."
53+
end
54+
end
55+
# LinearMPC relies on a different softening mechanism (new implicit slacks for each
56+
# softened bounds), so we apply an approximate conversion factor on the Cwt weight:
57+
Cwt = weights.Ñ_Hc[end, end]
58+
nsoft = sum((mpc.con.A[:,end] .< 0) .& (mpc.con.i_b)) - 1
59+
newmpc.settings.soft_weight = 10*sqrt(nsoft*Cwt)
60+
else
61+
C_u = zeros(nu*Hp)
62+
C_Δu = zeros(nu*Hc)
63+
C_y = zeros(ny*Hp)
64+
c_x̂ = zeros(nx̂)
65+
end
66+
# --- Manipulated inputs constraints ---
67+
Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop
68+
I_u = Matrix{Float64}(I, nu, nu)
69+
# add_constraint! does not support u bounds pass the control horizon Hc
70+
# so we compute the extremum bounds from k=Hc-1 to Hp, and apply them at k=Hc-1
71+
Umin_finals = reshape(Umin[nu*(Hc-1)+1:end], nu, :)
72+
Umax_finals = reshape(Umax[nu*(Hc-1)+1:end], nu, :)
73+
umin_end = mapslices(maximum, Umin_finals; dims=2)
74+
umax_end = mapslices(minimum, Umax_finals; dims=2)
75+
for k in 0:Hc-1
76+
if k < Hc - 1
77+
umin_k, umax_k = Umin[k*nu+1:(k+1)*nu], Umax[k*nu+1:(k+1)*nu]
78+
else
79+
umin_k, umax_k = umin_end, umax_end
80+
end
81+
c_u_k = C_u[k*nu+1:(k+1)*nu]
82+
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
83+
for i in 1:nu
84+
lb = isfinite(umin_k[i]) ? [umin_k[i]] : zeros(0)
85+
ub = isfinite(umax_k[i]) ? [umax_k[i]] : zeros(0)
86+
soft = !only_hard && c_u_k[i] > 0
87+
Au = I_u[i:i, :]
88+
LinearMPC.add_constraint!(newmpc; Au, lb, ub, ks, soft)
89+
end
90+
end
91+
# --- Input increment constraints ---
92+
ΔUmin, ΔUmax = mpc.con.ΔŨmin[1:nΔU], mpc.con.ΔŨmax[1:nΔU]
93+
I_Δu = Matrix{Float64}(I, nu, nu)
94+
for k in 0:Hc-1
95+
Δumin_k, Δumax_k = ΔUmin[k*nu+1:(k+1)*nu], ΔUmax[k*nu+1:(k+1)*nu]
96+
c_Δu_k = C_Δu[k*nu+1:(k+1)*nu]
97+
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
98+
for i in 1:nu
99+
lb = isfinite(Δumin_k[i]) ? [Δumin_k[i]] : zeros(0)
100+
ub = isfinite(Δumax_k[i]) ? [Δumax_k[i]] : zeros(0)
101+
soft = !only_hard && c_Δu_k[i] > 0
102+
Au, Aup = I_Δu[i:i, :], -I_Δu[i:i, :]
103+
LinearMPC.add_constraint!(newmpc; Au, Aup, lb, ub, ks, soft)
104+
end
105+
end
106+
# --- Output constraints ---
107+
Y0min, Y0max = mpc.con.Y0min, mpc.con.Y0max
108+
for k in 1:Hp
109+
ymin_k, ymax_k = Y0min[(k-1)*ny+1:k*ny], Y0max[(k-1)*ny+1:k*ny]
110+
c_y_k = C_y[(k-1)*ny+1:k*ny]
111+
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
112+
for i in 1:ny
113+
lb = isfinite(ymin_k[i]) ? [ymin_k[i]] : zeros(0)
114+
ub = isfinite(ymax_k[i]) ? [ymax_k[i]] : zeros(0)
115+
soft = !only_hard && c_y_k[i] > 0
116+
Ax, Ad = C[i:i, :], Dd[i:i, :]
117+
LinearMPC.add_constraint!(newmpc; Ax, Ad, lb, ub, ks, soft)
118+
end
119+
end
120+
# --- Terminal constraints ---
121+
x̂0min, x̂0max = mpc.con.x̂0min, mpc.con.x̂0max
122+
I_x̂ = Matrix{Float64}(I, nx̂, nx̂)
123+
ks = [Hp + 1] # a `1` in ks argument corresponds to the present time step k+0
124+
for i in 1:nx̂
125+
lb = isfinite(x̂0min[i]) ? [x̂0min[i]] : zeros(0)
126+
ub = isfinite(x̂0max[i]) ? [x̂0max[i]] : zeros(0)
127+
soft = !only_hard && c_x̂[i] > 0
128+
Ax = I_x̂[i:i, :]
129+
LinearMPC.add_constraint!(newmpc; Ax, lb, ub, ks, soft)
130+
end
131+
return newmpc
132+
end
133+
134+
function validate_compatibility(mpc::ModelPredictiveControl.LinMPC)
135+
if mpc.transcription isa MultipleShooting
136+
error("LinearMPC only supports SingleShooting transcription.")
137+
end
138+
if !(mpc.estim isa SteadyKalmanFilter) || !mpc.estim.direct
139+
error("LinearMPC only supports SteadyKalmanFilter with direct=true option.")
140+
end
141+
if JuMP.solver_name(mpc.optim) != "DAQP"
142+
@warn "LinearMPC relies on DAQP, and the solver in the mpc object " *
143+
"is currently $(JuMP.solver_name(mpc.optim)).\n" *
144+
"The results in closed-loop may be different."
145+
end
146+
validate_weights(mpc)
147+
validate_constraints(mpc)
148+
return nothing
149+
end
150+
151+
function validate_weights(mpc::ModelPredictiveControl.LinMPC)
152+
ny, nu = mpc.estim.model.ny, mpc.estim.model.nu
153+
Hp, Hc = mpc.Hp, mpc.Hc
154+
nΔU = Hc * nu
155+
M_Hp, N_Hc, L_Hp = mpc.weights.M_Hp, mpc.weights.Ñ_Hc[1:nΔU, 1:nΔU], mpc.weights.L_Hp
156+
M_1, N_1, L_1 = M_Hp[1:ny, 1:ny], N_Hc[1:nu, 1:nu], L_Hp[1:nu, 1:nu]
157+
for i in 2:mpc.Hp-1 # last block is terminal weight, can be different
158+
M_i = M_Hp[(i-1)*ny+1:i*ny, (i-1)*ny+1:i*ny]
159+
if !isapprox(M_i, M_1)
160+
error("LinearMPC only supports identical weights for each stages in M_Hp.")
161+
end
162+
end
163+
isblockdiag(M_Hp, ny, Hp) || error("M_Hp must be block diagonal.")
164+
for i in 2:mpc.Hc
165+
N_i = N_Hc[(i-1)*nu+1:i*nu, (i-1)*nu+1:i*nu]
166+
if !isapprox(N_i, N_1)
167+
error("LinearMPC only supports identical weights for each stages in Ñ_Hc.")
168+
end
169+
end
170+
isblockdiag(N_Hc, nu, Hc) || error("Ñ_Hc must be block diagonal.")
171+
for i in 2:mpc.Hp
172+
L_i = L_Hp[(i-1)*nu+1:i*nu, (i-1)*nu+1:i*nu]
173+
if !isapprox(L_i, L_1)
174+
error("LinearMPC only supports identical weights for each stages in L_Hp.")
175+
end
176+
end
177+
isblockdiag(L_Hp, nu, Hp) || error("L_Hp must be block diagonal.")
178+
return nothing
179+
end
180+
181+
function validate_constraints(mpc::ModelPredictiveControl.LinMPC)
182+
nΔU = mpc.Hc * mpc.estim.model.nu
183+
mpc.weights.isinf_C && return nothing # only hard constraints are entirely supported
184+
C_umin, C_umax = -mpc.con.A_Umin[:, end], -mpc.con.A_Umax[:, end]
185+
C_Δumin, C_Δumax = -mpc.con.A_ΔŨmin[1:nΔU, end], -mpc.con.A_ΔŨmax[1:nΔU, end]
186+
C_ymin, C_ymax = -mpc.con.A_Ymin[:, end], -mpc.con.A_Ymax[:, end]
187+
C_x̂min, C_x̂max = -mpc.con.A_x̂min[:, end], -mpc.con.A_x̂max[:, end]
188+
is0or1(C) = all(x -> x 0 || x 1, C)
189+
if (
190+
!is0or1(C_umin) || !is0or1(C_umax) ||
191+
!is0or1(C_Δumin) || !is0or1(C_Δumax) ||
192+
!is0or1(C_ymin) || !is0or1(C_ymax) ||
193+
!is0or1(C_x̂min) || !is0or1(C_x̂max)
194+
195+
)
196+
error("LinearMPC only supports softness parameters c = 0 or 1.")
197+
end
198+
if (
199+
!isapprox(C_umin, C_umax) ||
200+
!isapprox(C_Δumin, C_Δumax) ||
201+
!isapprox(C_ymin, C_ymax) ||
202+
!isapprox(C_x̂min, C_x̂max)
203+
)
204+
error("LinearMPC only supports identical softness parameters for lower and upper bounds.")
205+
end
206+
return nothing
207+
end
208+
209+
@doc raw"""
210+
LinearMPC.MPC(mpc::LinMPC)
211+
212+
Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object.
213+
214+
The `LinearMPC` package needs to be installed and available in the activated Julia
215+
environment. The converted object can be used to generate lightweight C-code for embedded
216+
applications using the `LinearMPC.codegen` function. Note that not all features of [`LinMPC`](@ref)
217+
are supported, including these restrictions:
218+
219+
- the solver is limited to [`DAQP`](https://darnstrom.github.io/daqp/).
220+
- the transcription method must be [`SingleShooting`](@ref).
221+
- the state estimator must be a [`SteadyKalmanFilter`](@ref) with `direct=true`.
222+
- only block-diagonal weights are allowed.
223+
- the constraint relaxation mechanism is different, so a 1-on-1 conversion of the soft
224+
constraints is impossible (use `Cwt=Inf` to disable relaxation).
225+
226+
But the package has also several exclusive functionalities, such as pre-stabilization,
227+
constrained explicit MPC, and binary manipulated inputs. See the [`LinearMPC.jl`](@extref LinearMPC)
228+
documentation for more details on the supported features and how to generate code.
229+
230+
# Examples
231+
```jldoctest
232+
julia> import LinearMPC, JuMP, DAQP;
233+
234+
julia> mpc1 = LinMPC(LinModel(tf(2, [10, 1]), 1.0); optim=JuMP.Model(DAQP.Optimizer));
235+
236+
julia> preparestate!(mpc1, [1.0]);
237+
238+
julia> u = moveinput!(mpc1, [10.0]); round.(u, digits=6)
239+
1-element Vector{Float64}:
240+
17.577311
241+
242+
julia> mpc2 = LinearMPC.MPC(mpc1);
243+
244+
julia> x̂ = LinearMPC.correct_state!(mpc2, [1.0]);
245+
246+
julia> u = LinearMPC.compute_control(mpc2, x̂, r=[10.0]); round.(u, digits=6)
247+
1-element Vector{Float64}:
248+
17.577311
249+
```
250+
"""
251+
LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc)
252+
253+
254+
end # LinearMPCext

0 commit comments

Comments
 (0)