Skip to content

Commit 1ed3284

Browse files
authored
Merge pull request #248 from JuliaControl/skf_mhe
added: `covestim` in `MovingHorizonEstimator` now supports `SteadyKalmanFilter`
2 parents bc08d73 + e0e7c84 commit 1ed3284

File tree

6 files changed

+105
-49
lines changed

6 files changed

+105
-49
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5"
2121
SparseMatrixColorings = "0a514795-09f3-496d-8182-132a7b665d35"
2222

2323
[compat]
24-
ControlSystemsBase = "1.9"
24+
ControlSystemsBase = "1.18.2"
2525
DAQP = "0.6, 0.7.1"
2626
DifferentiationInterface = "0.6.45, 0.7"
2727
Documenter = "1"

src/estimator/construct.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@ struct KalmanCovariances{
5656
::Q̂C, R̂::R̂C, P̂_0, He
5757
) where {NT<:Real, Q̂C<:AbstractMatrix{NT}, R̂C<:AbstractMatrix{NT}}
5858
if isnothing(P̂_0)
59-
P̂_0 = zeros(NT, 0, 0)
59+
P̂_0 = zeros(NT, 0, 0) # does not apply to steady-state Kalman filter
60+
= zeros(NT, size(Q̂)) # will hold the steady-state error covariance
61+
else
62+
= copy(P̂_0)
6063
end
6164
P̂_0 = Hermitian(P̂_0, :L)
62-
= copy(P̂_0)
65+
= Hermitian(P̂, :L)
6366
= Hermitian(Q̂, :L)
6467
= Hermitian(R̂, :L)
6568
# the following variables are only for the moving horizon estimator:

