Skip to content

Commit 61de78d

Browse files
authored
Merge pull request #214 from JuliaControl/mhe_inv_inplace
added: `inv!` for `MovingHorizonEstimator` covariance matrices
2 parents 20a5ed0 + 13387b4 commit 61de78d

File tree

4 files changed

+55
-27
lines changed

4 files changed

+55
-27
lines changed

src/estimator/mhe/construct.jl

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,15 @@ struct MovingHorizonEstimator{
159159
buffer = StateEstimatorBuffer{NT}(nu, nx̂, nym, ny, nd, nk)
160160
P̂_0 = Hermitian(P̂_0, :L)
161161
Q̂, R̂ = Hermitian(Q̂, :L), Hermitian(R̂, :L)
162-
P̂_0 = Hermitian(P̂_0, :L)
163-
invP̄ = inv_cholesky!(buffer.P̂, P̂_0)
164-
invQ̂ = inv_cholesky!(buffer.Q̂, Q̂)
165-
invR̂ = inv_cholesky!(buffer.R̂, R̂)
162+
invP̄ = Hermitian(buffer.P̂, :L)
163+
invP̄ .= P̂_0
164+
inv!(invP̄)
165+
invQ̂ = Hermitian(buffer.Q̂, :L)
166+
invQ̂ .=
167+
inv!(invQ̂)
168+
invR̂ = Hermitian(buffer.R̂, :L)
169+
invR̂ .=
170+
inv!(invR̂)
166171
invQ̂_He = Hermitian(repeatdiag(invQ̂, He), :L)
167172
invR̂_He = Hermitian(repeatdiag(invR̂, He), :L)
168173
x̂0arr_old = zeros(NT, nx̂)
@@ -684,7 +689,7 @@ function setconstraint!(
684689
con.A_Ŵmin, con.A_Ŵmax, con.A_V̂min, con.A_V̂max
685690
)
686691
A = con.A[con.i_b, :]
687-
b = con.b[con.i_b]
692+
b = zeros(count(con.i_b)) # dummy value, updated before optimization (avoid ±Inf)
688693
Z̃var = optim[:Z̃var]
689694
JuMP.delete(optim, optim[:linconstraint])
690695
JuMP.unregister(optim, :linconstraint)

src/estimator/mhe/execute.jl

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,10 @@ end
528528

529529
"Invert the covariance estimate at arrival `P̄`."
530530
function invert_cov!(estim::MovingHorizonEstimator, P̄)
531+
invP̄ = Hermitian(estim.buffer.P̂, :L)
532+
invP̄ .=
531533
try
532-
estim.invP̄ .= inv_cholesky!(estim.buffer.P̂, P̄)
534+
inv!(invP̄)
533535
catch err
534536
if err isa PosDefException
535537
@error("Arrival covariance P̄ is not invertible: keeping the old one")
@@ -759,7 +761,7 @@ function setmodel_estimator!(
759761
con.A_V̂max
760762
]
761763
A = con.A[con.i_b, :]
762-
b = con.b[con.i_b]
764+
b = zeros(count(con.i_b)) # dummy value, updated before optimization (avoid ±Inf)
763765
Z̃var::Vector{JuMP.VariableRef} = estim.optim[:Z̃var]
764766
JuMP.delete(estim.optim, estim.optim[:linconstraint])
765767
JuMP.unregister(estim.optim, :linconstraint)
@@ -792,11 +794,17 @@ function setmodel_estimator!(
792794
# --- covariance matrices ---
793795
if !isnothing(Q̂)
794796
estim.Q̂ .= to_hermitian(Q̂)
795-
estim.invQ̂_He .= repeatdiag(inv(estim.Q̂), He)
797+
invQ̂ = Hermitian(estim.buffer.Q̂, :L)
798+
invQ̂ .= estim.
799+
inv!(invQ̂)
800+
estim.invQ̂_He .= Hermitian(repeatdiag(invQ̂, He), :L)
796801
end
797802
if !isnothing(R̂)
798803
estim.R̂ .= to_hermitian(R̂)
799-
estim.invR̂_He .= repeatdiag(inv(estim.R̂), He)
804+
invR̂ = Hermitian(estim.buffer.R̂, :L)
805+
invR̂ .= estim.
806+
inv!(invR̂)
807+
estim.invR̂_He .= Hermitian(repeatdiag(invR̂, He), :L)
800808
end
801809
return nothing
802810
end

src/general.jl

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,30 @@ to_hermitian(A::Hermitian) = A
8484
to_hermitian(A) = A
8585

8686
"""
87-
Compute the inverse of a the Hermitian positive definite matrix `A` using `cholesky`.
87+
Compute the inverse of a the Hermitian positive definite matrix `A` in-place and return it.
8888
89-
Builtin `inv` function uses LU factorization which is not the best choice for Hermitian
90-
positive definite matrices. The function will mutate `buffer` to reduce memory allocations.
89+
There is 3 methods for this function:
90+
- If `A` is a `Hermitian{<Real, Matrix{<:Real}}`, it uses `LAPACK.potrf!` and
91+
`LAPACK.potri!` functions to compute the Cholesky factor and then the inverse. This is
92+
allocation-free. See <https://tinyurl.com/4pwdwbcj> for the source.
93+
- If `A` is a `Hermitian{<Real, Diagonal{<:Real, Vector{<:Real}}}`, it computes the
94+
inverse of the diagonal elements in-place (allocation-free).
95+
- Else if `A` is a `Hermitian{<:Real, <:AbstractMatrix}`, it computes the Cholesky factor
96+
with `cholesky!` and then the inverse with `inv`, which allocates memory.
9197
"""
92-
function inv_cholesky!(buffer::Matrix, A::Hermitian)
93-
Achol = Hermitian(buffer, :L)
94-
Achol .= A
95-
chol_obj = cholesky!(Achol)
96-
invA = Hermitian(inv(chol_obj), :L)
97-
return invA
98+
function inv!(A::Hermitian{NT, Matrix{NT}}) where {NT<:Real}
99+
_, info = LAPACK.potrf!(A.uplo, A.data)
100+
(info == 0) || throw(PosDefException(info))
101+
LAPACK.potri!(A.uplo, A.data)
102+
return A
103+
end
104+
function inv!(A::Hermitian{NT, Diagonal{NT, Vector{NT}}}) where {NT<:Real}
105+
A.data.diag .= 1 ./ A.data.diag
106+
return A
107+
end
108+
function inv!(A::Hermitian{<:Real, <:AbstractMatrix})
109+
Achol = cholesky!(A)
110+
invA = inv(Achol)
111+
A .= Hermitian(invA, :L)
112+
return A
98113
end

test/2_test_state_estim.jl

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,8 @@ end
13181318
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra
13191319
linmodel = LinModel(ss(0.5, 0.3, 1.0, 0, 10.0))
13201320
linmodel = setop!(linmodel, uop=[2.0], yop=[50.0], xop=[3.0], fop=[3.0])
1321-
mhe = MovingHorizonEstimator(linmodel, He=1, nint_ym=0, direct=false)
1321+
He = 5
1322+
mhe = MovingHorizonEstimator(linmodel; He, nint_ym=0, direct=false)
13221323
setconstraint!(mhe, x̂min=[-1000], x̂max=[1000])
13231324
@test mhe. [0.5]
13241325
@test evaloutput(mhe) [50.0]
@@ -1331,19 +1332,18 @@ end
13311332
@test mhe. [0.2]
13321333
@test evaloutput(mhe) [55.0]
13331334
@test mhe.lastu0 [2.0 - 3.0]
1334-
@test mhe.U0 [2.0 - 3.0]
1335-
@test mhe.Y0m [50.0 - 55.0]
1336-
preparestate!(mhe, [55.0])
1337-
= updatestate!(mhe, [3.0], [55.0])
1335+
@test mhe.U0 repeat([2.0 - 3.0], He)
1336+
@test mhe.Y0m repeat([50.0 - 55.0], He)
1337+
= preparestate!(mhe, [55.0])
13381338
@test [3.0]
13391339
newlinmodel = setop!(newlinmodel, uop=[3.0], yop=[55.0], xop=[8.0], fop=[8.0])
13401340
setmodel!(mhe, newlinmodel)
13411341
@test mhe.x̂0 [3.0 - 8.0]
13421342
@test mhe.Z̃[1] 3.0 - 8.0
1343-
@test mhe.X̂0 [3.0 - 8.0]
1343+
@test mhe.X̂0 repeat([3.0 - 8.0], He)
13441344
@test mhe.x̂0arr_old [3.0 - 8.0]
1345-
@test mhe.con.X̂0min [-1000 - 8.0]
1346-
@test mhe.con.X̂0max [+1000 - 8.0]
1345+
@test mhe.con.X̂0min repeat([-1000 - 8.0], He)
1346+
@test mhe.con.X̂0max repeat([+1000 - 8.0], He)
13471347
@test mhe.con.x̃0min [-1000 - 8.0]
13481348
@test mhe.con.x̃0max [+1000 - 8.0]
13491349
setmodel!(mhe, Q̂=[1e-3], R̂=[1e-6])
@@ -1352,7 +1352,7 @@ end
13521352
f(x,u,d,model) = model.A*x + model.Bu*u + model.Bd*d
13531353
h(x,d,model) = model.C*x + model.Du*d
13541354
nonlinmodel = NonLinModel(f, h, 10.0, 1, 1, 1, p=linmodel, solver=nothing)
1355-
mhe2 = MovingHorizonEstimator(nonlinmodel, He=1, nint_ym=0)
1355+
mhe2 = MovingHorizonEstimator(nonlinmodel; He, nint_ym=0)
13561356
setmodel!(mhe2, Q̂=[1e-3], R̂=[1e-6])
13571357
@test mhe2. [1e-3]
13581358
@test mhe2. [1e-6]

0 commit comments

Comments
 (0)