Skip to content

Commit 7e3e789

Browse files
committed
debug: Extended Kalman Filter use corrected state for state function Jacobian
1 parent c8ea8b3 commit 7e3e789

File tree

3 files changed

+120
-80
lines changed

3 files changed

+120
-80
lines changed

docs/src/internals/state_estim.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@ ModelPredictiveControl.remove_op!
5252
ModelPredictiveControl.init_estimate!
5353
```
5454

55-
## Correct and Update Estimate
55+
## Correct Estimate
56+
57+
```@docs
58+
ModelPredictiveControl.correct_estimate!
59+
```
60+
61+
## Update Estimate
5662

5763
!!! info
5864
All these methods assume that the `u0`, `y0m` and `d0` arguments are deviation vectors
@@ -63,6 +69,5 @@ ModelPredictiveControl.init_estimate!
6369
``\mathbf{x̂_0}``, respectively.
6470

6571
```@docs
66-
ModelPredictiveControl.correct_estimate!
6772
ModelPredictiveControl.update_estimate!
6873
```

src/estimator/kalman.jl

Lines changed: 108 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,46 @@ end
193193
correct_estimate!(estim::SteadyKalmanFilter, y0m, d0)
194194
195195
Correct `estim.x̂0` with measured outputs `y0m` and disturbances `d0` for current time step.
196+
197+
It computes the corrected state estimate ``\mathbf{x̂}_{k}(k)``. See the docstring of
198+
[`update_estimate!(::SteadyKalmanFilter, ::Any, ::Any)`](@ref) for the equations.
196199
"""
197200
function correct_estimate!(estim::SteadyKalmanFilter, y0m, d0)
198-
return correct_estimate_obsv!(estim, y0m, d0)
201+
return correct_estimate_obsv!(estim, y0m, d0, estim.K̂)
202+
end
203+
204+
@doc raw"""
205+
update_estimate!(estim::SteadyKalmanFilter, y0m, d0, u0)
206+
207+
Update `estim.x̂0` estimate with current inputs `u0`, measured outputs `y0m` and dist. `d0`.
208+
209+
If `estim.direct == false`, the [`SteadyKalmanFilter`](@ref) first corrects the state
210+
estimate with the precomputed Kalman gain ``\mathbf{K̂}``. Afterward, it predicts the next
211+
state with the augmented process model. The correction step is skipped if `direct == true`
212+
since it is already done by the user through the [`preparestate!`](@ref) function (that
213+
calls [`correct_estimate!`](@ref)). The correction and prediction step equations are
214+
provided below.
215+
216+
# Correction Step
217+
```math
218+
\mathbf{x̂}_k(k) = \mathbf{x̂}_{k-1}(k) + \mathbf{K̂}[\mathbf{y^m}(k) - \mathbf{Ĉ^m x̂}_{k-1}(k)
219+
- \mathbf{D̂_d^m d}(k) ]
220+
```
221+
222+
# Prediction Step
223+
```math
224+
\mathbf{x̂}_{k}(k+1) = \mathbf{Â x̂}_{k}(k) + \mathbf{B̂_u u}(k) + \mathbf{B̂_d d}(k)
225+
```
226+
"""
227+
function update_estimate!(estim::SteadyKalmanFilter, y0m, d0, u0)
228+
if !estim.direct
229+
correct_estimate_obsv!(estim, y0m, d0, estim.K̂)
230+
end
231+
return predict_estimate_obsv!(estim::StateEstimator, y0m, d0, u0)
199232
end
200233

201234
"Allow code reuse for `SteadyKalmanFilter` and `Luenberger` (observers with constant gain)."
202-
function correct_estimate_obsv!(estim::StateEstimator, y0m, d0)
203-
= estim.
235+
function correct_estimate_obsv!(estim::StateEstimator, y0m, d0, K̂)
204236
Ĉm, D̂dm = @views estim.Ĉ[estim.i_ym, :], estim.D̂d[estim.i_ym, :]
205237
ŷ0m = @views estim.buffer.ŷ[estim.i_ym]
206238
# in-place operations to reduce allocations:
@@ -213,26 +245,8 @@ function correct_estimate_obsv!(estim::StateEstimator, y0m, d0)
213245
return nothing
214246
end
215247

216-
@doc raw"""
217-
update_estimate!(estim::SteadyKalmanFilter, y0m, d0, u0)
218-
219-
Update `estim.x̂0` estimate with current inputs `u0`, measured outputs `y0m` and dist. `d0`.
220-
221-
The [`SteadyKalmanFilter`](@ref) updates it with the precomputed Kalman gain ``\mathbf{K̂}``:
222-
```math
223-
\mathbf{x̂}_{k}(k+1) = \mathbf{Â x̂}_{k-1}(k) + \mathbf{B̂_u u}(k) + \mathbf{B̂_d d}(k)
224-
+ \mathbf{K̂}[\mathbf{y^m}(k) - \mathbf{Ĉ^m x̂}_{k-1}(k) - \mathbf{D̂_d^m d}(k)]
225-
```
226-
"""
227-
function update_estimate!(estim::SteadyKalmanFilter, y0m, d0, u0)
228-
return update_estimate_obsv!(estim::StateEstimator, y0m, d0, u0)
229-
end
230-
231248
"Allow code reuse for `SteadyKalmanFilter` and `Luenberger` (observers with constant gain)."
232-
function update_estimate_obsv!(estim::StateEstimator, y0m, d0, u0)
233-
if !estim.direct
234-
correct_estimate_obsv!(estim, y0m, d0)
235-
end
249+
function predict_estimate_obsv!(estim::StateEstimator, y0m, d0, u0)
236250
x̂0corr = estim.x̂0
237251
Â, B̂u, B̂d = estim.Â, estim.B̂u, estim.B̂d
238252
x̂0next = estim.buffer.
@@ -385,10 +399,13 @@ function KalmanFilter(
385399
return KalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
386400
end
387401

388-
"""
402+
@doc raw"""
389403
correct_estimate!(estim::KalmanFilter, y0m, d0)
390404
391-
Do the same but for the time varying [`KalmanFilter`](@ref).
405+
Correct `estim.x̂0` and `estim.P̂` using the time-varying [`KalmanFilter`](@ref).
406+
407+
It computes the corrected state estimate ``\mathbf{x̂}_{k}(k)`` estimation covariance
408+
``\mathbf{P̂}_{k}(k)``.
392409
"""
393410
function correct_estimate!(estim::KalmanFilter, y0m, d0)
394411
Ĉm = @views estim.Ĉ[estim.i_ym, :]
@@ -401,32 +418,40 @@ end
401418
402419
Update [`KalmanFilter`](@ref) state `estim.x̂0` and estimation error covariance `estim.P̂`.
403420
404-
It implements the time-varying Kalman Filter in its predictor (observer) form :
421+
It implements the classical time-varying Kalman Filter based on the process model described
422+
in [`SteadyKalmanFilter`](@ref). If `estim.direct == false`, it first corrects the estimate
423+
before predicting the next state. The correction step is skipped if `estim.direct == true`
424+
since it's already done by the user. The correction and prediction step equations are
425+
provided below, see [^2] for details.
426+
427+
# Correction Step
405428
```math
406429
\begin{aligned}
407-
\mathbf{M̂}(k) &= \mathbf{P̂}_{k-1}(k)\mathbf{Ĉ^m}'
408-
[\mathbf{Ĉ^m P̂}_{k-1}(k)\mathbf{Ĉ^m}' + \mathbf{R̂}]^{-1} \\
409-
\mathbf{K̂}(k) &= \mathbf{Â M̂}(k) \\
410-
\mathbf{ŷ^m}(k) &= \mathbf{Ĉ^m x̂}_{k-1}(k) + \mathbf{D̂_d^m d}(k) \\
411-
\mathbf{x̂}_{k}(k+1) &= \mathbf{Â x̂}_{k-1}(k) + \mathbf{B̂_u u}(k) + \mathbf{B̂_d d}(k)
412-
+ \mathbf{K̂}(k)[\mathbf{y^m}(k) - \mathbf{ŷ^m}(k)] \\
413-
414-
430+
\mathbf{Ŝ}(k) &= \mathbf{Ĉ^m P̂}_{k-1}(k)\mathbf{Ĉ^m}' + \mathbf{R̂} \\
431+
\mathbf{K̂}(k) &= \mathbf{P̂}_{k-1}(k)\mathbf{Ĉ^m}'\mathbf{Ŝ^{-1}}(k) \\
432+
\mathbf{ŷ^m}(k) &= \mathbf{Ĉ^m x̂}_{k-1}(k) + \mathbf{D̂_d^m d}(k) \\
433+
\mathbf{x̂}_{k}(k) &= \mathbf{x̂}_{k-1}(k) + \mathbf{K̂}(k)[\mathbf{y^m}(k) - \mathbf{ŷ^m}(k)] \\
434+
\mathbf{P̂}_{k}(k) &= [\mathbf{I - K̂}(k)\mathbf{Ĉ^m}]\mathbf{P̂}_{k-1}(k)
435+
\end{aligned}
436+
```
415437
416-
\mathbf{P̂}_{k}(k+1) &= \mathbf{Â}[\mathbf{P̂}_{k-1}(k)
417-
- \mathbf{M̂}(k)\mathbf{Ĉ^m P̂}_{k-1}(k)]\mathbf{Â}' + \mathbf{Q̂}
438+
# Prediction Step
439+
```math
440+
\begin{aligned}
441+
\mathbf{x̂}_{k}(k+1) &= \mathbf{Â x̂}_{k}(k) + \mathbf{B̂_u u}(k) + \mathbf{B̂_d d}(k) \\
442+
\mathbf{P̂}_{k}(k+1) &= \mathbf{Â P̂}_{k}(k)\mathbf{Â}' + \mathbf{Q̂}
418443
\end{aligned}
419444
```
420-
based on the process model described in [`SteadyKalmanFilter`](@ref). The notation
421-
``\mathbf{x̂}_{k-1}(k)`` refers to the state for the current time ``k`` estimated at the last
422-
control period ``k-1``. See [^2] for details.
423445
424-
[^2]: Boyd S., "Lecture 8 : The Kalman Filter" (Winter 2008-09) [course slides], *EE363:
425-
Linear Dynamical Systems*, <https://web.stanford.edu/class/ee363/lectures/kf.pdf>.
446+
[^2]: "Kalman Filter", *Wikipedia: The Free Encyclopedia*,
447+
<https://en.wikipedia.org/wiki/Kalman_filter>, Accessed 2024-08-08.
426448
"""
427449
function update_estimate!(estim::KalmanFilter, y0m, d0, u0)
428450
Ĉm = @views estim.Ĉ[estim.i_ym, :]
429-
return update_estimate_kf!(estim, y0m, d0, u0, Ĉm, estim.Â)
451+
if !estim.direct
452+
correct_estimate_kf!(estim, y0m, d0, Ĉm)
453+
end
454+
return predict_estimate_kf!(estim, y0m, d0, u0, Ĉm, estim.Â)
430455
end
431456

432457

@@ -916,48 +941,60 @@ end
916941
Update [`ExtendedKalmanFilter`](@ref) state `estim.x̂0` and covariance `estim.P̂`.
917942
918943
The equations are similar to [`update_estimate!(::KalmanFilter)`](@ref) but with the
919-
substitutions ``\mathbf{Â = F̂}(k)`` and ``\mathbf{Ĉ^m = Ĥ^m}(k)``:
944+
substitutions ``\mathbf{Â = F̂}(k)`` and ``\mathbf{Ĉ^m = Ĥ^m}(k)``, the Jacobians of the
945+
augmented process model:
920946
```math
921947
\begin{aligned}
922-
\mathbf{M̂}(k) &= \mathbf{P̂}_{k-1}(k)\mathbf{Ĥ^m}'(k)
923-
[\mathbf{Ĥ^m}(k)\mathbf{P̂}_{k-1}(k)\mathbf{Ĥ^m}'(k) + \mathbf{R̂}]^{-1} \\
924-
\mathbf{K̂}(k) &= \mathbf{F̂}(k) \mathbf{M̂}(k) \\
925-
\mathbf{ŷ^m}(k) &= \mathbf{ĥ^m}\Big( \mathbf{x̂}_{k-1}(k), \mathbf{d}(k) \Big) \\
926-
\mathbf{x̂}_{k}(k+1) &= \mathbf{f̂}\Big( \mathbf{x̂}_{k-1}(k), \mathbf{u}(k), \mathbf{d}(k) \Big)
927-
+ \mathbf{K̂}(k)[\mathbf{y^m}(k) - \mathbf{ŷ^m}(k)] \\
928-
\mathbf{P̂}_{k}(k+1) &= \mathbf{F̂}(k)[\mathbf{P̂}_{k-1}(k)
929-
- \mathbf{M̂}(k)\mathbf{Ĥ^m}(k)\mathbf{P̂}_{k-1}(k)]\mathbf{F̂}'(k)
930-
+ \mathbf{Q̂}
948+
\mathbf{Ĥ}(k) &= \left. \frac{∂\mathbf{ĥ}(\mathbf{x̂}, \mathbf{d})}{∂\mathbf{x̂}} \right|_{\mathbf{x̂ = x̂}_{k-1}(k),\, \mathbf{d = d}(k)} \\
949+
\mathbf{F̂}(k) &= \left. \frac{∂\mathbf{f̂}(\mathbf{x̂}, \mathbf{u}, \mathbf{d})}{∂\mathbf{x̂}} \right|_{\mathbf{x̂ = x̂}_{k}(k), \, \mathbf{u = u}(k),\, \mathbf{d = d}(k)}
931950
\end{aligned}
932951
```
933-
[`ForwardDiff.jacobian`](https://juliadiff.org/ForwardDiff.jl/stable/user/api/#ForwardDiff.jacobian)
934-
automatically computes the Jacobians:
952+
The matrix ``\mathbf{Ĥ^m}`` is the rows of ``\mathbf{Ĥ}`` that are measured outputs. The
953+
function [`ForwardDiff.jacobian`](https://juliadiff.org/ForwardDiff.jl/stable/user/api/#ForwardDiff.jacobian)
954+
automatically computes them. The correction and prediction step equations are provided below.
955+
956+
# Correction Step
935957
```math
936958
\begin{aligned}
937-
\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)} \\
938-
\mathbf{Ĥ}(k) &= \left. \frac{∂\mathbf{ĥ}(\mathbf{x̂}, \mathbf{d})}{∂\mathbf{x̂}} \right|_{\mathbf{x̂ = x̂}_{k-1}(k),\, \mathbf{d = d}(k)}
959+
\mathbf{Ŝ}(k) &= \mathbf{Ĥ^m}(k)\mathbf{P̂}_{k-1}(k)\mathbf{Ĥ^m}'(k) + \mathbf{R̂} \\
960+
\mathbf{K̂}(k) &= \mathbf{P̂}_{k-1}(k)\mathbf{Ĥ^m}'(k)\mathbf{Ŝ^{-1}}(k) \\
961+
\mathbf{ŷ^m}(k) &= \mathbf{ĥ^m}\Big( \mathbf{x̂}_{k-1}(k), \mathbf{d}(k) \Big) \\
962+
\mathbf{x̂}_{k}(k) &= \mathbf{x̂}_{k-1}(k) + \mathbf{K̂}(k)[\mathbf{y^m}(k) - \mathbf{ŷ^m}(k)] \\
963+
\mathbf{P̂}_{k}(k) &= [\mathbf{I - K̂}(k)\mathbf{Ĥ^m}(k)]\mathbf{P̂}_{k-1}(k)
964+
\end{aligned}
965+
```
966+
967+
# Prediction Step
968+
```math
969+
\begin{aligned}
970+
\mathbf{x̂}_{k}(k+1) &= \mathbf{f̂}\Big( \mathbf{x̂}_{k}(k), \mathbf{u}(k), \mathbf{d}(k) \Big) \\
971+
\mathbf{P̂}_{k}(k+1) &= \mathbf{F̂}(k)\mathbf{P̂}_{k}(k)\mathbf{F̂}'(k) + \mathbf{Q̂}
939972
\end{aligned}
940973
```
941-
The matrix ``\mathbf{Ĥ^m}`` is the rows of ``\mathbf{Ĥ}`` that are measured outputs.
942974
"""
943975
function update_estimate!(estim::ExtendedKalmanFilter{NT}, y0m, d0, u0) where NT<:Real
944976
model, x̂0 = estim.model, estim.x̂0
945977
nx̂, nu = estim.nx̂, model.nu
946-
# concatenate x̂0next and û0 vectors to allows û0 vector with dual numbers for AD:
947-
# TODO: remove this allocation using estim.buffer
948-
x̂0nextû = Vector{NT}(undef, nx̂ + nu)
949-
f̂AD! = (x̂0nextû, x̂0) -> @views f̂!(
950-
x̂0nextû[1:nx̂], x̂0nextû[nx̂+1:end], estim, model, x̂0, u0, d0
951-
)
952-
ForwardDiff.jacobian!(estim.F̂_û, f̂AD!, x̂0nextû, x̂0)
978+
= estim.
953979
if !estim.direct
954980
ŷ0 = estim.buffer.
955981
ĥAD! = (ŷ0, x̂0) -> ĥ!(ŷ0, estim, model, x̂0, d0)
956-
ForwardDiff.jacobian!(estim.Ĥ, ĥAD!, ŷ0, x̂0)
982+
ForwardDiff.jacobian!(Ĥ, ĥAD!, ŷ0, x̂0)
983+
Ĥm = @views Ĥ[estim.i_ym, :]
984+
correct_estimate_kf!(estim, y0m, d0, Ĥm)
985+
else
986+
Ĥm = @views Ĥ[estim.i_ym, :]
957987
end
988+
x̂0corr = estim.x̂0
989+
# concatenate x̂0next and û0 vectors to allows û0 vector with dual numbers for AD:
990+
# TODO: remove this allocation using estim.buffer
991+
x̂0nextû = Vector{NT}(undef, nx̂ + nu)
992+
f̂AD! = (x̂0nextû, x̂0corr) -> @views f̂!(
993+
x̂0nextû[1:nx̂], x̂0nextû[nx̂+1:end], estim, model, x̂0corr, u0, d0
994+
)
995+
ForwardDiff.jacobian!(estim.F̂_û, f̂AD!, x̂0nextû, x̂0corr)
958996
= @views estim.F̂_û[1:estim.nx̂, :]
959-
Ĥm = @views estim.Ĥ[estim.i_ym, :]
960-
return update_estimate_kf!(estim, y0m, d0, u0, Ĥm, F̂)
997+
return predict_estimate_kf!(estim, y0m, d0, u0, Ĥm, F̂)
961998
end
962999

9631000
"Set `estim.P̂` to `estim.P̂_0` for the time-varying Kalman Filters."
@@ -1024,20 +1061,15 @@ function correct_estimate_kf!(estim::Union{KalmanFilter, ExtendedKalmanFilter},
10241061
end
10251062

10261063
"""
1027-
update_estimate_kf!(estim::Union{KalmanFilter, ExtendedKalmanFilter}, y0m, d0, u0, Ĉm, Â)
1064+
predict_estimate_kf!(estim::Union{KalmanFilter, ExtendedKalmanFilter}, y0m, d0, u0, Ĉm, Â)
10281065
1029-
Update time-varying/extended Kalman Filter estimates with augmented `Ĉm` and `Â` matrices.
1066+
Predict time-varying/extended Kalman Filter estimates with augmented `Ĉm` and `Â` matrices.
10301067
10311068
Allows code reuse for [`KalmanFilter`](@ref), [`ExtendedKalmanFilterKalmanFilter`](@ref).
1032-
They update the state `x̂` and covariance `P̂` with the same equations. The extended filter
1033-
substitutes the augmented model matrices with its Jacobians (`Â = F̂` and `Ĉm = Ĥm`).
1034-
The implementation uses in-place operations and explicit factorization to reduce
1035-
allocations. See e.g. [`KalmanFilter`](@ref) docstring for the equations.
1069+
They predict the state `x̂` and covariance `P̂` with the same equations. See
1070+
[`update_estimate`](@ref) methods for the equations.
10361071
"""
1037-
function update_estimate_kf!(estim::Union{KalmanFilter, ExtendedKalmanFilter}, y0m, d0, u0, Ĉm, Â)
1038-
if !estim.direct
1039-
correct_estimate_kf!(estim, y0m, d0, Ĉm)
1040-
end
1072+
function predict_estimate_kf!(estim::Union{KalmanFilter, ExtendedKalmanFilter}, y0m, d0, u0, Ĉm, Â)
10411073
x̂0corr, P̂corr = estim.x̂0, estim.
10421074
= estim.
10431075
x̂0next, û0 = estim.buffer.x̂, estim.buffer.

src/estimator/luenberger.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ end
116116
Identical to [`correct_estimate!(::SteadyKalmanFilter)`](@ref) but using [`Luenberger`](@ref).
117117
"""
118118
function correct_estimate!(estim::Luenberger, y0m, d0)
119-
return correct_estimate_obsv!(estim, y0m, d0)
119+
return correct_estimate_obsv!(estim, y0m, d0, estim.)
120120
end
121121

122122

@@ -126,7 +126,10 @@ end
126126
Same than [`update_estimate!(::SteadyKalmanFilter)`](@ref) but using [`Luenberger`](@ref).
127127
"""
128128
function update_estimate!(estim::Luenberger, y0m, d0, u0)
129-
return update_estimate_obsv!(estim, y0m, d0, u0)
129+
if !estim.direct
130+
correct_estimate_obsv!(estim, y0m, d0, estim.K̂)
131+
end
132+
return predict_estimate_obsv!(estim, y0m, d0, u0)
130133
end
131134

132135
"Throw an error if `setmodel!` is called on `Luenberger` observer."

0 commit comments

Comments
 (0)