Skip to content

Commit a20db3d

Browse files
committed
Merge branch 'main' into extension
2 parents 4ce2a1d + 399968e commit a20db3d

File tree

14 files changed

+287
-21
lines changed

14 files changed

+287
-21
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- name: Set JULIA_DEBUG environment variable if applicable
3535
if: ${{ runner.debug == '1' }}
3636
run: echo "JULIA_DEBUG=ModelPredictiveControl" >> $GITHUB_ENV
37-
- uses: actions/checkout@v5
37+
- uses: actions/checkout@v6
3838
- uses: julia-actions/setup-julia@v2
3939
with:
4040
version: ${{ matrix.version }}

.github/workflows/DocCleanup.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- name: Checkout gh-pages branch
12-
uses: actions/checkout@v5
12+
uses: actions/checkout@v6
1313
with:
1414
ref: gh-pages
1515

.github/workflows/documentation.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
contents: write
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v5
16+
- uses: actions/checkout@v6
1717
- uses: julia-actions/setup-julia@v2
1818
with:
1919
version: '1'

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.13.3"
3+
version = "1.14.1"
44
authors = ["Francis Gagnon"]
55

66
[deps]

benchmark/2_bench_state_estim.jl

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ UNIT_ESTIM["MovingHorizonEstimator"]["updatestate!"]["LinModel"]["Prediction for
172172
setup=preparestate!($mhe_lin_pred, $y, $d),
173173
samples=samples, evals=evals, seconds=seconds,
174174
)
175+
UNIT_ESTIM["MovingHorizonEstimator"]["getinfo!"]["LinModel"] =
176+
@benchmarkable(
177+
getinfo($mhe_lin_curr),
178+
setup=preparestate!($mhe_lin_curr, $y, $d),
179+
samples=samples, evals=evals, seconds=seconds,
180+
)
175181
UNIT_ESTIM["MovingHorizonEstimator"]["preparestate!"]["NonLinModel"]["Current form"] =
176182
@benchmarkable(
177183
preparestate!($mhe_nonlin_curr, $y, $d),
@@ -193,7 +199,13 @@ UNIT_ESTIM["MovingHorizonEstimator"]["updatestate!"]["NonLinModel"]["Prediction
193199
updatestate!($mhe_nonlin_pred, $u, $y, $d),
194200
setup=preparestate!($mhe_nonlin_pred, $y, $d),
195201
samples=samples, evals=evals, seconds=seconds,
196-
)
202+
)
203+
UNIT_ESTIM["MovingHorizonEstimator"]["getinfo!"]["NonLinModel"] =
204+
@benchmarkable(
205+
getinfo($mhe_nonlin_curr),
206+
setup=preparestate!($mhe_nonlin_curr, $y, $d),
207+
samples=samples, evals=evals, seconds=seconds,
208+
)
197209

198210
## ----------------------------------------------------------------------------------------
199211
## ----------------- CASE STUDIES ---------------------------------------------------------

benchmark/3_bench_predictive_control.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ UNIT_MPC["LinMPC"]["moveinput!"]["MultipleShooting"] =
2525
setup=preparestate!($linmpc_ms, $y, $d),
2626
samples=samples, evals=evals, seconds=seconds
2727
)
28+
UNIT_MPC["LinMPC"]["getinfo!"] =
29+
@benchmarkable(
30+
getinfo($linmpc_ss),
31+
setup=(preparestate!($linmpc_ss, $y, $d); moveinput!($linmpc_ss, $y, $d)),
32+
samples=samples, evals=evals, seconds=seconds
33+
)
2834

2935
empc = ExplicitMPC(linmodel, Mwt=[1, 1], Nwt=[0.1, 0.1], Lwt=[0.1, 0.1], Hp=10)
3036

