Skip to content

Commit 79b35cb

Browse files
authored
Merge pull request #207 from JuliaControl/debug_mhe
debug: force update of gradient/jacobian in `MovingHorzionEstimator` when window not filled
2 parents db2827a + f579da4 commit 79b35cb

File tree

8 files changed

+108
-55
lines changed

8 files changed

+108
-55
lines changed

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 = "1.6.1"
4+
version = "1.6.2"
55

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

docs/src/internals/state_estim.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ ModelPredictiveControl.linconstraint!(::MovingHorizonEstimator, ::LinModel)
3939

4040
```@docs
4141
ModelPredictiveControl.optim_objective!(::MovingHorizonEstimator)
42+
ModelPredictiveControl.set_warmstart_mhe!
4243
```
4344

4445
## Remove Operating Points

src/controller/nonlinmpc.jl

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -583,9 +583,9 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
583583
)
584584
∇J_prep = prepare_gradient(Jfunc!, grad, Z̃_∇J, ∇J_context...; strict)
585585
∇J = Vector{JNT}(undef, nZ̃)
586-
function update_objective!(J, ∇J, , Z̃arg)
587-
if isdifferent(Z̃arg, )
588-
.= Z̃arg
586+
function update_objective!(J, ∇J, Z̃_∇J, Z̃arg)
587+
if isdifferent(Z̃arg, Z̃_∇J)
588+
Z̃_∇J .= Z̃arg
589589
J[], _ = value_and_gradient!(Jfunc!, ∇J, ∇J_prep, grad, Z̃_∇J, ∇J_context...)
590590
end
591591
end
@@ -620,10 +620,10 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
620620
∇g_prep = prepare_jacobian(gfunc!, g, jac, Z̃_∇g, ∇g_context...; strict)
621621
mpc.con.i_g[1:end-nc] .= false
622622
∇g = init_diffmat(JNT, jac, ∇g_prep, nZ̃, ng)
623-
function update_con!(g, ∇g, , Z̃arg)
624-
if isdifferent(Z̃arg, )
625-
.= Z̃arg
626-
value_and_jacobian!(gfunc!, g, ∇g, ∇g_prep, jac, , ∇g_context...)
623+
function update_con!(g, ∇g, Z̃_∇g, Z̃arg)
624+
if isdifferent(Z̃arg, Z̃_∇g)
625+
Z̃_∇g .= Z̃arg
626+
value_and_jacobian!(gfunc!, g, ∇g, ∇g_prep, jac, Z̃_∇g, ∇g_context...)
627627
end
628628
end
629629
gfuncs = Vector{Function}(undef, ng)
@@ -662,10 +662,10 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
662662
)
663663
∇geq_prep = prepare_jacobian(geqfunc!, geq, jac, Z̃_∇geq, ∇geq_context...; strict)
664664
∇geq = init_diffmat(JNT, jac, ∇geq_prep, nZ̃, neq)
665-
function update_con_eq!(geq, ∇geq, , Z̃arg)
666-
if isdifferent(Z̃arg, )
667-
.= Z̃arg
668-
value_and_jacobian!(geqfunc!, geq, ∇geq, ∇geq_prep, jac, , ∇geq_context...)
665+
function update_con_eq!(geq, ∇geq, Z̃_∇geq, Z̃arg)
666+
if isdifferent(Z̃arg, Z̃_∇geq)
667+
Z̃_∇geq .= Z̃arg
668+
value_and_jacobian!(geqfunc!, geq, ∇geq, ∇geq_prep, jac, Z̃_∇geq, ∇geq_context...)
669669
end
670670
end
671671
geqfuncs = Vector{Function}(undef, neq)

