Skip to content

Commit 7295588

Browse files
authored
Merge pull request #315 from JuliaControl/custom_linconstraint_Cgen
added: C codegen support for `LinMPC` custom linear inequality constraints
2 parents 8a20fee + 4c6f86e commit 7295588

File tree

3 files changed

+175
-30
lines changed

3 files changed

+175
-30
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
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.16.0"
3+
version = "1.16.1"
44
authors = ["Francis Gagnon"]
55

66
[deps]

ext/LinearMPCext.jl

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import ModelPredictiveControl: isblockdiag
99

1010
function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC)
1111
model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights
12-
nu, ny, nx̂ = model.nu, model.ny, estim.nx̂
12+
nu, ny, nd, nx̂, nw = model.nu, model.ny, model.nd, estim.nx̂, mpc.con.nw
1313
Hp, Hc = mpc.Hp, mpc.Hc
1414
nΔU = Hc * nu
1515
validate_compatibility(mpc)
@@ -36,17 +36,18 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC)
3636
R = weights.L_Hp[1:nu, 1:nu]
3737
LinearMPC.set_objective!(newmpc; Q, Rr, R, Qf)
3838
# --- Custom move blocking ---
39-
LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged
39+
LinearMPC.move_block!(newmpc, mpc.nb)
4040
# ---- Constraint softening ---
4141
only_hard = weights.isinf_C
4242
if !only_hard
4343
issoft(C) = any(x -> x > 0, C)
4444
C_u = -mpc.con.A_Umin[:, end]
4545
C_Δu = -mpc.con.A_ΔŨmin[1:nΔU, end]
4646
C_y = -mpc.con.A_Ymin[:, end]
47+
C_w = -mpc.con.A_Wmin[:, end]
4748
c_x̂ = -mpc.con.A_x̂min[:, end]
4849
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+
if issoft(C_u) || issoft(C_Δu) || issoft(C_y) || issoft(C_w) || issoft(C_x̂)
5051
@warn "The LinearMPC conversion is approximate for the soft constraints.\n"*
5152
"You may need to adjust the soft_weight field of the "*
5253
"LinearMPC.MPC object to replicate behaviors."
@@ -61,23 +62,14 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC)
6162
C_u = zeros(nu*Hp)
6263
C_Δu = zeros(nu*Hc)
6364
C_y = zeros(ny*Hp)
65+
C_w = zeros(nw*(Hp+1))
6466
c_x̂ = zeros(nx̂)
6567
end
6668
# --- Manipulated inputs constraints ---
6769
Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop
6870
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
71+
for k = 0:Hp-1
72+
umin_k, umax_k = Umin[k*nu+1:(k+1)*nu], Umax[k*nu+1:(k+1)*nu]
8173
c_u_k = C_u[k*nu+1:(k+1)*nu]
8274
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
8375
for i in 1:nu
@@ -91,7 +83,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC)
9183
# --- Input increment constraints ---
9284
ΔUmin, ΔUmax = mpc.con.ΔŨmin[1:nΔU], mpc.con.ΔŨmax[1:nΔU]
9385
I_Δu = Matrix{Float64}(I, nu, nu)
94-
for k in 0:Hc-1
86+
for k = 0:Hc-1
9587
Δumin_k, Δumax_k = ΔUmin[k*nu+1:(k+1)*nu], ΔUmax[k*nu+1:(k+1)*nu]
9688
c_Δu_k = C_Δu[k*nu+1:(k+1)*nu]
9789
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
@@ -105,7 +97,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC)
10597
end
10698
# --- Output constraints ---
10799
Y0min, Y0max = mpc.con.Y0min, mpc.con.Y0max
108-
for k in 1:Hp
100+
for k = 1:Hp
109101
ymin_k, ymax_k = Y0min[(k-1)*ny+1:k*ny], Y0max[(k-1)*ny+1:k*ny]
110102
c_y_k = C_y[(k-1)*ny+1:k*ny]
111103
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
@@ -117,6 +109,30 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC)
117109
LinearMPC.add_constraint!(newmpc; Ax, Ad, lb, ub, ks, soft)
118110
end
119111
end
112+
# --- Custom linear constraints ---
113+
Wmin, Wmax = mpc.con.Wmin, mpc.con.Wmax
114+
Wy = mpc.con.W̄y[1:nw, 1:ny]
115+
Wu = mpc.con.W̄u[1:nw, 1:nu]
116+
Wd = mpc.con.W̄d[1:nw, 1:nd]
117+
Wr = mpc.con.W̄r[1:nw, 1:ny]
118+
for k in 0:Hp
119+
wmin_k, wmax_k = Wmin[k*nw+1:(k+1)*nw], Wmax[k*nw+1:(k+1)*nw]
120+
c_w_k = C_w[k*nw+1:(k+1)*nw]
121+
ks = [k + 1]
122+
for i in 1:nw
123+
Wy_i, Wu_i, Wd_i, Wr_i = Wy[i:i, :], Wu[i:i, :], Wd[i:i, :], Wr[i:i, :]
124+
lb_k_i = wmin_k[i:i] - Wy_i*yoff
125+
ub_k_i = wmax_k[i:i] - Wy_i*yoff
126+
lb = isfinite(lb_k_i[]) ? lb_k_i : zeros(0)
127+
ub = isfinite(ub_k_i[]) ? ub_k_i : zeros(0)
128+
soft = !only_hard && c_w_k[i] > 0
129+
Ax = Wy_i*C
130+
Au = Wu_i
131+
Ad = Wy_i*Dd + Wd_i
132+
Ar = Wr_i
133+
LinearMPC.add_constraint!(newmpc; Ax, Au, Ad, Ar, lb, ub, ks, soft)
134+
end
135+
end
120136
# --- Terminal constraints ---
121137
x̂0min, x̂0max = mpc.con.x̂0min, mpc.con.x̂0max
122138
I_x̂ = Matrix{Float64}(I, nx̂, nx̂)
@@ -180,29 +196,32 @@ end
180196