@@ -76,6 +82,12 @@ UNIT_MPC["NonLinMPC"]["moveinput!"]["LinModel"]["MultipleShooting"] =
7682
setup=preparestate!($nmpc_lin_ms, $y, $d),
7783
samples=samples, evals=evals, seconds=seconds
7884
)
85+
UNIT_MPC["NonLinMPC"]["getinfo!"]["LinModel"] =
86+
@benchmarkable(
87+
getinfo($nmpc_lin_ss),
88+
setup=(preparestate!($nmpc_lin_ss, $y, $d); moveinput!($nmpc_lin_ss, $y, $d)),
89+
samples=samples, evals=evals, seconds=seconds
90+
)
7991
UNIT_MPC["NonLinMPC"]["moveinput!"]["NonLinModel"]["SingleShooting"] =
8092
@benchmarkable(
8193
moveinput!($nmpc_nonlin_ss, $y, $d),
@@ -106,6 +118,12 @@ UNIT_MPC["NonLinMPC"]["moveinput!"]["NonLinModel"]["TrapezoidalCollocation"] =
106118
setup=preparestate!($nmpc_nonlin_tc, $y_c, $d_c),
107119
samples=samples, evals=evals, seconds=seconds
108120
)
121+
UNIT_MPC["NonLinMPC"]["getinfo!"]["NonLinModel"] =
122+
@benchmarkable(
123+
getinfo($nmpc_nonlin_ss),
124+
setup=(preparestate!($nmpc_nonlin_ss, $y, $d); moveinput!($nmpc_nonlin_ss, $y, $d)),
125+
samples=samples, evals=evals, seconds=seconds
126+
)
109127

110128
## ----------------------------------------------------------------------------------------
111129
## ---------------------- CASE STUDIES ----------------------------------------------------

src/ModelPredictiveControl.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ using RecipesBase
99

1010
using DifferentiationInterface: ADTypes.AbstractADType, AutoForwardDiff
1111
using DifferentiationInterface: AutoSparse, SecondOrder
12+
using DifferentiationInterface: gradient, jacobian, hessian
13+
using DifferentiationInterface: value_and_gradient, value_and_jacobian
14+
using DifferentiationInterface: value_gradient_and_hessian
1215
using DifferentiationInterface: gradient!, value_and_gradient!, prepare_gradient
1316
using DifferentiationInterface: jacobian!, value_and_jacobian!, prepare_jacobian
1417
using DifferentiationInterface: hessian!, value_gradient_and_hessian!, prepare_hessian

