Skip to content

Commit a965c12

Browse files
committed
added: custom estimator for MHE covariance at arrival (now default to UKF)
1 parent 7e64174 commit a965c12

File tree

3 files changed

+49
-45
lines changed

3 files changed

+49
-45
lines changed

src/estimator/kalman.jl

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ This syntax allows nonzero off-diagonal elements in ``\mathbf{Q̂, R̂}``.
163163
"""
164164
function SteadyKalmanFilter(model::SM, i_ym, nint_u, nint_ym, Q̂, R̂) where {NT<:Real, SM<:LinModel{NT}}
165165
Q̂, R̂ = to_mat(Q̂), to_mat(R̂)
166-
return SteadyKalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, Q̂ , R̂)
166+
return SteadyKalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, Q̂, R̂)
167167
end
168168

169169

@@ -315,7 +315,7 @@ This syntax allows nonzero off-diagonal elements in ``\mathbf{P̂}_{-1}(0), \mat
315315
"""
316316
function KalmanFilter(model::SM, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂) where {NT<:Real, SM<:LinModel{NT}}
317317
P̂0, Q̂, R̂ = to_mat(P̂0), to_mat(Q̂), to_mat(R̂)
318-
return KalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂0, Q̂ , R̂)
318+
return KalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂)
319319
end
320320

321321
@doc raw"""
@@ -478,14 +478,14 @@ function UnscentedKalmanFilter(
478478
end
479479

480480
@doc raw"""
481-
UnscentedKalmanFilter(model, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, α, β, κ)
481+
UnscentedKalmanFilter(model, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, α=1e-3, β=2, κ=0)
482482
483483
Construct the estimator from the augmented covariance matrices `P̂0`, `Q̂` and `R̂`.
484484
485485
This syntax allows nonzero off-diagonal elements in ``\mathbf{P̂}_{-1}(0), \mathbf{Q̂, R̂}``.
486486
"""
487487
function UnscentedKalmanFilter(
488-
model::SM, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, α, β, κ
488+
model::SM, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, α=1e-3, β=2, κ=0
489489
) where {NT<:Real, SM<:SimModel{NT}}
490490
P̂0, Q̂, R̂ = to_mat(P̂0), to_mat(Q̂), to_mat(R̂)
491491
return UnscentedKalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂0, Q̂ , R̂, α, β, κ)
@@ -742,7 +742,7 @@ This syntax allows nonzero off-diagonal elements in ``\mathbf{P̂}_{-1}(0), \mat
742742
"""
743743
function ExtendedKalmanFilter(model::SM, i_ym, nint_u, nint_ym,P̂0, Q̂, R̂) where {NT<:Real, SM<:SimModel{NT}}
744744
P̂0, Q̂, R̂ = to_mat(P̂0), to_mat(Q̂), to_mat(R̂)
745-
return ExtendedKalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂0, Q̂ , R̂)
745+
return ExtendedKalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂)
746746
end
747747

748748