181197
function validate_constraints(mpc::ModelPredictiveControl.LinMPC)
182198
nΔU = mpc.Hc * mpc.estim.model.nu
183-
mpc.con.nw > 0 && error("Conversion of custom linear inequality constraints is not supported for now.")
184199
mpc.weights.isinf_C && return nothing # only hard constraints are entirely supported
185200
C_umin, C_umax = -mpc.con.A_Umin[:, end], -mpc.con.A_Umax[:, end]
186201
C_Δumin, C_Δumax = -mpc.con.A_ΔŨmin[1:nΔU, end], -mpc.con.A_ΔŨmax[1:nΔU, end]
187202
C_ymin, C_ymax = -mpc.con.A_Ymin[:, end], -mpc.con.A_Ymax[:, end]
203+
C_wmin, C_wmax = -mpc.con.A_Wmin[:, end], -mpc.con.A_Wmax[:, end]
188204
C_x̂min, C_x̂max = -mpc.con.A_x̂min[:, end], -mpc.con.A_x̂max[:, end]
205+
if (
206+
!isapprox(C_umin, C_umax) ||
207+
!isapprox(C_Δumin, C_Δumax) ||
208+
!isapprox(C_ymin, C_ymax) ||
209+
!isapprox(C_wmin, C_wmax) ||
210+
!isapprox(C_x̂min, C_x̂max)
211+
)
212+
error("LinearMPC only supports identical softness parameters for lower and upper bounds.")
213+
end
189214
is0or1(C) = all(x -> x 0 || x 1, C)
190215
if (
191216
!is0or1(C_umin) || !is0or1(C_umax) ||
192217
!is0or1(C_Δumin) || !is0or1(C_Δumax) ||
193-
!is0or1(C_ymin) || !is0or1(C_ymax) ||
218+
!is0or1(C_ymin) || !is0or1(C_ymax) ||
219+
!is0or1(C_wmin) || !is0or1(C_wmax) ||
194220
!is0or1(C_x̂min) || !is0or1(C_x̂max)
195221

196222
)
197-
error("LinearMPC only supports softness parameters c = 0 or 1.")
198-
end
199-
if (
200-
!isapprox(C_umin, C_umax) ||
201-
!isapprox(C_Δumin, C_Δumax) ||
202-
!isapprox(C_ymin, C_ymax) ||
203-
!isapprox(C_x̂min, C_x̂max)
204-
)
205-
error("LinearMPC only supports identical softness parameters for lower and upper bounds.")
223+
@warn "LinearMPC only supports softness parameters c = 0 or 1.\n"*
224+
"All constraints with c > 0 will be considered soft."
206225
end
207226
return nothing
208227
end

