Skip to content

Commit e7c8c89

Browse files
authored
Merge pull request #216 from JuliaControl/cov_sparsity
added: new `KalmanCovariance` struct to handle covariance sparsity efficiently
2 parents 336eaf3 + 73e36ce commit e7c8c89

File tree

12 files changed

+397
-337
lines changed

12 files changed

+397
-337
lines changed

src/controller/construct.jl

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,29 @@ function ControllerWeights(
9191
model::SimModel{NT}, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt=Inf, Ewt=0
9292
) where {NT<:Real}
9393
validate_weights(model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt, Ewt)
94-
return ControllerWeights{NT}(NT.(M_Hp), NT.(N_Hc), NT.(L_Hp), Cwt, Ewt)
94+
M_Hp, N_Hc, L_Hp = NT.(M_Hp), NT.(N_Hc), NT.(L_Hp)
95+
return ControllerWeights{NT}(M_Hp, N_Hc, L_Hp, Cwt, Ewt)
96+
end
97+
98+
"Validate predictive controller weight and horizon specified values."
99+
function validate_weights(model, Hp, Hc, M_Hp, N_Hc, L_Hp, C=Inf, E=nothing)
100+
nu, ny = model.nu, model.ny
101+
nM, nN, nL = ny*Hp, nu*Hc, nu*Hp
102+
Hp < 1 && throw(ArgumentError("Prediction horizon Hp should be ≥ 1"))
103+
Hc < 1 && throw(ArgumentError("Control horizon Hc should be ≥ 1"))
104+
Hc > Hp && throw(ArgumentError("Control horizon Hc should be ≤ prediction horizon Hp"))
105+
size(M_Hp) (nM,nM) && throw(ArgumentError("M_Hp size $(size(M_Hp)) ≠ (ny*Hp, ny*Hp) ($nM,$nM)"))
106+
size(N_Hc) (nN,nN) && throw(ArgumentError("N_Hc size $(size(N_Hc)) ≠ (nu*Hc, nu*Hc) ($nN,$nN)"))
107+
size(L_Hp) (nL,nL) && throw(ArgumentError("L_Hp size $(size(L_Hp)) ≠ (nu*Hp, nu*Hp) ($nL,$nL)"))
108+
(isdiag(M_Hp) && any(diag(M_Hp) .< 0)) && throw(ArgumentError("Mwt values should be nonnegative"))
109+
(isdiag(N_Hc) && any(diag(N_Hc) .< 0)) && throw(ArgumentError("Nwt values should be nonnegative"))
110+
(isdiag(L_Hp) && any(diag(L_Hp) .< 0)) && throw(ArgumentError("Lwt values should be nonnegative"))
111+
!ishermitian(M_Hp) && throw(ArgumentError("M_Hp should be hermitian"))
112+
!ishermitian(N_Hc) && throw(ArgumentError("N_Hc should be hermitian"))
113+
!ishermitian(L_Hp) && throw(ArgumentError("L_Hp should be hermitian"))
114+
size(C) () && throw(ArgumentError("Cwt should be a real scalar"))
115+
C < 0 && throw(ArgumentError("Cwt weight should be ≥ 0"))
116+
!isnothing(E) && size(E) () && throw(ArgumentError("Ewt should be a real scalar"))
95117
end
96118