src/controller/execute.jl

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,22 @@ The function should be called after calling [`moveinput!`](@ref). It returns the
105105
- `:d` : current measured disturbance, ``\mathbf{d}(k)``
106106
107107
For [`LinMPC`](@ref) and [`NonLinMPC`](@ref), the field `:sol` also contains the optimizer
108-
solution summary that can be printed. Lastly, the economical cost `:JE` and the custom
109-
nonlinear constraints `:gc` values at the optimum are also available for [`NonLinMPC`](@ref).
108+
solution summary that can be printed. Lastly, for [`NonLinMPC`](@ref), the following fields
109+
are also available:
110+
111+
- `:JE`: economic cost value at the optimum, ``J_E``
112+
- `:gc`: custom nonlinear constraints values at the optimum, ``\mathbf{g_c}``
113+
- `:∇J` or *`:nablaJ`* : optimal gradient of the objective function, ``\mathbf{\nabla} J``
114+
- `:∇²J` or *`:nabla2J`* : optimal Hessian of the objective function, ``\mathbf{\nabla^2}J``
115+
- `:g` : optimal nonlinear inequality constraint values, ``\mathbf{g}``
116+
- `:∇g` or *`:nablag`* : optimal Jacobian of the inequality constraint, ``\mathbf{\nabla g}``
117+
- `:∇²ℓg` or *`:nabla2lg`* : optimal Hessian of the inequality Lagrangian, ``\mathbf{\nabla^2}\ell_{\mathbf{g}}``
118+
- `:geq` : optimal nonlinear equality constraint values, ``\mathbf{g_{eq}}``
119+
- `:∇geq` or *`:nablageq`* : optimal Jacobian of the equality constraint, ``\mathbf{\nabla g_{eq}}``
120+
- `:∇²ℓgeq` or *`:nabla2lgeq`* : optimal Hessian of the equality Lagrangian, ``\mathbf{\nabla^2}\ell_{\mathbf{g_{eq}}}``
121+
122+
Note that retrieving optimal Hessians of Lagrangian are not fully supported yet. Their
123+
nonzero coefficients are random values for now.
110124
111125
# Examples
112126
```jldoctest

src/controller/nonlinmpc.jl

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -526,9 +526,11 @@ end
526526
"""
527527
addinfo!(info, mpc::NonLinMPC) -> info
528528
529-
For [`NonLinMPC`](@ref), add `:sol` and the optimal economic cost `:JE`.
529+
For [`NonLinMPC`](@ref), add `:sol`, the custom nonlinear objective `:JE`, the nonlinear
530+
constraint vectors and the various derivatives.
530531
"""
531532
function addinfo!(info, mpc::NonLinMPC{NT}) where NT<:Real
533+
# --- variables specific to NonLinMPC ---
532534
U, Ŷ, D̂, ŷ, d, ϵ = info[:U], info[:Ŷ], info[:D̂], info[:ŷ], info[:d], info[]
533535
Ue = [U; U[(end - mpc.estim.model.nu + 1):end]]
534536
Ŷe = [ŷ; Ŷ]
@@ -539,6 +541,120 @@ function addinfo!(info, mpc::NonLinMPC{NT}) where NT<:Real
539541
info[:JE] = JE
540542
info[:gc] = LHS
541543
info[:sol] = JuMP.solution_summary(mpc.optim, verbose=true)
544+
# --- objective derivatives ---
545+
model, optim, con = mpc.estim.model, mpc.optim, mpc.con
546+
transcription = mpc.transcription
547+
nu, ny, nx̂, nϵ = model.nu, model.ny, mpc.estim.nx̂, mpc.
548+
nk = get_nk(model, transcription)
549+
Hp, Hc = mpc.Hp, mpc.Hc
550+
ng = length(con.i_g)
551+
nc, neq = con.nc, con.neq
552+
nU, nŶ, nX̂, nK = mpc.Hp*nu, Hp*ny, Hp*nx̂, Hp*nk
553+
nΔŨ, nUe, nŶe = nu*Hc + nϵ, nU + nu, nŶ + ny
554+
ΔŨ = zeros(NT, nΔŨ)
555+
x̂0end = zeros(NT, nx̂)
556+
K0 = zeros(NT, nK)
557+
Ue, Ŷe = zeros(NT, nUe), zeros(NT, nŶe)
558+
U0, Ŷ0 = zeros(NT, nU), zeros(NT, nŶ)
559+
Û0, X̂0 = zeros(NT, nU), zeros(NT, nX̂)
560+
gc, g = zeros(NT, nc), zeros(NT, ng)
561+
geq = zeros(NT, neq)
562+
J_cache = (
563+
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
564+
Cache(Û0), Cache(K0), Cache(X̂0),
565+
Cache(gc), Cache(g), Cache(geq),
566+
)
567+
function J!(Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq)
568+
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
569+
return obj_nonlinprog!(Ŷ0, U0, mpc, model, Ue, Ŷe, ΔŨ)
570+
end
571+
if !isnothing(mpc.hessian)
572+
_, ∇J, ∇²J = value_gradient_and_hessian(J!, mpc.hessian, mpc.Z̃, J_cache...)
573+
else
574+
∇J, ∇²J = gradient(J!, mpc.gradient, mpc.Z̃, J_cache...), nothing
575+
end
576+
# --- inequality constraint derivatives ---
577+
old_i_g = copy(con.i_g)
578+
con.i_g .= 1 # temporarily set all constraints as finite so g is entirely computed
579+
∇g_cache = (
580+
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
581+
Cache(Û0), Cache(K0), Cache(X̂0),
582+
Cache(gc), Cache(geq),
583+
)
584+
function g!(g, Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, geq)
585+
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
586+
return nothing
587+
end
588+
g, ∇g = value_and_jacobian(g!, g, mpc.jacobian, mpc.Z̃, ∇g_cache...)
589+
if !isnothing(mpc.hessian) && any(old_i_g)
590+
@warn(
591+
"Retrieving optimal Hessian of the Lagrangian is not fully supported yet.\n"*
592+
"Its nonzero coefficients are random values for now.", maxlog=1
593+
)
594+
function ℓ_g(Z̃, λ, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, geq, g)
595+
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
596+
return dot(λ, g)
597+
end
598+
∇²g_cache = (
599+
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
600+
Cache(Û0), Cache(K0), Cache(X̂0),
601+
Cache(gc), Cache(geq), Cache(g)
602+
)
603+
nonlincon = optim[:nonlinconstraint]
604+
λ = JuMP.dual.(nonlincon) # FIXME: does not work for now
605+
λ = rand(NT, ng)
606+
∇²ℓg = hessian(ℓ_g, mpc.hessian, mpc.Z̃, Constant(λ), ∇²g_cache...)
607+
else
608+
∇²ℓg = nothing
609+
end
610+
con.i_g .= old_i_g # restore original finite/infinite constraint indices
611+
# --- equality constraint derivatives ---
612+
geq_cache = (
613+
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
614+
Cache(Û0), Cache(K0), Cache(X̂0),
615+
Cache(gc), Cache(g)
616+
)
617+
function geq!(geq, Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g)
618+
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
619+
return nothing
620+
end
621+
geq, ∇geq = value_and_jacobian(geq!, geq, mpc.jacobian, mpc.Z̃, geq_cache...)
622+
if !isnothing(mpc.hessian) && con.neq > 0
623+
@warn(
624+
"Retrieving optimal Hessian of the Lagrangian is not fully supported yet.\n"*
625+
"Its nonzero coefficients are random values for now.", maxlog=1
626+
)
627+
∇²geq_cache = (
628+
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
629+
Cache(Û0), Cache(K0), Cache(X̂0),
630+
Cache(gc), Cache(geq), Cache(g)
631+
)
632+
function ℓ_geq(Z̃, λeq, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, geq, g)
633+
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
634+
return dot(λeq, geq)
635+
end
636+
nonlinconeq = optim[:nonlinconstrainteq]
637+
λeq = JuMP.dual.(nonlinconeq) # FIXME: does not work for now
638+
λeq = ones(NT, neq)
639+
∇²ℓgeq = hessian(ℓ_geq, mpc.hessian, mpc.Z̃, Constant(λeq), ∇²geq_cache...)
640+
else
641+
∇²ℓgeq = nothing
642+
end
643+
info[:∇J] = ∇J
644+
info[:∇²J] = ∇²J
645+
info[:g] = g
646+
info[:∇g] = ∇g
647+
info[:∇²ℓg] = ∇²ℓg
648+
info[:geq] = geq
649+
info[:∇geq] = ∇geq
650+
info[:∇²ℓgeq] = ∇²ℓgeq
651+
# --- non-Unicode fields ---
652+
info[:nablaJ] = ∇J
653+
info[:nabla2J] = ∇²J
654+
info[:nablag] = ∇g
655+
info[:nabla2lg] = ∇²ℓg
656+
info[:nablageq] = ∇geq
657+
info[:nabla2lgeq] = ∇²ℓgeq
542658
return info
543659
end
544660

@@ -942,10 +1058,10 @@ function set_nonlincon!(
9421058
optim, JuMP.Vector{JuMP.VariableRef}, MOI.VectorNonlinearOracle{JNT}
9431059
)
9441060
map(con_ref -> JuMP.delete(optim, con_ref), nonlin_constraints)
945-
optim[:g_oracle] = g_oracle
946-
optim[:geq_oracle] = geq_oracle
947-
any(mpc.con.i_g) && @constraint(optim, Z̃var in g_oracle)
948-
mpc.con.neq > 0 && @constraint(optim, Z̃var in geq_oracle)
1061+
JuMP.unregister(optim, :nonlinconstraint)
1062+
JuMP.unregister(optim, :nonlinconstrainteq)
1063+
any(mpc.con.i_g) && @constraint(optim, nonlinconstraint, Z̃var in g_oracle)
1064+
mpc.con.neq > 0 && @constraint(optim, nonlinconstrainteq, Z̃var in geq_oracle)
9491065
return nothing
9501066
end
9511067

src/estimator/mhe/construct.jl

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,9 +1504,7 @@ function get_nonlincon_oracle(
15041504
return dot(λi, gi)
15051505
end
15061506
Z̃_∇gi = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
1507-
∇gi_cache = (
1508-
Cache(V̂), Cache(X̂0), Cache(û0), Cache(k0), Cache(ŷ0), Cache(g)
1509-
)
1507+
∇gi_cache = (Cache(V̂), Cache(X̂0), Cache(û0), Cache(k0), Cache(ŷ0), Cache(g))
15101508
# temporarily "fill" the estimation window for the preparation of the gradient:
15111509
estim.Nk[] = He
15121510
∇gi_prep = prepare_jacobian(gi!, gi, jac, Z̃_∇gi, ∇gi_cache...; strict)
@@ -1577,7 +1575,7 @@ function set_nonlincon!(
15771575
optim, JuMP.Vector{JuMP.VariableRef}, MOI.VectorNonlinearOracle{JNT}
15781576
)
15791577
map(con_ref -> JuMP.delete(optim, con_ref), nonlin_constraints)
1580-
optim[:g_oracle] = g_oracle
1581-
any(estim.con.i_g) && @constraint(optim, Z̃var in g_oracle)
1578+
JuMP.unregister(optim, :nonlinconstraint)
1579+
any(estim.con.i_g) && @constraint(optim, nonlinconstraint, Z̃var in g_oracle)
15821580
return nothing
15831581
end

0 commit comments

Comments
 (0)