test/5_test_extensions.jl

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@testitem "LinearMPCext extension" setup=[SetupMPCtests] begin
1+
@testitem "LinearMPCext general" setup=[SetupMPCtests] begin
22
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, DAQP
33
import LinearMPC
44
model = LinModel(sys, Ts, i_u=1:2)
@@ -50,4 +50,130 @@
5050
"OSQP.\nThe results in closed-loop may be different."),
5151
LinearMPC.MPC(mpc_osqp)
5252
)
53+
54+
end
55+
56+
@testitem "LinearMPCext with Wy weight" setup=[SetupMPCtests] begin
57+
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, DAQP
58+
import LinearMPC
59+
model = LinModel(tf([2], [10, 1]), 3.0)
60+
model = setop!(model, yop=[50], uop=[20])
61+
optim = JuMP.Model(DAQP.Optimizer)
62+
mpc1 = LinMPC(model, Hp=20, Hc=5, Wy=[1], optim=optim)
63+
mpc1 = setconstraint!(mpc1, wmax=[55])
64+
mpc2 = LinearMPC.MPC(mpc1)
65+
function sim_wy(model, mpc1, mpc2, N)
66+
r = [60.0]
67+
u1 = [20.0]
68+
u2 = [20.0]
69+
model.x0 .= 0
70+
u_data1, u_data2 = zeros(1, N), zeros(1, N)
71+
for k in 0:N-1
72+
y = model()
73+
= preparestate!(mpc1, y)
74+
u1 = moveinput!(mpc1, r, lastu=u1)
75+
u2 = LinearMPC.compute_control(mpc2, x̂, r=r, uprev=u2)
76+
u_data1[:, k+1], u_data2[:, k+1] = u1, u2
77+
updatestate!(model, u1)
78+
updatestate!(mpc1, u1, y)
79+
end
80+
return u_data1, u_data2
81+
end
82+
N = 30
83+
u_data1, u_data2 = sim_wy(model, mpc1, mpc2, N)
84+
@test u_data1 u_data2 atol=1e-2 rtol=1e-2
85+
end
86+
87+
@testitem "LinearMPCext with Wu weight" setup=[SetupMPCtests] begin
88+
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, DAQP
89+
import LinearMPC
90+
model = LinModel(tf([2], [10, 1]), 3.0)
91+
model = setop!(model, uop=[20], yop=[50])
92+
optim = JuMP.Model(DAQP.Optimizer)
93+
mpc1 = LinMPC(model, Nwt=[0], Hp=250, Hc=1, Wu=[1], optim=optim)
94+
mpc1 = setconstraint!(mpc1, wmin=[19.0])
95+
mpc2 = LinearMPC.MPC(mpc1)
96+
function sim_wu(model, mpc1, mpc2, N)
97+
r = [40.0]
98+
u1 = [20.0]
99+
u2 = [20.0]
100+
model.x0 .= 0
101+
u_data1, u_data2 = zeros(1, N), zeros(1, N)
102+
for k in 0:N-1
103+
y = model()
104+
= preparestate!(mpc1, y)
105+
u1 = moveinput!(mpc1, r, lastu=u1)
106+
u2 = LinearMPC.compute_control(mpc2, x̂, r=r, uprev=u2)
107+
u_data1[:, k+1], u_data2[:, k+1] = u1, u2
108+
updatestate!(model, u1)
109+
updatestate!(mpc1, u1, y)
110+
end
111+
return u_data1, u_data2
112+
end
113+
N = 30
114+
u_data1, u_data2 = sim_wu(model, mpc1, mpc2, N)
115+
@test u_data1 u_data2 atol=1e-2 rtol=1e-2
116+
end
117+
118+
@testitem "LinearMPCext with Wd weight" setup=[SetupMPCtests] begin
119+
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, DAQP
120+
import LinearMPC
121+
model = LinModel([tf([2], [10, 1]) tf(0.1, [7, 1])], 3.0, i_d=[2])
122+
model = setop!(model, uop=[25], dop=[30], yop=[50])
123+
optim = JuMP.Model(DAQP.Optimizer)
124+
mpc1 = LinMPC(model, Nwt=[0], Hp=250, Hc=1, Wd=[1], Wu=[1], optim=optim)
125+
mpc1 = setconstraint!(mpc1, wmax=[60])
126+
mpc2 = LinearMPC.MPC(mpc1)
127+
function sim_wd(model, mpc1, mpc2, N)
128+
r = [80.0]
129+
d = [30.0]
130+
u1 = [25.0]
131+
u2 = [25.0]
132+
model.x0 .= 0
133+
u_data1, u_data2 = zeros(1, N), zeros(1, N)
134+
for k in 0:N-1
135+
y = model(d)
136+
= preparestate!(mpc1, y, d)
137+
u1 = moveinput!(mpc1, r, d, lastu=u1)
138+
u2 = LinearMPC.compute_control(mpc2, x̂, r=r, d=d, uprev=u2)
139+
u_data1[:, k+1], u_data2[:, k+1] = u1, u2
140+
updatestate!(model, u1, d)
141+
updatestate!(mpc1, u1, y, d)
142+
end
143+
return u_data1, u_data2
144+
end
145+
N = 30
146+
u_data1, u_data2 = sim_wd(model, mpc1, mpc2, N)
147+
@test u_data1 u_data2 atol=1e-2 rtol=1e-2
148+
end
149+
150+
@testitem "LinearMPCext with Wr weight" setup=[SetupMPCtests] begin
151+
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, DAQP
152+
import LinearMPC
153+
model = LinModel(tf([2], [10, 1]), 3.0)
154+
model = setop!(model, yop=[50], uop=[20])
155+
optim = JuMP.Model(DAQP.Optimizer)
156+
mpc1 = LinMPC(model, Hp=20, Hc=5, Wy=[1], Wr=[1], optim=optim)
157+
mpc1 = setconstraint!(mpc1, wmin=[85])
158+
mpc2 = LinearMPC.MPC(mpc1)
159+
function sim_wr(model, mpc1, mpc2, N)
160+
r = [40.0]
161+
u1 = [20.0]
162+
u2 = [20.0]
163+
model.x0 .= 0
164+
u_data1, u_data2 = zeros(1, N), zeros(1, N)
165+
for k in 0:N-1
166+
y = model()
167+
= preparestate!(mpc1, y)
168+
u1 = moveinput!(mpc1, r, lastu=u1)
169+
u2 = LinearMPC.compute_control(mpc2, x̂, r=r, uprev=u2)
170+
u_data1[:, k+1], u_data2[:, k+1] = u1, u2
171+
updatestate!(model, u1)
172+
updatestate!(mpc1, u1, y)
173+
end
174+
return u_data1, u_data2
175+
end
176+
N = 30
177+
u_data1, u_data2 = sim_wr(model, mpc1, mpc2, N)
178+
@test u_data1 u_data2 atol=1e-2 rtol=1e-2
53179
end

0 commit comments

Comments
 (0)