Skip to content

Commit c06273f

Browse files
authored
Merge pull request #331 from JuliaControl/ortho_colloc_foh
added: `h=1` support for `OrthogonalCollocation` + misc. improvements
2 parents c4290c6 + cd026dd commit c06273f

File tree

4 files changed

+100
-75
lines changed

4 files changed

+100
-75
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 = "2.0.0"
3+
version = "2.1.0"
44
authors = ["Francis Gagnon"]
55

66
[deps]

benchmark/3_bench_predictive_control.jl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,12 @@ nmpc_madnlp_ms_hess = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt, optim, transcripti
395395
nmpc_madnlp_ms_hess = setconstraint!(nmpc_madnlp_ms_hess; umin, umax)
396396
JuMP.unset_time_limit_sec(nmpc_madnlp_ms_hess.optim)
397397

398+
optim = JuMP.Model(()->UnoSolver.Optimizer(preset="filtersqp"), add_bridges=false)
399+
transcription, hessian = OrthogonalCollocation(), true
400+
nmpc_uno_oc_hess = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt, optim, transcription, hessian)
401+
nmpc_uno_oc_hess = setconstraint!(nmpc_uno_oc_hess; umin, umax)
402+
JuMP.unset_time_limit_sec(nmpc_uno_oc_hess.optim)
403+
398404
samples, evals, seconds = 100, 1, 15*60
399405
CASE_MPC["Pendulum"]["NonLinMPC"]["Noneconomic"]["Ipopt"]["SingleShooting"] =
400406
@benchmarkable(
@@ -461,7 +467,11 @@ CASE_MPC["Pendulum"]["NonLinMPC"]["Noneconomic"]["Uno"]["MultipleShooting (Hessi
461467
sim!($nmpc_uno_ms_hess, $N, $ry; plant=$plant, x_0=$x_0, x̂_0=$x̂_0, progress=false),
462468
samples=samples, evals=evals, seconds=seconds, setup=GC.gc()
463469
)
464-
470+
CASE_MPC["Pendulum"]["NonLinMPC"]["Noneconomic"]["Uno"]["OrthogonalCollocation (Hessian)"] =
471+
@benchmarkable(
472+
sim!($nmpc_uno_oc_hess, $N, $ry; plant=$plant, x_0=$x_0, x̂_0=$x̂_0, progress=false),
473+
samples=samples, evals=evals, seconds=seconds, setup=GC.gc()
474+
)
465475

466476
# ----------------- Case study: Pendulum economic --------------------------------
467477
model2, p = pendulum_model2, pendulum_p2

src/controller/transcription.jl

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,9 @@ end
129129
Construct an orthogonal collocation on finite elements [`TranscriptionMethod`](@ref).
130130
131131
Also known as pseudo-spectral method. It supports continuous-time [`NonLinModel`](@ref)s
132-
only. The `h` argument is the hold order for ``\mathbf{u}``, and `no` argument, the number
133-
of collocation points ``n_o``. Only zero-order hold is currently implemented, so `h` must
134-
be `0`. The decision variable is similar to [`MultipleShooting`](@ref), but it also includes
135-
the collocation points:
132+
only. The `h` argument is the hold order for ``\mathbf{u}``, and the `no` argument, the
133+
number of collocation points ``n_o``. The decision variable is similar to [`MultipleShooting`](@ref),
134+
but it also includes the collocation points:
136135
```math
137136
\mathbf{Z} = \begin{bmatrix} \mathbf{ΔU} \\ \mathbf{X̂_0} \\ \mathbf{K} \end{bmatrix}
138137
```
@@ -187,9 +186,6 @@ struct OrthogonalCollocation <: CollocationMethod
187186
function OrthogonalCollocation(
188187
h::Int=0, no::Int=3; f_threads=false, h_threads=false, roots=:gaussradau
189188
)
190-
if !(h == 0)
191-
throw(ArgumentError("Only the zero-order hold (h=0) is currently implemented."))
192-
end
193189
if roots==:gaussradau
194190
x, _ = FastGaussQuadrature.gaussradau(no, COLLOCATION_NODE_TYPE)
195191
# we reverse the nodes to include the τ=1.0 node:
@@ -1378,6 +1374,11 @@ function con_nonlinprogeq!(
13781374
nk = get_nk(model, transcription)
13791375
D̂0 = mpc.D̂0
13801376
X̂0_Z̃ = @views Z̃[(nΔU+1):(nΔU+nX̂)]
1377+
for j=0:Hp-1 # prefilling Û0 to avoid race-condition (both û0 and û0next are needed):
1378+
x̂0_Z̃ = @views j < 1 ? mpc.estim.x̂0[1:nx̂] : X̂0_Z̃[(1 + nx̂*(j-1)):(nx̂*j)]
1379+
u0, û0 = @views U0[(1 + nu*j):(nu*(j+1))], Û0[(1 + nu*j):(nu*(j+1))]
1380+
f̂_input!(û0, mpc.estim, model, x̂0_Z̃, u0)
1381+
end
13811382
@threadsif f_threads for j=1:Hp
13821383
if j < 2
13831384
x̂0_Z̃ = @views mpc.estim.x̂0[1:nx̂]
@@ -1401,25 +1402,21 @@ function con_nonlinprogeq!(
14011402
fs!(x̂0next, mpc.estim, model, x̂0_Z̃)
14021403
ssnext .= @. xsnext - xsnext_Z̃
14031404
# ----------------- deterministic defects: trapezoidal collocation -------------
1404-
u0 = @views U0[(1 + nu*(j-1)):(nu*j)]
14051405
û0 = @views Û0[(1 + nu*(j-1)):(nu*j)]
1406-
f̂_input!(û0, mpc.estim, model, x̂0_Z̃, u0)
14071406
if f_threads || h < 1 || j < 2
14081407
# we need to recompute k1 with multi-threading, even with h==1, since the
14091408
# last iteration (j-1) may not be executed (iterations are re-orderable)
14101409
model.f!(k̇1, x0_Z̃, û0, d̂0, p)
14111410
else
14121411
k̇1 .= @views K̇[(1 + nk*(j-1)-nx):(nk*(j-1))] # k2 of of the last iter. j-1
14131412
end
1414-
if h < 1 || j Hp
1415-
# j = Hp special case: u(k+Hp-1) = u(k+Hp) since Hc ≤ Hp implies Δu(k+Hp) = 0
1416-
û0next = û0
1413+
if h < 1
1414+
model.f!(k̇2, x0next_Z̃, û0, d̂0next, p)
14171415
else
1418-
u0next = @views U0[(1 + nu*j):(nu*(j+1))]
1419-
û0next = @views Û0[(1 + nu*j):(nu*(j+1))]
1420-
f̂_input!(û0next, mpc.estim, model, x̂0next_Z̃, u0next)
1416+
# j = Hp special case: u(k+Hp-1) = u(k+Hp) since Hc≤Hp implies Δu(k+Hp) = 0:
1417+
û0next = @views j Hp ? û0 : Û0[(1 + nu*j):(nu*(j+1))]
1418+
model.f!(k̇2, x0next_Z̃, û0next, d̂0next, p)
14211419
end
1422-
model.f!(k̇2, x0next_Z̃, û0next, d̂0next, p)
14231420
sdnext .= @. x0_Z̃ - x0next_Z̃ + 0.5*Ts*(k̇1 + k̇2)
14241421
end
14251422
return geq
@@ -1456,16 +1453,21 @@ extracted from the decision variable `Z̃`. The ``\mathbf{x_0}`` vectors are the
14561453
deterministic state extracted from `Z̃`. The ``\mathbf{k̇}_i`` derivative for the ``i``th
14571454
collocation point is computed from the continuous-time function `model.f!` and:
14581455
```math
1459-
\mathbf{k̇}_i(k+j) = \mathbf{f}\Big(\mathbf{k}_i(k+j), \mathbf{û_0}(k+j), \mathbf{d̂}_i(k+j), \mathbf{p}\Big)
1456+
\mathbf{k̇}_i(k+j) = \mathbf{f}\Big(\mathbf{k}_i(k+j), \mathbf{û_i}(k+j), \mathbf{d̂}_i(k+j), \mathbf{p}\Big)
14601457
```
1461-
The disturbed input ``\mathbf{û_0}(k+j)`` is defined in [`f̂_input!`](@ref). Based on the
1462-
normalized time ``τ_i ∈ [0, 1]``, measured disturbances are linearly interpolated:
1458+
Based on the normalized time ``τ_i ∈ [0, 1]`` and hold order `transcription.h`, the inputs
1459+
and disturbances are piecewise constant or linear:
14631460
```math
1464-
\mathbf{d̂}_i(k+j) = (1-τ_i)\mathbf{d̂_0}(k+j) + τ_i\mathbf{d̂_0}(k+j+1)
1461+
\begin{aligned}
1462+
\mathbf{û}_i(k+j) &= \begin{cases}
1463+
\mathbf{û_0}(k+1) & h = 0 \\
1464+
(1-τ_i)\mathbf{û_0}(k+j) + τ_i\mathbf{û_0}(k+j+1) & h = 1 \end{cases} \\
1465+
\mathbf{d̂}_i(k+j) &= (1-τ_i)\mathbf{d̂_0}(k+j) + τ_i\mathbf{d̂_0}(k+j+1)
1466+
\end{aligned}
14651467
```
1466-
The defects for the stochastic states ``\mathbf{s_s}`` are computed as in the
1467-
[`TrapezoidalCollocation`](@ref) method, and the ones for the continuity constraint of the
1468-
deterministic state trajectories are given by:
1468+
The disturbed input ``\mathbf{û_0}(k+j)`` is defined in [`f̂_input!`](@ref). The defects for
1469+
the stochastic states ``\mathbf{s_s}`` are computed as the [`TrapezoidalCollocation`](@ref)
1470+
method, and the ones for the continuity constraint of the deterministic states are:
14691471
```math
14701472
\mathbf{s_c}(k+j+1)
14711473
= \mathbf{C_o} \begin{bmatrix}
@@ -1483,7 +1485,7 @@ function con_nonlinprogeq!(
14831485
mpc::PredictiveController, model::NonLinModel, transcription::OrthogonalCollocation,
14841486
U0, Z̃
14851487
)
1486-
nu, nx̂, nd, nx = model.nu, mpc.estim.nx̂, model.nd, model.nx
1488+
nu, nx̂, nd, nx, h = model.nu, mpc.estim.nx̂, model.nd, model.nx, transcription.h
14871489
Hp, Hc = mpc.Hp, mpc.Hc
14881490
nΔU, nX̂ = nu*Hc, nx̂*Hp
14891491
f_threads = transcription.f_threads
@@ -1494,8 +1496,12 @@ function con_nonlinprogeq!(
14941496
nx̂_nk = nx̂ + nk
14951497
D̂0 = mpc.D̂0
14961498
X̂0_Z̃, K_Z̃ = @views Z̃[(nΔU+1):(nΔU+nX̂)], Z̃[(nΔU+nX̂+1):(nΔU+nX̂+nk*Hp)]
1497-
di = mpc.estim.buffer.d
1498-
ΔK = similar(K̇) # TODO: remove this allocation
1499+
D̂temp = mpc.buffer.
1500+
for j=0:Hp-1 # prefilling Û0 to avoid race-condition (both û0 and û0next are needed):
1501+
x̂0_Z̃ = @views j < 1 ? mpc.estim.x̂0[1:nx̂] : X̂0_Z̃[(1 + nx̂*(j-1)):(nx̂*j)]
1502+
u0, û0 = @views U0[(1 + nu*j):(nu*(j+1))], Û0[(1 + nu*j):(nu*(j+1))]
1503+
f̂_input!(û0, mpc.estim, model, x̂0_Z̃, u0)
1504+
end
14991505
@threadsif f_threads for j=1:Hp
15001506
if j < 2
15011507
x̂0_Z̃ = @views mpc.estim.x̂0[1:nx̂]
@@ -1505,7 +1511,6 @@ function con_nonlinprogeq!(
15051511
d̂0 = @views D̂0[(1 + nd*(j-2)):(nd*(j-1))]
15061512
end
15071513
= @views K̇[(1 + nk*(j-1)):(nk*j)]
1508-
Δk = @views ΔK[(1 + nk*(j-1)):(nk*j)]
15091514
k_Z̃ = @views K_Z̃[(1 + nk*(j-1)):(nk*j)]
15101515
d̂0next = @views D̂0[(1 + nd*(j-1)):(nd*j)]
15111516
x̂0next = @views X̂0[(1 + nx̂*(j-1)):(nx̂*j)]
@@ -1521,18 +1526,30 @@ function con_nonlinprogeq!(
15211526
fs!(x̂0next, mpc.estim, model, x̂0_Z̃)
15221527
ssnext .= @. xsnext - xsnext_Z̃
15231528
# ----------------- collocation constraint defects -----------------------------
1524-
u0 = @views U0[(1 + nu*(j-1)):(nu*j)]
15251529
û0 = @views Û0[(1 + nu*(j-1)):(nu*j)]
1526-
f̂_input!(û0, mpc.estim, model, x̂0_Z̃, u0)
1530+
Δk =
1531+
for i=1:no
1532+
Δk[(1 + (i-1)*nx):(i*nx)] = @views k_Z̃[(1 + (i-1)*nx):(i*nx)] .- x0_Z̃
1533+
end
1534+
mul!(sk, Mo, Δk)
1535+
d̂i = @views D̂temp[(1 + nd*(j-1)):(nd*j)]
1536+
if h > 0
1537+
ûi = similar(û0) # TODO: remove this allocation
1538+
end
15271539
for i=1:no
15281540
k̇i = @views k̇[(1 + (i-1)*nx):(i*nx)]
1529-
Δki = @views Δk[(1 + (i-1)*nx):(i*nx)]
15301541
ki_Z̃ = @views k_Z̃[(1 + (i-1)*nx):(i*nx)]
1531-
Δki .= @. ki_Z̃ - x0_Z̃
1532-
di .= (1-τ[i]).*d̂0 .+ τ[i].*d̂0next
1533-
model.f!(k̇i, ki_Z̃, û0, d̂0, p)
1542+
d̂i .= (1-τ[i]).*d̂0 .+ τ[i].*d̂0next
1543+
if h < 1
1544+
model.f!(k̇i, ki_Z̃, û0, d̂i, p)
1545+
else
1546+
# j = Hp special case: u(k+Hp-1) = u(k+Hp) since Hc≤Hp implies Δu(k+Hp) = 0:
1547+
û0next = @views j Hp ? û0 : Û0[(1 + nu*j):(nu*(j+1))]
1548+
ûi .= (1-τ[i]).*û0 .+ τ[i].*û0next
1549+
model.f!(k̇i, ki_Z̃, ûi, d̂i, p)
1550+
end
15341551
end
1535-
sk .= mul!(sk, Mo, Δk) .-
1552+
sk .-=
15361553
# ----------------- continuity constraint defects ------------------------------
15371554
scnext .= mul!(scnext, Co, k_Z̃) .+ (λo.*x0_Z̃) .- x0next_Z̃
15381555
end

test/3_test_predictive_control.jl

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -955,36 +955,53 @@ end
955955
f = (x,u,d,model) -> model.A*x + model.Bu*u + model.Bd*d
956956
h = (x,d,model) -> model.C*x + model.Dd*d
957957
nonlinmodel = NonLinModel(f, h, 3000.0, 1, 2, 1, 1, solver=nothing, p=linmodel2)
958+
959+
f! = (ẋ,x,u,_,_) -> ẋ .= -0.001x .+ u
960+
h! = (y,x,_,_) -> y .= x
961+
nonlinmodel_c = NonLinModel(f!, h!, 500, 1, 1, 1)
958962

959-
nmpc2 = NonLinMPC(nonlinmodel, Nwt=[0], Hp=100, Hc=1)
960-
preparestate!(nmpc2, [0], [0])
963+
nmpc1 = NonLinMPC(nonlinmodel, Nwt=[0], Hp=100, Hc=1)
964+
preparestate!(nmpc1, [0], [0])
961965
# if d=[0.1], the output will eventually reach 7*0.1=0.7, no action needed (u=0):
962966
d = [0.1]
963-
u = moveinput!(nmpc2, 7d, d)
967+
u = moveinput!(nmpc1, 7d, d)
964968
@test u [0] atol=5e-2
965-
u = nmpc2(7d, d)
969+
u = nmpc1(7d, d)
966970
@test u [0] atol=5e-2
967-
info = getinfo(nmpc2)
971+
info = getinfo(nmpc1)
968972
@test info[:u] u
969973
@test info[:Ŷ][end] 7d[1] atol=5e-2
970974

971-
nmpc3 = NonLinMPC(nonlinmodel, Nwt=[0], Cwt=Inf, Hp=100, Hc=1)
972-
preparestate!(nmpc3, [0], [0])
973-
u = moveinput!(nmpc3, 7d, d)
975+
nmpc2 = NonLinMPC(nonlinmodel, Nwt=[0], Cwt=Inf, Hp=100, Hc=1)
976+
preparestate!(nmpc2, [0], [0])
977+
u = moveinput!(nmpc2, 7d, d)
974978
@test u [0] atol=5e-2
975979

976-
nmpc4 = NonLinMPC(nonlinmodel, Hp=15, Mwt=[0], Nwt=[0], Lwt=[1])
977-
preparestate!(nmpc4, [0], [0])
978-
u = moveinput!(nmpc4, [0], d, R̂u=fill(12, nmpc4.Hp))
980+
nmpc3 = NonLinMPC(nonlinmodel, Hp=15, Mwt=[0], Nwt=[0], Lwt=[1])
981+
preparestate!(nmpc3, [0], [0])
982+
u = moveinput!(nmpc3, [0], d, R̂u=fill(12, nmpc3.Hp))
979983
@test u [12] atol=5e-2
984+
985+
transcription = MultipleShooting()
986+
nmpc4 = NonLinMPC(nonlinmodel; Nwt=[0], Hp=100, Hc=1, transcription)
987+
preparestate!(nmpc4, [0], [0])
988+
u = moveinput!(nmpc4, [10], [0])
989+
@test u [2] atol=5e-2
990+
info = getinfo(nmpc4)
991+
@test info[:u] u
992+
@test info[:Ŷ][end] 10 atol=5e-2
980993

981-
nmpc5 = NonLinMPC(nonlinmodel, Hp=1, Hc=1, Cwt=Inf, transcription=MultipleShooting())
982-
nmpc5 = setconstraint!(nmpc5, ymin=[1])
983-
f! = (ẋ,x,u,_,_) -> ẋ .= -0.001x .+ u
984-
h! = (y,x,_,_) -> y .= x
985-
nonlinmodel_c = NonLinModel(f!, h!, 500, 1, 1, 1)
994+
transcription = MultipleShooting(f_threads=true, h_threads=true)
995+
nmpc4t = NonLinMPC(nonlinmodel; Nwt=[0], Hp=100, Hc=1, transcription, hessian=true)
996+
nmpc4t = setconstraint!(nmpc4t, ymax=[100], ymin=[-100]) # coverage of getinfo! Hessians of Lagrangian
997+
preparestate!(nmpc4t, [0], [0])
998+
u = moveinput!(nmpc4t, [10], [0])
999+
@test u [2] atol=5e-2
1000+
info = getinfo(nmpc4t)
1001+
@test info[:u] u
1002+
@test info[:Ŷ][end] 10 atol=5e-2
1003+
9861004
transcription = TrapezoidalCollocation(0, f_threads=true, h_threads=true)
987-
9881005
nmpc5 = NonLinMPC(nonlinmodel_c; Nwt=[0], Hp=100, Hc=1, transcription)
9891006
preparestate!(nmpc5, [0.0])
9901007
u = moveinput!(nmpc5, [1/0.001])
@@ -997,13 +1014,13 @@ end
9971014
@test u [1.0] atol=5e-2
9981015

9991016
transcription = OrthogonalCollocation(0, 4)
1000-
nmpc6 = NonLinMPC(InternalModel(nonlinmodel_c); Nwt=[0], Hp=100, Hc=1, transcription)
1017+
nmpc6 = NonLinMPC(nonlinmodel_c; Nwt=[0], Hp=100, Hc=1, transcription)
10011018
preparestate!(nmpc6, [0.0])
10021019
u = moveinput!(nmpc6, [1/0.001])
10031020
@test u [1.0] atol=5e-2
10041021

1005-
transcription = OrthogonalCollocation(roots=:gausslegendre)
1006-
nmpc6_1 = NonLinMPC(InternalModel(nonlinmodel_c); Nwt=[0], Hp=100, Hc=1, transcription)
1022+
transcription = OrthogonalCollocation(1, roots=:gausslegendre)
1023+
nmpc6_1 = NonLinMPC(nonlinmodel_c; Nwt=[0], Hp=100, Hc=1, transcription)
10071024
preparestate!(nmpc6_1, [0.0])
10081025
u = moveinput!(nmpc6_1, [1/0.001])
10091026
@test u [1.0] atol=5e-2
@@ -1014,25 +1031,6 @@ end
10141031
ModelPredictiveControl.h!(y, nonlinmodel2, Float32[0,0], Float32[0], nonlinmodel2.p)
10151032
preparestate!(nmpc7, [0], [0])
10161033
@test moveinput!(nmpc7, [0], [0]) [0.0] atol=5e-2
1017-
transcription = MultipleShooting()
1018-
1019-
nmpc8 = NonLinMPC(nonlinmodel; Nwt=[0], Hp=100, Hc=1, transcription)
1020-
preparestate!(nmpc8, [0], [0])
1021-
u = moveinput!(nmpc8, [10], [0])
1022-
@test u [2] atol=5e-2
1023-
info = getinfo(nmpc8)
1024-
@test info[:u] u
1025-
@test info[:Ŷ][end] 10 atol=5e-2
1026-
1027-
transcription = MultipleShooting(f_threads=true, h_threads=true)
1028-
nmpc8t = NonLinMPC(nonlinmodel; Nwt=[0], Hp=100, Hc=1, transcription, hessian=true)
1029-
nmpc8t = setconstraint!(nmpc8t, ymax=[100], ymin=[-100]) # coverage of getinfo! Hessians of Lagrangian
1030-
preparestate!(nmpc8t, [0], [0])
1031-
u = moveinput!(nmpc8t, [10], [0])
1032-
@test u [2] atol=5e-2
1033-
info = getinfo(nmpc8t)
1034-
@test info[:u] u
1035-
@test info[:Ŷ][end] 10 atol=5e-2
10361034

10371035
nmpc10 = setconstraint!(NonLinMPC(
10381036
nonlinmodel, Nwt=[0], Hp=100, Hc=1,

0 commit comments

Comments
 (0)