@@ -759,7 +759,8 @@ substitutions ``\mathbf{Â = F̂}(k)`` and ``\mathbf{Ĉ^m = Ĥ^m}(k)``:
759759
[\mathbf{Ĥ^m}(k)\mathbf{P̂}_{k-1}(k)\mathbf{Ĥ^m}'(k) + \mathbf{R̂}]^{-1} \\
760760
\mathbf{K̂}(k) &= \mathbf{F̂}(k) \mathbf{M̂}(k) \\
761761
\mathbf{ŷ^m}(k) &= \mathbf{ĥ^m}\Big( \mathbf{x̂}_{k-1}(k), \mathbf{d}(k) \Big) \\
762-
\mathbf{x̂}_{k}(k+1) &= \mathbf{f̂}\Big( \mathbf{x̂}_{k-1}(k), \mathbf{u}(k), \mathbf{d}(k) \Big)
762+
\mathbf{x̂}_{k}(k+1) &= \math, covestim::CE,
763+
) bf{f̂}\Big( \mathbf{x̂}_{k-1}(k), \mathbf{u}(k), \mathbf{d}(k) \Big)
763764
+ \mathbf{K̂}(k)[\mathbf{y^m}(k) - \mathbf{ŷ^m}(k)] \\
764765
\mathbf{P̂}_{k}(k+1) &= \mathbf{F̂}(k)[\mathbf{P̂}_{k-1}(k)
765766
- \mathbf{M̂}(k)\mathbf{Ĥ^m}(k)\mathbf{P̂}_{k-1}(k)]\mathbf{F̂}'(k)
@@ -770,7 +771,8 @@ substitutions ``\mathbf{Â = F̂}(k)`` and ``\mathbf{Ĉ^m = Ĥ^m}(k)``:
770771
automatically computes the Jacobians:
771772
```math
772773
\begin{aligned}
773-
\mathbf{F̂}(k) &= \left. \frac{∂\mathbf{f̂}(\mathbf{x̂}, \mathbf{u}, \mathbf{d})}{∂\mathbf{x̂}} \right|_{\mathbf{x̂ = x̂}_{k-1}(k),\, \mathbf{u = u}(k),\, \mathbf{d = d}(k)} \\
774+
\mathbf{F̂}(k) &= \left. \fra, covestim::CE,
775+
) c{∂\mathbf{f̂}(\mathbf{x̂}, \mathbf{u}, \mathbf{d})}{∂\mathbf{x̂}} \right|_{\mathbf{x̂ = x̂}_{k-1}(k),\, \mathbf{u = u}(k),\, \mathbf{d = d}(k)} \\
774776
\mathbf{Ĥ}(k) &= \left. \frac{∂\mathbf{ĥ}(\mathbf{x̂}, \mathbf{d})}{∂\mathbf{x̂}} \right|_{\mathbf{x̂ = x̂}_{k-1}(k),\, \mathbf{d = d}(k)}
775777
\end{aligned}
776778
```

src/estimator/mhe/construct.jl

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,16 @@ end
4343