97119
"Include all the data for the constraints of [`PredictiveController`](@ref)"
@@ -867,25 +889,4 @@ end
867889
"Return empty matrices if `estim` is not a [`InternalModel`](@ref)."
868890
function init_stochpred(estim::StateEstimator{NT}, _ ) where NT<:Real
869891
return zeros(NT, 0, estim.nxs), zeros(NT, 0, estim.model.ny)
870-
end
871-
872-
"Validate predictive controller weight and horizon specified values."
873-
function validate_weights(model, Hp, Hc, M_Hp, N_Hc, L_Hp, C=Inf, E=nothing)
874-
nu, ny = model.nu, model.ny
875-
nM, nN, nL = ny*Hp, nu*Hc, nu*Hp
876-
Hp < 1 && throw(ArgumentError("Prediction horizon Hp should be ≥ 1"))
877-
Hc < 1 && throw(ArgumentError("Control horizon Hc should be ≥ 1"))
878-
Hc > Hp && throw(ArgumentError("Control horizon Hc should be ≤ prediction horizon Hp"))
879-
size(M_Hp) (nM,nM) && throw(ArgumentError("M_Hp size $(size(M_Hp)) ≠ (ny*Hp, ny*Hp) ($nM,$nM)"))
880-
size(N_Hc) (nN,nN) && throw(ArgumentError("N_Hc size $(size(N_Hc)) ≠ (nu*Hc, nu*Hc) ($nN,$nN)"))
881-
size(L_Hp) (nL,nL) && throw(ArgumentError("L_Hp size $(size(L_Hp)) ≠ (nu*Hp, nu*Hp) ($nL,$nL)"))
882-
(isdiag(M_Hp) && any(diag(M_Hp) .< 0)) && throw(ArgumentError("Mwt values should be nonnegative"))
883-
(isdiag(N_Hc) && any(diag(N_Hc) .< 0)) && throw(ArgumentError("Nwt values should be nonnegative"))
884-
(isdiag(L_Hp) && any(diag(L_Hp) .< 0)) && throw(ArgumentError("Lwt values should be nonnegative"))
885-
!ishermitian(M_Hp) && throw(ArgumentError("M_Hp should be hermitian"))
886-
!ishermitian(N_Hc) && throw(ArgumentError("N_Hc should be hermitian"))
887-
!ishermitian(L_Hp) && throw(ArgumentError("L_Hp should be hermitian"))
888-
size(C) () && throw(ArgumentError("Cwt should be a real scalar"))
889-
C < 0 && throw(ArgumentError("Cwt weight should be ≥ 0"))
890-
!isnothing(E) && size(E) () && throw(ArgumentError("Ewt should be a real scalar"))
891892
end