src/estimator/kalman.jl

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
"Abstract supertype of all Kalman-type state estimators."
2+
abstract type KalmanEstimator{NT<:Real} <: StateEstimator{NT} end
3+
14
struct SteadyKalmanFilter{
25
NT<:Real,
36
SM<:LinModel,
47
KC<:KalmanCovariances
5-
} <: StateEstimator{NT}
8+
} <: KalmanEstimator{NT}
69
model::SM
710
cov ::KC
811
x̂op ::Vector{NT}
@@ -40,24 +43,8 @@ struct SteadyKalmanFilter{
4043
Â, B̂u, Ĉ, B̂d, D̂d, x̂op, f̂op = augment_model(model, As, Cs_u, Cs_y)
4144
Ĉm, D̂dm = Ĉ[i_ym, :], D̂d[i_ym, :]
4245
R̂, Q̂ = cov.R̂, cov.
43-
if ny == nym
44-
R̂_y =
45-
else
46-
R̂_y = zeros(NT, ny, ny)
47-
R̂_y[i_ym, i_ym] =
48-
R̂_y = Hermitian(R̂_y, :L)
49-
end
50-
= try
51-
ControlSystemsBase.kalman(Discrete, Â, Ĉ, Q̂, R̂_y; direct)[:, i_ym]
52-
catch my_error
53-
if isa(my_error, ErrorException)
54-
error("Cannot compute the optimal Kalman gain K̂ for the "*
55-
"SteadyKalmanFilter. You may try to remove integrators with "*
56-
"nint_u/nint_ym parameter or use the time-varying KalmanFilter.")
57-
else
58-
rethrow()
59-
end
60-
end
46+
K̂, P̂ = init_skf(i_ym, Â, Ĉ, Q̂, R̂; direct)
47+
cov.P̂ .=
6148
x̂0 = [zeros(NT, model.nx); zeros(NT, nxs)]
6249
corrected = [false]
6350
buffer = StateEstimatorBuffer{NT}(nu, nx̂, nym, ny, nd, nk)
@@ -208,6 +195,36 @@ function SteadyKalmanFilter(
208195
return SteadyKalmanFilter{NT}(model, i_ym, nint_u, nint_ym, cov; direct)
209196
end
210197

198+
"""
199+
init_skf(i_ym, Â, Ĉ, Q̂, R̂; direct=true) -> K̂, P̂
200+
201+
Initialize the steady-state Kalman gain `K̂` and estimation error covariance `P̂`.
202+
"""
203+
function init_skf(i_ym, Â, Ĉ, Q̂, R̂; direct=true)
204+
ny, nym = size(Ĉ, 1), length(i_ym)
205+
if ny != nym
206+
R̂_y = zeros(eltype(R̂), ny, ny)
207+
R̂_y[i_ym, i_ym] =
208+
= Hermitian(R̂_y, :L)
209+
end
210+
K̂, P̂ = try
211+
ControlSystemsBase.kalman(Discrete, Â, Ĉ, Q̂, R̂; direct, extra=Val(true))
212+
catch my_error
213+
if isa(my_error, ErrorException)
214+
error("Cannot compute the optimal Kalman gain K̂ for the "*
215+
"SteadyKalmanFilter. You may try to remove integrators with "*
216+
"nint_u/nint_ym parameter or use the time-varying KalmanFilter.")
217+
else
218+
rethrow()
219+
end
220+
end
221+
if ny != nym
222+
= K̂[:, i_ym]
223+
end
224+
= Hermitian(P̂, :L)
225+
return K̂, P̂
226+
end
227+
211228
"Throw an error if `setmodel!` is called on a SteadyKalmanFilter w/o the default values."
212229
function setmodel_estimator!(estim::SteadyKalmanFilter, model, _ , _ , _ , Q̂, R̂)
213230
if estim.model !== model || !isnothing(Q̂) || !isnothing(R̂)
@@ -216,12 +233,6 @@ function setmodel_estimator!(estim::SteadyKalmanFilter, model, _ , _ , _ , Q̂,
216233
return nothing
217234
end
218235

219-
"Throw an error if P̂ != nothing."
220-
function setstate_cov!(::SteadyKalmanFilter, P̂)
221-
isnothing(P̂) || error("SteadyKalmanFilter does not compute an estimation covariance matrix P̂.")
222-
return nothing
223-
end
224-
225236
@doc raw"""
226237
correct_estimate!(estim::SteadyKalmanFilter, y0m, d0)
227238
@@ -296,7 +307,7 @@ struct KalmanFilter{
296307
NT<:Real,
297308
SM<:LinModel,
298309
KC<:KalmanCovariances
299-
} <: StateEstimator{NT}
310+
} <: KalmanEstimator{NT}
300311
model::SM
301312
cov ::KC
302313
x̂op::Vector{NT}
@@ -508,7 +519,7 @@ struct UnscentedKalmanFilter{
508519
NT<:Real,
509520
SM<:SimModel,
510521
KC<:KalmanCovariances
511-
} <: StateEstimator{NT}
522+
} <: KalmanEstimator{NT}
512523
model::SM
513524
cov ::KC
514525
x̂op ::Vector{NT}
@@ -554,7 +565,7 @@ struct UnscentedKalmanFilter{
554565
nx̂ = model.nx + nxs
555566
Â, B̂u, Ĉ, B̂d, D̂d, x̂op, f̂op = augment_model(model, As, Cs_u, Cs_y)
556567
Ĉm, D̂dm = Ĉ[i_ym, :], D̂d[i_ym, :]
557-
nσ, γ, m̂, Ŝ = init_ukf(model, nx̂, α, β, κ)
568+
nσ, γ, m̂, Ŝ = init_ukf(nx̂, α, β, κ)
558569
x̂0 = [zeros(NT, model.nx); zeros(NT, nxs)]
559570
= zeros(NT, nx̂, nym)
560571
= Hermitian(zeros(NT, nym, nym), :L)
@@ -715,7 +726,7 @@ end
715726

716727

717728
@doc raw"""
718-
init_ukf(model, nx̂, α, β, κ) -> nσ, γ, m̂, Ŝ
729+
init_ukf(nx̂, α, β, κ) -> nσ, γ, m̂, Ŝ
719730
720731
Compute the [`UnscentedKalmanFilter`](@ref) constants from ``α, β`` and ``κ``.
721732
@@ -735,14 +746,15 @@ covariance are respectively:
735746
```
736747
See [`update_estimate!(::UnscentedKalmanFilter)`](@ref) for other details.
737748
"""
738-
function init_ukf(::SimModel{NT}, nx̂, α, β, κ) where {NT<:Real}
739-
= 2nx̂ + 1 # number of sigma points
740-
γ = α * (nx̂ + κ) # constant factor of standard deviation √P
749+
function init_ukf(nx̂, α, β, κ)
750+
α, β, κ = promote(α, β, κ)
751+
=2nx̂ + 1 # number of sigma points
752+
γ = α * (nx̂ + κ) # constant factor of standard deviation √P
741753
m̂_0 = 1 - nx̂ / γ^2
742754
Ŝ_0 = m̂_0 + 1 - α^2 + β
743755
w = 1 / 2 / γ^2
744-
= NT[m̂_0; fill(w, 2 * nx̂)] # weights for the mean
745-
= Diagonal(NT[Ŝ_0; fill(w, 2 * nx̂)]) # weights for the covariance
756+
= [m̂_0; fill(w, 2 * nx̂)] # weights for the mean
757+
= Diagonal([Ŝ_0; fill(w, 2 * nx̂)]) # weights for the covariance
746758
return nσ, γ, m̂, Ŝ
747759
end
748760

@@ -885,7 +897,7 @@ struct ExtendedKalmanFilter{
885897
JB<:AbstractADType,
886898
FF<:Function,
887899
HF<:Function
888-
} <: StateEstimator{NT}
900+
} <: KalmanEstimator{NT}
889901
model::SM
890902
cov ::KC
891903
x̂op ::Vector{NT}

src/estimator/mhe.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ include("mhe/execute.jl")
55
function print_details(io::IO, estim::MovingHorizonEstimator)
66
println(io, "├ optimizer: $(JuMP.solver_name(estim.optim)) ")
77
print_backends(io, estim, estim.model)
8+
println(io, "├ arrival covariance: $(nameof(typeof(estim.covestim))) ")
89
end
910

1011
"Print the differentiation backends for `SimModel`."

src/estimator/mhe/construct.jl

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ struct MovingHorizonEstimator{
5151
JM<:JuMP.GenericModel,
5252
GB<:AbstractADType,
5353
JB<:AbstractADType,
54-
CE<:StateEstimator,
54+
CE<:KalmanEstimator,
5555
} <: StateEstimator{NT}
5656
model::SM
5757
cov ::KC
@@ -121,7 +121,7 @@ struct MovingHorizonEstimator{
121121
JM<:JuMP.GenericModel,
122122
GB<:AbstractADType,
123123
JB<:AbstractADType,
124-
CE<:StateEstimator{NT}
124+
CE<:KalmanEstimator{NT}
125125
}
126126
nu, ny, nd, nk = model.nu, model.ny, model.nd, model.nk
127127
He < 1 && throw(ArgumentError("Estimation horizon He should be ≥ 1"))
@@ -280,6 +280,7 @@ MovingHorizonEstimator estimator with a sample time Ts = 10.0 s:
280280
├ optimizer: Ipopt
281281
├ gradient: AutoForwardDiff
282282
├ jacobian: AutoForwardDiff
283+
├ arrival covariance: UnscentedKalmanFilter
283284
└ dimensions:
284285
├ 5 estimation steps He
285286
├ 0 slack variable ε (estimation constraints)
@@ -423,8 +424,10 @@ This syntax allows nonzero off-diagonal elements in ``\mathbf{P̂_i}, \mathbf{Q
423424
where ``\mathbf{P̂_i}`` is the initial estimation covariance, provided by `P̂_0` argument. The
424425
keyword argument `covestim` also allows specifying a custom [`StateEstimator`](@ref) object
425426
for the estimation of covariance at the arrival ``\mathbf{P̂}_{k-N_k}(k-N_k+p)``. The
426-
supported types are [`KalmanFilter`](@ref), [`UnscentedKalmanFilter`](@ref) and
427-
[`ExtendedKalmanFilter`](@ref).
427+
supported types are [`SteadyKalmanFilter`](@ref), [`KalmanFilter`](@ref),
428+
[`UnscentedKalmanFilter`](@ref) and [`ExtendedKalmanFilter`](@ref). A constant arrival
429+
covariance is supported with [`SteadyKalmanFilter`](@ref), and by setting the `P̂` argument
430+
of [`setstate!`](@ref) at the desired value.
428431
"""
429432
function MovingHorizonEstimator(
430433
model::SM, He, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂, Cwt=Inf;
@@ -436,6 +439,7 @@ function MovingHorizonEstimator(
436439
) where {NT<:Real, SM<:SimModel{NT}, JM<:JuMP.GenericModel, CE<:StateEstimator{NT}}
437440
P̂_0, Q̂, R̂ = to_mat(P̂_0), to_mat(Q̂), to_mat(R̂)
438441
cov = KalmanCovariances(model, i_ym, nint_u, nint_ym, Q̂, R̂, P̂_0, He)
442+
validate_covestim(cov, covestim)
439443
return MovingHorizonEstimator{NT}(
440444
model,
441445
He, i_ym, nint_u, nint_ym, cov, Cwt,
@@ -444,13 +448,26 @@ function MovingHorizonEstimator(
444448
)
445449
end
446450

451+
447452
function default_covestim_mhe(model::LinModel, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
448453
return KalmanFilter(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
449454
end
450455
function default_covestim_mhe(model::SimModel, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
451456
return UnscentedKalmanFilter(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
452457
end
453458

459+
"Validate covestim type and dimensions."
460+
function validate_covestim(cov::KalmanCovariances, covestim::KalmanEstimator)
461+
if size(cov.P̂) != size(covestim.cov.P̂)
462+
throw(ArgumentError("estimation covariance covestim.cov.P̂ size does not match the MHE"))
463+
end
464+
return nothing
465+
end
466+
function validate_covestim(::KalmanCovariances, ::StateEstimator)
467+
error( "covestim argument must be a SteadyKalmanFilter, KalmanFilter, "*
468+
"ExtendedKalmanFilter or UnscentedKalmanFilter")
469+
end
470+
454471
@doc raw"""
455472
setconstraint!(estim::MovingHorizonEstimator; <keyword arguments>) -> estim
456473
@@ -498,6 +515,7 @@ julia> estim = setconstraint!(estim, x̂min=[-50, -50], x̂max=[50, 50])
498515
MovingHorizonEstimator estimator with a sample time Ts = 1.0 s:
499516
├ model: LinModel
500517
├ optimizer: OSQP
518+
├ arrival covariance: KalmanFilter
501519
└ dimensions:
502520
├ 3 estimation steps He
503521
├ 0 slack variable ε (estimation constraints)

test/2_test_state_estim.jl

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ end
112112
@test [0, 0]
113113
@test isa(x̂, Vector{Float32})
114114
@test_throws ArgumentError updatestate!(kalmanfilter1, [10, 50])
115-
@test_throws ErrorException setstate!(kalmanfilter1, [1,2,3,4], diagm(.1:.1:.4))
116115
end
117116

118117
@testitem "SteadyKalmanFilter set model" setup=[SetupMPCtests] begin
@@ -904,17 +903,26 @@ end
904903
@test_throws ArgumentError MovingHorizonEstimator(linmodel)
905904
@test_throws ArgumentError MovingHorizonEstimator(linmodel, He=0)
906905
@test_throws ArgumentError MovingHorizonEstimator(linmodel, Cwt=-1)
906+
@test_throws ErrorException MovingHorizonEstimator(
907+
nonlinmodel, 5, 1:2, 0, [1, 1], I_6, I_6, I_2, Inf; optim,
908+
covestim = InternalModel(nonlinmodel)
909+
)
910+
@test_throws ArgumentError MovingHorizonEstimator(
911+
nonlinmodel, 5, 1:2, 0, [1, 1], I_6, I_6, I_2, Inf; optim,
912+
covestim = UnscentedKalmanFilter(nonlinmodel, nint_ym=[2,2])
913+
)
907914
end
908915

909916
@testitem "MovingHorizonEstimator estimation and getinfo" setup=[SetupMPCtests] begin
910-
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, Ipopt, ForwardDiff
917+
using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, ForwardDiff
918+
using JuMP, Ipopt, DAQP
911919
linmodel = LinModel(sys,Ts,i_u=[1,2], i_d=[3])
912920
linmodel = setop!(linmodel, uop=[10,50], yop=[50,30], dop=[5])
913921
f(x,u,d,model) = model.A*x + model.Bu*u + model.Bd*d
914922
h(x,d,model) = model.C*x + model.Dd*d
915923
nonlinmodel = NonLinModel(f, h, Ts, 2, 4, 2, 1, solver=nothing, p=linmodel)
916924
nonlinmodel = setop!(nonlinmodel, uop=[10,50], yop=[50,30], dop=[5])
917-
925+
918926
mhe1 = MovingHorizonEstimator(nonlinmodel, He=2)
919927
JuMP.set_attribute(mhe1.optim, "tol", 1e-7)
920928
preparestate!(mhe1, [50, 30], [5])
@@ -950,7 +958,6 @@ end
950958
= updatestate!(mhe1, [10, 50], [50, 30], [5])
951959
@test zeros(6) atol=1e-9
952960
@test mhe1.x̂0 zeros(6) atol=1e-9
953-
@test evaloutput(mhe1, [5]) mhe1([5]) [50, 30]
954961
info = getinfo(mhe1)
955962
@test info[:x̂] x̂ atol=1e-9
956963
@test info[:Ŷ][end-1:end] [50, 30] atol=1e-9
@@ -976,7 +983,6 @@ end
976983
@test zeros(6) atol=1e-9
977984
@test mhe2.x̂0 zeros(6) atol=1e-9
978985
preparestate!(mhe2, [50, 30], [5])
979-
@test evaloutput(mhe2, [5]) mhe2([5]) [50, 30]
980986
info = getinfo(mhe2)
981987
@test info[:x̂] x̂ atol=1e-9
982988
@test info[:Ŷ][end-1:end] [50, 30] atol=1e-9
@@ -998,7 +1004,6 @@ end
9981004
= updatestate!(mhe2, [10, 50], [50, 30], [5])
9991005
@test zeros(6) atol=1e-9
10001006
@test mhe2.x̂0 zeros(6) atol=1e-9
1001-
@test evaloutput(mhe2, [5]) mhe2([5]) [50, 30]
10021007
info = getinfo(mhe2)
10031008
@test info[:x̂] x̂ atol=1e-9
10041009
@test info[:Ŷ][end-1:end] [50, 30] atol=1e-9
@@ -1012,12 +1017,29 @@ end
10121017
updatestate!(mhe2, [10, 50], [51, 32], [5])
10131018
end
10141019
@test mhe2([5]) [51, 32] atol=1e-2
1020+
1021+
= diagm([1/4, 1/4, 1/4, 1/4].^2)
1022+
= diagm([1, 1].^2)
1023+
optim = Model(DAQP.Optimizer)
1024+
covestim = SteadyKalmanFilter(linmodel, 1:2, 0, 0, Q̂, R̂)
1025+
P̂_0 = covestim.cov.
1026+
mhe3 = MovingHorizonEstimator(linmodel, 2, 1:2, 0, 0, P̂_0, Q̂, R̂; optim, covestim)
1027+
preparestate!(mhe3, [50, 30], [5])
1028+
= updatestate!(mhe3, [10, 50], [50, 30], [5])
1029+
@test zeros(4) atol=1e-9
1030+
@test mhe3.x̂0 zeros(4) atol=1e-9
1031+
preparestate!(mhe3, [50, 30], [5])
1032+
info = getinfo(mhe3)
1033+
@test info[:x̂] x̂ atol=1e-9
1034+
@test info[:Ŷ][end-1:end] [50, 30] atol=1e-9
1035+
10151036
linmodel3 = LinModel{Float32}(0.5*ones(1,1), ones(1,1), ones(1,1), zeros(1,0), zeros(1,0), 1.0)
10161037
mhe3 = MovingHorizonEstimator(linmodel3, He=1)
10171038
preparestate!(mhe3, [0])
10181039
= updatestate!(mhe3, [0], [0])
10191040
@test [0, 0] atol=1e-3
10201041
@test isa(x̂, Vector{Float32})
1042+
10211043
mhe4 = setconstraint!(MovingHorizonEstimator(nonlinmodel, He=1, nint_ym=0), v̂max=[50,50])
10221044
g_V̂max_end = mhe4.optim[:g_V̂max_2].func
10231045
# execute update_predictions! branch in `gfunc_i` for coverage:
@@ -1027,7 +1049,7 @@ end
10271049
= diagm([1, 1].^2)
10281050
optim = Model(Ipopt.Optimizer)
10291051
covestim = ExtendedKalmanFilter(nonlinmodel, 1:2, 0, 0, Q̂, Q̂, R̂)
1030-
mhe5 = MovingHorizonEstimator(nonlinmodel, 1, 1:2, 0, 0, Q̂, Q̂, R̂, Inf; optim, covestim)
1052+
mhe5 = MovingHorizonEstimator(nonlinmodel, 1, 1:2, 0, 0, Q̂, Q̂, R̂; optim, covestim)
10311053
preparestate!(mhe5, [50, 30], [5])
10321054
= updatestate!(mhe5, [10, 50], [50, 30], [5])
10331055
@test zeros(4) atol=1e-9

0 commit comments

Comments
 (0)