4444
struct MovingHorizonEstimator{
4545
NT<:Real,
46-
SM<:SimModel,
47-
JM<:JuMP.GenericModel
46+
SM<:SimModel,
47+
JM<:JuMP.GenericModel,
48+
CE<:StateEstimator,
4849
} <: StateEstimator{NT}
4950
model::SM
5051
# note: `NT` and the number type `JNT` in `JuMP.GenericModel{JNT}` can be
5152
# different since solvers that support non-Float64 are scarce.
5253
optim::JM
5354
con::EstimatorConstraint{NT}
55+
covestim::CE
5456
::Vector{NT}
5557
lastu0::Vector{NT}
5658
::Vector{NT}
@@ -95,17 +97,16 @@ struct MovingHorizonEstimator{
9597
x̂arr_old::Vector{NT}
9698
P̂arr_old::Hermitian{NT, Matrix{NT}}
9799
Nk::Vector{Int}
98-
function MovingHorizonEstimator{NT, SM, JM}(
99-
model::SM, He, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, Cwt, optim::JM
100-
) where {NT<:Real, SM<:SimModel{NT}, JM<:JuMP.GenericModel}
100+
function MovingHorizonEstimator{NT, SM, JM, CE}(
101+
model::SM, He, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, Cwt, optim::JM, covestim::CE
102+
) where {NT<:Real, SM<:SimModel{NT}, JM<:JuMP.GenericModel, CE<:StateEstimator{NT}}
101103
nu, nd = model.nu, model.nd
102104
He < 1 && throw(ArgumentError("Estimation horizon He should be ≥ 1"))
103105
Cwt < 0 && throw(ArgumentError("Cwt weight should be ≥ 0"))
104106
nym, nyu = validate_ym(model, i_ym)
105107
As, Cs_u, Cs_y, nint_u, nint_ym = init_estimstoch(model, i_ym, nint_u, nint_ym)
106108
nxs = size(As, 1)
107109
nx̂ = model.nx + nxs
108-
nŵ = nx̂
109110
Â, B̂u, Ĉ, B̂d, D̂d = augment_model(model, As, Cs_u, Cs_y)
110111
validate_kfcov(nym, nx̂, Q̂, R̂, P̂0)
111112
lastu0 = zeros(NT, model.nu)
@@ -129,8 +130,8 @@ struct MovingHorizonEstimator{
129130
x̂arr_old = zeros(NT, nx̂)
130131
P̂arr_old = copy(P̂0)
131132
Nk = [0]
132-
estim = new{NT, SM, JM}(
133-
model, optim, con,
133+
estim = new{NT, SM, JM, CE}(
134+
model, optim, con, covestim,
134135
Z̃, lastu0, x̂,
135136
He,
136137
i_ym, nx̂, nym, nyu, nxs,
@@ -188,7 +189,7 @@ The vectors ``\mathbf{Ŵ}`` and ``\mathbf{V̂}`` encompass the estimated proces
188189
``\mathbf{ŵ}(k-j)`` and sensor noise ``\mathbf{v̂}(k-j)`` from ``j=N_k-1`` to ``0``. The
189190
Extended Help defines the two vectors and the scalar ``ϵ``. See [`UnscentedKalmanFilter`](@ref)
190191
for details on the augmented process model and ``\mathbf{R̂}, \mathbf{Q̂}`` covariances. The
191-
matrix ``\mathbf{P̂}_{k-N_k}(k-N_k+1)`` is estimated with an [`ExtendedKalmanFilter`](@ref).
192+
covariance ``\mathbf{P̂}_{k-N_k}(k-N_k+1)`` is estimated with an [`ExtendedKalmanFilter`](@ref).
192193
193194
!!! warning
194195
See the Extended Help if you get an error like:
@@ -262,7 +263,7 @@ MovingHorizonEstimator estimator with a sample time Ts = 10.0 s, Ipopt optimizer
262263
"""
263264
function MovingHorizonEstimator(
264265
model::SM;
265-
He::Union{Int, Nothing}=nothing,
266+
He::Union{Int, Nothing} = nothing,
266267
i_ym::IntRangeOrVector = 1:model.ny,
267268
σP0::Vector = fill(1/model.nx, model.nx),
268269
σQ ::Vector = fill(1/model.nx, model.nx),
@@ -280,33 +281,40 @@ function MovingHorizonEstimator(
280281
P̂0 = Hermitian(diagm(NT[σP0; σP0int_u; σP0int_ym].^2), :L)
281282
= Hermitian(diagm(NT[σQ; σQint_u; σQint_ym ].^2), :L)
282283
= Hermitian(diagm(NT[σR;].^2), :L)
283-
isnothing(He) && throw(ArgumentError("Estimation horizon He must be explicitly specified"))
284-
return MovingHorizonEstimator{NT, SM, JM}(
285-
model, He, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, Cwt, optim
286-
)
284+
isnothing(He) && throw(ArgumentError("Estimation horizon He must be explicitly specified"))
285+
return MovingHorizonEstimator(model, He, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, Cwt, optim)
287286
end
288287

289-
"Return a `JuMP.Model` with OSQP optimizer if `model` is a [`LinModel`](@ref)."
290288
default_optim_mhe(::LinModel) = JuMP.Model(DEFAULT_LINMHE_OPTIMIZER, add_bridges=false)
291-
"Else, return it with Ipopt optimizer."
292289
default_optim_mhe(::SimModel) = JuMP.Model(DEFAULT_NONLINMHE_OPTIMIZER, add_bridges=false)
293290

294291
@doc raw"""
295-
MovingHorizonEstimator(model, He, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, Cwt, optim)
292+
MovingHorizonEstimator(model, He, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, Cwt, optim[, covestim])
296293
297294
Construct the estimator from the augmented covariance matrices `P̂0`, `Q̂` and `R̂`.
298295
299296
This syntax allows nonzero off-diagonal elements in ``\mathbf{P̂}_{-1}(0), \mathbf{Q̂, R̂}``.
297+
The final argument `covestim` also allows specifying a custom [`StateEstimator`](@ref)
298+
object for the estimation of covariance at the arrival ``\mathbf{P̂}_{k-N_k}(k-N_k+1)``. The
299+
supported types are [`KalmanFilter`](@ref), [`UnscentedKalmanFilter`](@ref) and
300+
[`ExtendedKalmanFilter`](@ref).
300301
"""
301302
function MovingHorizonEstimator(
302-
model::SM, He, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, Cwt, optim::JM
303-
) where {NT<:Real, SM<:SimModel{NT}, JM<:JuMP.GenericModel}
303+
model::SM, He, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, Cwt, optim::JM,
304+
covestim::CE = default_covestim_mhe(model, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂)
305+
) where {NT<:Real, SM<:SimModel{NT}, JM<:JuMP.GenericModel, CE<:StateEstimator{NT}}
304306
P̂0, Q̂, R̂ = to_mat(P̂0), to_mat(Q̂), to_mat(R̂)
305-
return MovingHorizonEstimator{NT, SM, JM}(
306-
model, He, i_ym, nint_u, nint_ym, P̂0, Q̂ , R̂, Cwt, optim
307+
return MovingHorizonEstimator{NT, SM, JM, CE}(
308+
model, He, i_ym, nint_u, nint_ym, P̂0, Q̂ , R̂, Cwt, optim, covestim
307309
)
308310
end
309311

312+
function default_covestim_mhe(model::LinModel, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂)
313+
return KalmanFilter(model, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂)
314+
end
315+
function default_covestim_mhe(model::SimModel, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂)
316+
return UnscentedKalmanFilter(model, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂)
317+
end
310318

311319
@doc raw"""
312320
setconstraint!(estim::MovingHorizonEstimator; <keyword arguments>) -> estim

src/estimator/mhe/execute.jl

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,7 @@ function update_estimate!(estim::MovingHorizonEstimator{NT}, u, ym, d) where NT<
7878
estim.Ŵ[1:nŵ*Nk] .= @views estim.Z̃[nx̃+1:nx̃+nŵ*Nk] # update Ŵ with optimum for warm-start
7979
V̂, X̂ = predict!(V̂, X̂, ŷ, estim, model, estim.Z̃)
8080
x̂ .= X̂[end-nx̂+1:end]
81-
if Nk == estim.He
82-
uarr, ymarr, darr = estim.U[1:model.nu], estim.Ym[1:nym], estim.D[1:model.nd]
83-
update_cov!(estim, model, estim.x̂arr_old, estim.P̂arr_old, uarr, ymarr, darr)
84-
estim.invP̄.data .= Hermitian(inv(estim.P̂arr_old), :L)
85-
end
81+
Nk == estim.He && update_cov!(estim::MovingHorizonEstimator)
8682
return nothing
8783
end
8884

@@ -323,20 +319,18 @@ function trunc_bounds(estim::MovingHorizonEstimator{NT}, Bmin, Bmax, n) where NT
323319
return Bmin_t, Bmax_t
324320
end
325321

326-
"Update the covariance `P̂` with the `KalmanFilter` if `model` is a `LinModel`."
327-
function update_cov!(estim::MovingHorizonEstimator, ::LinModel, _ , P̂, u, ym, d)
328-
return update_estimate_kf!(estim, u, ym, d, estim.Â, estim.Ĉ[estim.i_ym, :], P̂)
329-
end
330-
"Update it with the `ExtendedKalmanFilter` if model is not a `LinModel`."
331-
function update_cov!(estim::MovingHorizonEstimator, model::SimModel, x̂, P̂, u, ym, d)
332-
# TODO: also support UnscentedKalmanFilter
333-
x̂next, ŷ = similar(estim.x̂), similar(model.yop)
334-
= ForwardDiff.jacobian((x̂next, x̂) -> f̂!(x̂next, estim, estim.model, x̂, u, d), x̂next, x̂)
335-
= ForwardDiff.jacobian((ŷ, x̂) -> ĥ!(ŷ, estim, estim.model, x̂, d), ŷ, x̂)
336-
return update_estimate_kf!(estim, u, ym, d, F̂, Ĥ[estim.i_ym, :], P̂)
322+
"Update the covariance estimate at arrival using `covestim` [`StateEstimator`](@ref)."
323+
function update_cov!(estim::MovingHorizonEstimator)
324+
nu, nd, nym = estim.model.nu, estim.model.nd, estim.nym
325+
uarr, ymarr, darr = @views estim.U[1:nu], estim.Ym[1:nym], estim.D[1:nd]
326+
estim.covestim.x̂ .= estim.x̂arr_old
327+
estim.covestim..data .= estim.P̂arr_old # .data is necessary for Hermitian
328+
update_estimate!(estim.covestim, uarr, ymarr, darr)
329+
estim.P̂arr_old.data .= estim.covestim.
330+
estim.invP̄.data .= Hermitian(inv(estim.P̂arr_old), :L)
331+
return nothing
337332
end
338333

339-
340334
"""
341335
obj_nonlinprog!( _ , estim::MovingHorizonEstimator, ::LinModel, _ , Z̃)
342336

0 commit comments

Comments
 (0)