src/controller/execute.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,12 +535,12 @@ prediction horizon ``H_p``.
535535
```jldoctest
536536
julia> mpc = LinMPC(KalmanFilter(LinModel(ss(0.1, 0.5, 1, 0, 4.0)), σR=[√25]), Hp=1, Hc=1);
537537
538-
julia> mpc.estim.model.A[1], mpc.estim.R̂[1], mpc.weights.M_Hp[1], mpc.weights.Ñ_Hc[1]
538+
julia> mpc.estim.model.A[1], mpc.estim.cov.R̂[1], mpc.weights.M_Hp[1], mpc.weights.Ñ_Hc[1]
539539
(0.1, 25.0, 1.0, 0.1)
540540
541541
julia> setmodel!(mpc, LinModel(ss(0.42, 0.5, 1, 0, 4.0)); R̂=[9], M_Hp=[10], Nwt=[0.666]);
542542
543-
julia> mpc.estim.model.A[1], mpc.estim.R̂[1], mpc.weights.M_Hp[1], mpc.weights.Ñ_Hc[1]
543+
julia> mpc.estim.model.A[1], mpc.estim.cov.R̂[1], mpc.weights.M_Hp[1], mpc.weights.Ñ_Hc[1]
544544
(0.42, 9.0, 10.0, 0.666)
545545
```
546546
"""

src/estimator/construct.jl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,97 @@ function StateEstimatorBuffer{NT}(
3838
return StateEstimatorBuffer{NT}(u, û, k, x̂, P̂, Q̂, R̂, K̂, ym, ŷ, d, empty)
3939
end
4040

41+
"Include all the covariance matrices for the Kalman filters and moving horizon estimator."
42+
struct KalmanCovariances{
43+
NT<:Real,
44+
# parameters to support both dense and Diagonal matrices (with specialization):
45+
Q̂C<:AbstractMatrix{NT},
46+
R̂C<:AbstractMatrix{NT},
47+
}
48+
P̂_0::Hermitian{NT, Matrix{NT}}
49+
::Hermitian{NT, Matrix{NT}}
50+
::Hermitian{NT, Q̂C}
51+
::Hermitian{NT, R̂C}
52+
invP̄::Hermitian{NT, Matrix{NT}}
53+
invQ̂_He::Hermitian{NT, Q̂C}
54+
invR̂_He::Hermitian{NT, R̂C}
55+
function KalmanCovariances{NT}(
56+
::Q̂C, R̂::R̂C, P̂_0, He
57+
) where {NT<:Real, Q̂C<:AbstractMatrix{NT}, R̂C<:AbstractMatrix{NT}}
58+
if isnothing(P̂_0)
59+
P̂_0 = zeros(NT, 0, 0)
60+
end
61+
P̂_0 = Hermitian(P̂_0, :L)
62+
= copy(P̂_0)
63+
= Hermitian(Q̂, :L)
64+
= Hermitian(R̂, :L)
65+
# the following variables are only for the moving horizon estimator:
66+
invP̄, invQ̂, invR̂ = copy(P̂_0), copy(Q̂), copy(R̂)
67+
try
68+
inv!(invP̄)
69+
catch err
70+
if err isa PosDefException
71+
error("P̂_0 is not positive definite")
72+
else
73+
rethrow()
74+
end
75+
end
76+
try
77+
inv!(invQ̂)
78+
catch err
79+
if err isa PosDefException
80+
error("Q̂ is not positive definite")
81+
else
82+
rethrow()
83+
end
84+
end
85+
try
86+
inv!(invR̂)
87+
catch err
88+
if err isa PosDefException
89+
error("R̂ is not positive definite")
90+
else
91+
rethrow()
92+
end
93+
end
94+
invQ̂_He = repeatdiag(invQ̂, He)
95+
invR̂_He = repeatdiag(invR̂, He)
96+
invQ̂_He = Hermitian(invQ̂_He, :L)
97+
invR̂_He = Hermitian(invR̂_He, :L)
98+
return new{NT, Q̂C, R̂C}(P̂_0, P̂, Q̂, R̂, invP̄, invQ̂_He, invR̂_He)
99+
end
100+
end
101+
102+
"Outer constructor to validate and convert covariance matrices if necessary."
103+
function KalmanCovariances(
104+
model::SimModel{NT}, i_ym, nint_u, nint_ym, Q̂, R̂, P̂_0=nothing, He=1
105+
) where {NT<:Real}
106+
validate_kfcov(model, i_ym, nint_u, nint_ym, Q̂, R̂, P̂_0)
107+
Q̂, R̂ = NT.(Q̂), NT.(R̂)
108+
!isnothing(P̂_0) && (P̂_0 = NT.(P̂_0))
109+
return KalmanCovariances{NT}(Q̂, R̂, P̂_0, He)
110+
end
111+
112+
"""
113+
validate_kfcov(model, i_ym, nint_u, nint_ym, Q̂, R̂, P̂_0=nothing)
114+
115+
Validate sizes and Hermitianity of process `Q̂`` and sensor `R̂` noises covariance matrices.
116+
117+
Also validate initial estimate covariance `P̂_0`, if provided.
118+
"""
119+
function validate_kfcov(model, i_ym, nint_u, nint_ym, Q̂, R̂, P̂_0=nothing)
120+
nym = length(i_ym)
121+
nx̂ = model.nx + sum(nint_u) + sum(nint_ym)
122+
size(Q̂) (nx̂, nx̂) && error("Q̂ size $(size(Q̂)) ≠ nx̂, nx̂ $((nx̂, nx̂))")
123+
!ishermitian(Q̂) && error("Q̂ is not Hermitian")
124+
size(R̂) (nym, nym) && error("R̂ size $(size(R̂)) ≠ nym, nym $((nym, nym))")
125+
!ishermitian(R̂) && error("R̂ is not Hermitian")
126+
if ~isnothing(P̂_0)
127+
size(P̂_0) (nx̂, nx̂) && error("P̂_0 size $(size(P̂_0)) ≠ nx̂, nx̂ $((nx̂, nx̂))")
128+
!ishermitian(P̂_0) && error("P̂_0 is not Hermitian")
129+
end
130+
end
131+
41132
@doc raw"""
42133
init_estimstoch(model, i_ym, nint_u, nint_ym) -> As, Cs_u, Cs_y, nxs, nint_u, nint_ym
43134

src/estimator/execute.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ removes the operating points with [`remove_op!`](@ref) and call [`init_estimate!
112112
bumpless manual to automatic transfer. See [`init_estimate!`](@ref) for details.
113113
- Else, `estim.x̂0` is left unchanged. Use [`setstate!`](@ref) to manually modify it.
114114
115-
If applicable, it also sets the error covariance `estim.P̂` to `estim.P̂_0`.
115+
If applicable, it also sets the error covariance `estim.cov.P̂` to `estim.cov.P̂_0`.
116116
117117
# Examples
118118
```jldoctest
@@ -327,7 +327,7 @@ end
327327
"""
328328
setstate!(estim::StateEstimator, x̂[, P̂]) -> estim
329329
330-
Set `estim.x̂0` to `x̂ - estim.x̂op` from the argument `x̂`, and `estim.P̂` to `P̂` if applicable.
330+
Set `estim.x̂0` to `x̂ - estim.x̂op` from the argument `x̂`, and `estim.cov.P̂` to `P̂` if applicable.
331331
332332
The covariance error estimate `P̂` can be set only if `estim` is a [`StateEstimator`](@ref)
333333
that computes it.
@@ -339,11 +339,11 @@ function setstate!(estim::StateEstimator, x̂, P̂=nothing)
339339
return estim
340340
end
341341

342-
"Set the covariance error estimate `estim.P̂` to `P̂`."
342+
"Set the covariance error estimate `estim.cov.P̂` to `P̂`."
343343
function setstate_cov!(estim::StateEstimator, P̂)
344344
if !isnothing(P̂)
345345
size(P̂) == (estim.nx̂, estim.nx̂) || error("P̂ size must be $((estim.nx̂, estim.nx̂))")
346-
estim.P̂ .= to_hermitian(P̂)
346+
estim.cov.P̂ .= to_hermitian(P̂)
347347
end
348348
return nothing
349349
end
@@ -375,12 +375,12 @@ augmented model is not verified (see Extended Help for more info).
375375
```jldoctest
376376
julia> kf = KalmanFilter(LinModel(ss(0.1, 0.5, 1, 0, 4.0)), σQ=[√4.0], σQint_ym=[√0.25]);
377377
378-
julia> kf.model.A[], kf.Q̂[1, 1], kf.Q̂[2, 2]
378+
julia> kf.model.A[], kf.cov.Q̂[1, 1], kf.cov.Q̂[2, 2]
379379
(0.1, 4.0, 0.25)
380380
381381
julia> setmodel!(kf, LinModel(ss(0.42, 0.5, 1, 0, 4.0)), Q̂=[1 0;0 0.5]);
382382
383-
julia> kf.model.A[], kf.Q̂[1, 1], kf.Q̂[2, 2]
383+
julia> kf.model.A[], kf.cov.Q̂[1, 1], kf.cov.Q̂[2, 2]
384384
(0.42, 1.0, 0.5)
385385
```
386386
@@ -449,7 +449,7 @@ function setmodel_estimator!(estim::StateEstimator, model, _ , _ , _ , Q̂, R̂)
449449
estim.f̂op .= f̂op
450450
estim.x̂0 .-= estim.x̂op # convert x̂ to x̂0 with the new operating point
451451
# --- update covariance matrices ---
452-
!isnothing(Q̂) && (estim.Q̂ .= to_hermitian(Q̂))
453-
!isnothing(R̂) && (estim.R̂ .= to_hermitian(R̂))
452+
!isnothing(Q̂) && (estim.cov.Q̂ .= to_hermitian(Q̂))
453+
!isnothing(R̂) && (estim.cov.R̂ .= to_hermitian(R̂))
454454
return nothing
455455
end

src/estimator/internal_model.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ function init_internalmodel(As, Bs, Cs, Ds)
225225
end
226226

227227
"Throw an error if P̂ != nothing."
228-
function setstate_cov!(estim::InternalModel, P̂)
229-
== nothing || error("InternalModel does not compute an estimation covariance matrix P̂.")
228+
function setstate_cov!(::InternalModel, P̂)
229+
isnothing(P̂) || error("InternalModel does not compute an estimation covariance matrix P̂.")
230230
return nothing
231231
end
232232

0 commit comments

Comments
 (0)