src/controller/transcription.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,7 @@ linconstrainteq!(::PredictiveController, ::SimModel, ::MultipleShooting) = nothi
954954
@doc raw"""
955955
set_warmstart!(mpc::PredictiveController, transcription::SingleShooting, Z̃var) -> Z̃s
956956
957-
Set and return the warm start value of `Z̃var` for [`SingleShooting`](@ref) transcription.
957+
Set and return the warm-start value of `Z̃var` for [`SingleShooting`](@ref) transcription.
958958
959959
If supported by `mpc.optim`, it warm-starts the solver at:
960960
```math
@@ -985,7 +985,7 @@ end
985985
@doc raw"""
986986
set_warmstart!(mpc::PredictiveController, transcription::MultipleShooting, Z̃var) -> Z̃s
987987
988-
Set and return the warm start value of `Z̃var` for [`MultipleShooting`](@ref) transcription.
988+
Set and return the warm-start value of `Z̃var` for [`MultipleShooting`](@ref) transcription.
989989
990990
It warm-starts the solver at:
991991
```math

src/estimator/internal_model.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,15 @@ function matrices_internalmodel(model::SimModel{NT}) where NT<:Real
168168
end
169169

170170
@doc raw"""
171-
f̂!(x̂0next, _ , x̂0i, estim::InternalModel, model::NonLinModel, x̂0, u0, d0)
171+
f̂!(x̂0next, _ , k0, estim::InternalModel, model::NonLinModel, x̂0, u0, d0)
172172
173173
State function ``\mathbf{f̂}`` of [`InternalModel`](@ref) for [`NonLinModel`](@ref).
174174
175-
It calls `model.solver_f!(x̂0next, x̂0i, x̂0, u0 ,d0, model.p)` directly since this estimator
175+
It calls `model.solver_f!(x̂0next, k0, x̂0, u0 ,d0, model.p)` directly since this estimator
176176
does not augment the states.
177177
"""
178-
function f̂!(x̂0next, _ , x̂0i, ::InternalModel, model::NonLinModel, x̂0, u0, d0)
179-
return model.solver_f!(x̂0next, x̂0i, x̂0, u0, d0, model.p)
178+
function f̂!(x̂0next, _ , k0, ::InternalModel, model::NonLinModel, x̂0, u0, d0)
179+
return model.solver_f!(x̂0next, k0, x̂0, u0, d0, model.p)
180180
end
181181

182182
@doc raw"""

src/estimator/mhe/construct.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,9 +1356,9 @@ function get_optim_functions(
13561356
∇J_prep = prepare_gradient(Jfunc!, grad, Z̃_∇J, ∇J_context...; strict)
13571357
estim.Nk[] = 0
13581358
∇J = Vector{JNT}(undef, nZ̃)
1359-
function update_objective!(J, ∇J, , Z̃arg)
1360-
if isdifferent(Z̃arg, )
1361-
.= Z̃arg
1359+
function update_objective!(J, ∇J, Z̃_∇J, Z̃arg)
1360+
if isdifferent(Z̃arg, Z̃_∇J)
1361+
Z̃_∇J .= Z̃arg
13621362
J[], _ = value_and_gradient!(Jfunc!, ∇J, ∇J_prep, grad, Z̃_∇J, ∇J_context...)
13631363
end
13641364
end
@@ -1387,10 +1387,10 @@ function get_optim_functions(
13871387
estim.con.i_g .= false
13881388
estim.Nk[] = 0
13891389
∇g = init_diffmat(JNT, jac, ∇g_prep, nZ̃, ng)
1390-
function update_con!(g, ∇g, , Z̃arg)
1391-
if isdifferent(Z̃arg, )
1392-
.= Z̃arg
1393-
value_and_jacobian!(gfunc!, g, ∇g, ∇g_prep, jac, , ∇g_context...)
1390+
function update_con!(g, ∇g, Z̃_∇g, Z̃arg)
1391+
if isdifferent(Z̃arg, Z̃_∇g)
1392+
Z̃_∇g .= Z̃arg
1393+
value_and_jacobian!(gfunc!, g, ∇g, ∇g_prep, jac, Z̃_∇g, ∇g_context...)
13941394
end
13951395
end
13961396
gfuncs = Vector{Function}(undef, ng)

src/estimator/mhe/execute.jl

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -362,41 +362,19 @@ end
362362
363363
Optimize objective of `estim` [`MovingHorizonEstimator`](@ref) and return the solution `Z̃`.
364364
365-
If supported by `estim.optim`, it warm-starts the solver at:
366-
```math
367-
\mathbf{Z̃_s} =
368-
\begin{bmatrix}
369-
ϵ_{k-1} \\
370-
\mathbf{x̂}_{k-1}(k-N_k+p) \\
371-
\mathbf{ŵ}_{k-1}(k-N_k+p+0) \\
372-
\mathbf{ŵ}_{k-1}(k-N_k+p+1) \\
373-
\vdots \\
374-
\mathbf{ŵ}_{k-1}(k-p-2) \\
375-
\mathbf{0} \\
376-
\end{bmatrix}
377-
```
378-
where ``\mathbf{ŵ}_{k-1}(k-j)`` is the input increment for time ``k-j`` computed at the
379-
last time step ``k-1``. It then calls `JuMP.optimize!(estim.optim)` and extract the
380-
solution. A failed optimization prints an `@error` log in the REPL and returns the
381-
warm-start value. A failed optimization also prints [`getinfo`](@ref) results in
382-
the debug log [if activated](@extref Julia Example:-Enable-debug-level-messages).
365+
If first warm-starts the solver with [`set_warmstart_mhe!`](@ref). It then calls
366+
`JuMP.optimize!(estim.optim)` and extract the solution. A failed optimization prints an
367+
`@error` log in the REPL and returns the warm-start value. A failed optimization also prints
368+
[`getinfo`](@ref) results in the debug log [if activated](@extref Julia Example:-Enable-debug-level-messages).
383369
"""
384370
function optim_objective!(estim::MovingHorizonEstimator{NT}) where NT<:Real
385371
model, optim, buffer = estim.model, estim.optim, estim.buffer
386-
nu, ny, nk = model.nu, model.ny, model.nk
387-
nx̂, nym, nŵ, nϵ, Nk = estim.nx̂, estim.nym, estim.nx̂, estim.nϵ, estim.Nk[]
372+
nym, nx̂, nŵ, nϵ, Nk = estim.nym, estim.nx̂, estim.nx̂, estim.nϵ, estim.Nk[]
388373
nx̃ =+ nx̂
389374
Z̃var::Vector{JuMP.VariableRef} = optim[:Z̃var]
390-
= Vector{NT}(undef, nym*Nk)
391-
X̂0 = Vector{NT}(undef, nx̂*Nk)
392-
û0, ŷ0, x̄, k0 = buffer.û, buffer.ŷ, buffer.x̂, buffer.k
393-
ϵ_0 = estim. 0 ? estim.Z̃[begin] : empty(estim.Z̃)
394-
Z̃s = [ϵ_0; estim.x̂0arr_old; estim.Ŵ]
395-
V̂, X̂0 = predict!(V̂, X̂0, û0, k0, ŷ0, estim, model, Z̃s)
396-
J_0 = obj_nonlinprog!(x̄, estim, model, V̂, Z̃s)
397-
# warm-start Z̃s with Ŵ=0 if objective or constraint function not finite :
398-
isfinite(J_0) || (Z̃s = [ϵ_0; estim.x̂0arr_old; zeros(NT, nŵ*estim.He)])
399-
JuMP.set_start_value.(Z̃var, Z̃s)
375+
= Vector{NT}(undef, nym*Nk) # TODO: remove this allocation
376+
X̂0 = Vector{NT}(undef, nx̂*Nk) # TODO: remove this allocation
377+
Z̃s = set_warmstart_mhe!(V̂, X̂0, estim, Z̃var)
400378
# ------- solve optimization problem --------------
401379
try
402380
JuMP.optimize!(optim)
@@ -433,13 +411,64 @@ function optim_objective!(estim::MovingHorizonEstimator{NT}) where NT<:Real
433411
estim.Z̃ .= JuMP.value.(Z̃var)
434412
end
435413
# --------- update estimate -----------------------
414+
û0, ŷ0, k0 = buffer.û, buffer.ŷ, buffer.k
436415
estim.Ŵ[1:nŵ*Nk] .= @views estim.Z̃[nx̃+1:nx̃+nŵ*Nk] # update Ŵ with optimum for warm-start
437416
V̂, X̂0 = predict!(V̂, X̂0, û0, k0, ŷ0, estim, model, estim.Z̃)
438417
x̂0next = @views X̂0[end-nx̂+1:end]
439418
estim.x̂0 .= x̂0next
440419
return estim.
441420
end
442421

422+
@doc raw"""
423+
set_warmstart_mhe!(V̂, X̂0, estim::MovingHorizonEstimator, Z̃var) -> Z̃s
424+
425+
Set and return the warm-start value of `Z̃var` for [`MovingHorizonEstimator`](@ref).
426+
427+
If supported by `estim.optim`, it warm-starts the solver at:
428+
```math
429+
\mathbf{Z̃_s} =
430+
\begin{bmatrix}
431+
ϵ_{k-1} \\
432+
\mathbf{x̂}_{k-1}(k-N_k+p) \\
433+
\mathbf{ŵ}_{k-1}(k-N_k+p+0) \\
434+
\mathbf{ŵ}_{k-1}(k-N_k+p+1) \\
435+
\vdots \\
436+
\mathbf{ŵ}_{k-1}(k-p-2) \\
437+
\mathbf{0} \\
438+
\end{bmatrix}
439+
```
440+
where ``ϵ(k-1)``, ``\mathbf{x̂}_{k-1}(k-N_k+p)`` and ``\mathbf{ŵ}_{k-1}(k-j)`` are
441+
respectively the slack variable, the arrival state estimate and the process noise estimates
442+
computed at the last time step ``k-1``. If the objective function is not finite at this
443+
point, all the process noises ``\mathbf{ŵ}_{k-1}(k-j)`` are warm-started at zeros. The
444+
method mutates all the arguments.
445+
"""
446+
function set_warmstart_mhe!(V̂, X̂0, estim::MovingHorizonEstimator{NT}, Z̃var) where NT<:Real
447+
model, buffer = estim.model, estim.buffer
448+
nϵ, nx̂, nŵ, nZ̃, Nk = estim.nϵ, estim.nx̂, estim.nx̂, length(estim.Z̃), estim.Nk[]
449+
nx̃ =+ nx̂
450+
Z̃s = Vector{NT}(undef, nZ̃) # TODO: remove this allocation
451+
û0, ŷ0, x̄, k0 = buffer.û, buffer.ŷ, buffer.x̂, buffer.k
452+
# --- slack variable ϵ ---
453+
estim.== 1 && (Z̃s[begin] = estim.Z̃[begin])
454+
# --- arrival state estimate x̂0arr ---
455+
Z̃s[nϵ+1:nx̃] = estim.x̂0arr_old
456+
# --- process noise estimates Ŵ ---
457+
Z̃s[nx̃+1:end] = estim.
458+
# verify definiteness of objective function:
459+
V̂, X̂0 = predict!(V̂, X̂0, û0, k0, ŷ0, estim, model, Z̃s)
460+
Js = obj_nonlinprog!(x̄, estim, model, V̂, Z̃s)
461+
if !isfinite(Js)
462+
Z̃s[nx̃+1:end] = 0
463+
end
464+
# --- unused variable in Z̃ (applied only when Nk ≠ He) ---
465+
# We force the update of the NLP gradient and jacobian by warm-starting the unused
466+
# variable in Z̃ at 1. Since estim.Ŵ is initialized with 0s, at least 1 variable in Z̃s
467+
# will be inevitably different at the following time step.
468+
Z̃s[nx̃+Nk*nŵ+1:end] .= 1
469+
JuMP.set_start_value.(Z̃var, Z̃s)
470+
end
471+
443472
"Correct the covariance estimate at arrival using `covestim` [`StateEstimator`](@ref)."
444473
function correct_cov!(estim::MovingHorizonEstimator)
445474
nym, nd = estim.nym, estim.model.nd

test/2_test_state_estim.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,29 @@ end
10641064
@test_throws ErrorException setstate!(mhe1, [1,2,3,4,5,6], diagm(.1:.1:.6))
10651065
end
10661066

1067+
@testitem "MovingHorizonEstimator estimation with unfilled window" setup=[SetupMPCtests] begin
1068+
f(x,u,_,_) = 0.5x + u
1069+
h(x,_,_) = x
1070+
model = NonLinModel(f, h, 10.0, 1, 1, 1, solver=nothing)
1071+
mhe1 = MovingHorizonEstimator(model, nint_u=[1], He=3, direct=true)
1072+
for i = 1:40
1073+
y = model()
1074+
= preparestate!(mhe1, y)
1075+
updatestate!(mhe1, [0.0], y)
1076+
updatestate!(model, [0.1])
1077+
end
1078+
@test mhe1() model() atol = 1e-9
1079+
model = NonLinModel(f, h, 10.0, 1, 1, 1, solver=nothing)
1080+
mhe2 = MovingHorizonEstimator(model, nint_u=[1], He=3, direct=false)
1081+
for i = 1:40
1082+
y = model()
1083+
= preparestate!(mhe2, y)
1084+
updatestate!(mhe2, [0.0], y)
1085+
updatestate!(model, [0.1])
1086+
end
1087+
@test mhe2() model() atol = 1e-9
1088+
end
1089+
10671090
@testitem "MovingHorizonEstimator fallbacks for arrival covariance estimation" setup=[SetupMPCtests] begin
10681091
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra
10691092
linmodel = setop!(LinModel(sys,Ts,i_u=[1,2], i_d=[3]), uop=[10,50], yop=[50,30], dop=[5])

0 commit comments

Comments
 (0)