Skip to content

Commit 35ee483

Browse files
authored
Merge pull request #151 from JuliaControl/try_mhe
debug: fallback if `MovingHorizonEstimator` arrival covariance update fails
2 parents 84f6688 + c2bb875 commit 35ee483

File tree

6 files changed

+92
-16
lines changed

6 files changed

+92
-16
lines changed

Project.toml

+1-1
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.2.0"
4+
version = "1.3.0"
55

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

src/estimator/luenberger.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ function update_estimate!(estim::Luenberger, y0m, d0, u0)
137137
end
138138

139139
"Throw an error if `setmodel!` is called on `Luenberger` observer w/o the default values."
140-
function setmodel!(estim::Luenberger, model, args...)
140+
function setmodel_estimator!(estim::Luenberger, model, args...)
141141
if estim.model !== model
142142
error("Luenberger does not support setmodel!")
143143
end

src/estimator/mhe/construct.jl

+9-6
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,6 @@ struct MovingHorizonEstimator{
123123
validate_kfcov(nym, nx̂, Q̂, R̂, P̂_0)
124124
lastu0 = zeros(NT, nu)
125125
x̂0 = [zeros(NT, model.nx); zeros(NT, nxs)]
126-
P̂_0 = Hermitian(P̂_0, :L)
127-
Q̂, R̂ = Hermitian(Q̂, :L), Hermitian(R̂, :L)
128-
invP̄ = Hermitian(inv(P̂_0), :L)
129-
invQ̂_He = Hermitian(repeatdiag(inv(Q̂), He), :L)
130-
invR̂_He = Hermitian(repeatdiag(inv(R̂), He), :L)
131126
r = direct ? 0 : 1
132127
E, G, J, B, ex̄, Ex̂, Gx̂, Jx̂, Bx̂ = init_predmat_mhe(
133128
model, He, i_ym, Â, B̂u, Ĉm, B̂d, D̂dm, x̂op, f̂op, r
@@ -146,10 +141,18 @@ struct MovingHorizonEstimator{
146141
nD0 = direct ? nd*(He+1) : nd*He
147142
U0, D0 = zeros(NT, nu*He), zeros(NT, nD0)
148143
= zeros(NT, nx̂*He)
144+
buffer = StateEstimatorBuffer{NT}(nu, nx̂, nym, ny, nd)
145+
P̂_0 = Hermitian(P̂_0, :L)
146+
Q̂, R̂ = Hermitian(Q̂, :L), Hermitian(R̂, :L)
147+
P̂_0 = Hermitian(P̂_0, :L)
148+
invP̄ = inv_cholesky!(buffer.P̂, P̂_0)
149+
invQ̂ = inv_cholesky!(buffer.Q̂, Q̂)
150+
invR̂ = inv_cholesky!(buffer.R̂, R̂)
151+
invQ̂_He = Hermitian(repeatdiag(invQ̂, He), :L)
152+
invR̂_He = Hermitian(repeatdiag(invR̂, He), :L)
149153
x̂0arr_old = zeros(NT, nx̂)
150154
P̂arr_old = copy(P̂_0)
151155
Nk = [0]
152-
buffer = StateEstimatorBuffer{NT}(nu, nx̂, nym, ny, nd)
153156
corrected = [false]
154157
estim = new{NT, SM, JM, CE}(
155158
model, optim, con, covestim,

src/estimator/mhe/execute.jl

+37-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function init_estimate_cov!(estim::MovingHorizonEstimator, _ , d0, u0)
1616
estim.D0[1:estim.model.nd] .= d0
1717
end
1818
# estim.P̂_0 is in fact P̂(-1|-1) is estim.direct==false, else P̂(-1|0)
19-
estim.invP̄ .= inv(estim.P̂_0)
19+
invert_cov!(estim, estim.P̂_0)
2020
estim.P̂arr_old .= estim.P̂_0
2121
estim.x̂0arr_old .= 0
2222
return nothing
@@ -442,9 +442,17 @@ function correct_cov!(estim::MovingHorizonEstimator)
442442
y0marr, d0arr = @views estim.Y0m[1:nym], estim.D0[1:nd]
443443
estim.covestim.x̂0 .= estim.x̂0arr_old
444444
estim.covestim.P̂ .= estim.P̂arr_old
445-
correct_estimate!(estim.covestim, y0marr, d0arr)
446-
estim.P̂arr_old .= estim.covestim.
447-
estim.invP̄ .= inv(estim.P̂arr_old)
445+
try
446+
correct_estimate!(estim.covestim, y0marr, d0arr)
447+
estim.P̂arr_old .= estim.covestim.
448+
invert_cov!(estim, estim.P̂arr_old)
449+
catch err
450+
if err isa PosDefException
451+
@warn("Arrival covariance is not positive definite: keeping the old one")
452+
else
453+
rethrow()
454+
end
455+
end
448456
return nothing
449457
end
450458

@@ -454,9 +462,31 @@ function update_cov!(estim::MovingHorizonEstimator)
454462
u0arr, y0marr, d0arr = @views estim.U0[1:nu], estim.Y0m[1:nym], estim.D0[1:nd]
455463
estim.covestim.x̂0 .= estim.x̂0arr_old
456464
estim.covestim.P̂ .= estim.P̂arr_old
457-
update_estimate!(estim.covestim, y0marr, d0arr, u0arr)
458-
estim.P̂arr_old .= estim.covestim.
459-
estim.invP̄ .= inv(estim.P̂arr_old)
465+
try
466+
update_estimate!(estim.covestim, y0marr, d0arr, u0arr)
467+
estim.P̂arr_old .= estim.covestim.
468+
invert_cov!(estim, estim.P̂arr_old)
469+
catch err
470+
if err isa PosDefException
471+
@warn("Arrival covariance is not positive definite: keeping the old one")
472+
else
473+
rethrow()
474+
end
475+
end
476+
return nothing
477+
end
478+
479+
"Invert the covariance estimate at arrival `P̄`."
480+
function invert_cov!(estim::MovingHorizonEstimator, P̄)
481+
try
482+
estim.invP̄ .= inv_cholesky!(estim.buffer.P̂, P̄)
483+
catch err
484+
if err isa PosDefException
485+
@warn("Arrival covariance is not invertible: keeping the old one")
486+
else
487+
rethrow()
488+
end
489+
end
460490
return nothing
461491
end
462492

src/general.jl

+15-1
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,18 @@ end
5959
to_hermitian(A::AbstractVector) = Hermitian(reshape(A, 1, 1), :L)
6060
to_hermitian(A::AbstractMatrix) = Hermitian(A, :L)
6161
to_hermitian(A::Hermitian) = A
62-
to_hermitian(A) = A
62+
to_hermitian(A) = A
63+
64+
"""
65+
Compute the inverse of a the Hermitian positive definite matrix `A` using `cholesky`.
66+
67+
Builtin `inv` function uses LU factorization which is not the best choice for Hermitian
68+
positive definite matrices. The function will mutate `buffer` to reduce memory allocations.
69+
"""
70+
function inv_cholesky!(buffer::Matrix, A::Hermitian)
71+
Achol = Hermitian(buffer, :L)
72+
Achol .= A
73+
chol_obj = cholesky!(Achol)
74+
invA = Hermitian(inv(chol_obj), :L)
75+
return invA
76+
end

test/test_state_estim.jl

+29
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,35 @@ end
975975
@test info[:Ŷ][end-1:end] [50, 30] atol=1e-9
976976
end
977977

978+
@testset "MovingHorizonEstimator fallbacks for arrival covariance estimation" begin
979+
linmodel = setop!(LinModel(sys,Ts,i_u=[1,2], i_d=[3]), uop=[10,50], yop=[50,30], dop=[5])
980+
f(x,u,d,_) = linmodel.A*x + linmodel.Bu*u + linmodel.Bd*d
981+
h(x,d,_) = linmodel.C*x + linmodel.Dd*d
982+
nonlinmodel = setop!(NonLinModel(f, h, Ts, 2, 4, 2, 1, solver=nothing), uop=[10,50], yop=[50,30], dop=[5])
983+
mhe = MovingHorizonEstimator(nonlinmodel, nint_ym=0, He=1)
984+
preparestate!(mhe, [50, 30], [5])
985+
updatestate!(mhe, [10, 50], [50, 30], [5])
986+
mhe.P̂arr_old[1, 1] = -1e-3 # negative eigenvalue to trigger fallback
987+
P̂arr_old_copy = deepcopy(mhe.P̂arr_old)
988+
invP̄_copy = deepcopy(mhe.invP̄)
989+
@test_logs(
990+
(:warn, "Arrival covariance is not positive definite: keeping the old one"),
991+
preparestate!(mhe, [50, 30], [5])
992+
)
993+
@test mhe.P̂arr_old P̂arr_old_copy
994+
@test mhe.invP̄ invP̄_copy
995+
@test_logs(
996+
(:warn, "Arrival covariance is not positive definite: keeping the old one"),
997+
updatestate!(mhe, [10, 50], [50, 30], [5])
998+
)
999+
@test mhe.P̂arr_old P̂arr_old_copy
1000+
@test mhe.invP̄ invP̄_copy
1001+
@test_logs(
1002+
(:warn, "Arrival covariance is not invertible: keeping the old one"),
1003+
ModelPredictiveControl.invert_cov!(mhe, Hermitian(zeros(mhe.nx̂, mhe.nx̂),:L))
1004+
)
1005+
end
1006+
9781007
@testset "MovingHorizonEstimator set constraints" begin
9791008
linmodel1 = setop!(LinModel(sys,Ts,i_u=[1,2]), uop=[10,50], yop=[50,30])
9801009
mhe1 = MovingHorizonEstimator(linmodel1, He=1, nint_ym=0, Cwt=1e3)

0 commit comments

Comments
 (0)