import matplotlib.pyplot as plt
import numpy as np
from scipy import linalg
# we use Sarkka's utilities to streamline a bit...
from common_utilities import generate_ssm, RandomState, rmse, plot_car_trajectory
# initialize
= 1. # process noise
q = 0.1 # time step
dt = 0.5 # measurement noise
s
= 4 # State dimension
M = 2 # Observation dimension
N
= np.array([[1, 0, dt, 0],
A 0, 1, 0, dt],
[0, 0, 1, 0],
[0, 0, 0, 1]])
[
= q * np.array([[dt ** 3 / 3, 0, dt ** 2 / 2, 0],
Q 0, dt **3 / 3, 0, dt ** 2 / 2],
[** 2 / 2, 0, dt, 0],
[dt 0, dt ** 2 / 2, 0, dt]])
[
= np.array([[1, 0, 0, 0],
H 0, 1, 0, 0]])
[
= np.array([[s ** 2, 0],
R 0, s ** 2]])
[
= np.array([0., 0., 1., -1.]) x_0
7 Example 4 - constant-velocity 2D motion tracking
Here, we will follow a series of examples taken from [4] for tracking vehicle motion in 2D.
Assume that a car is represented by a point mass at its centre \((x_1, x_2),\) and moves in the \(x_1\)-\(x_2\) plane subject to noisy forces \(g_1(t),\) \(g_2(t).\) Then, according to Newton’s law of motion, \(F=ma,\) we can simply write \(g_i(t)=ma_i(t),\) where \(a_i = \ddot{x_i}.\) Now, since \(g_i(t)\) are badly known, or unknown, we can model \(g_i(t)/m\) as random processes, and more precisely as independent, white noise (Wiener) processes. We thus obtain the 2nd order system
\[\begin{align} \ddot{x}_1 &= w_1(t), \\ \ddot{x}_2 &= w_2(t), \end{align}\]
where \(w_i \sim \mathcal{N} ( 0, \sigma^2_{w_{i}} ).\) Defining the velocities, \(x_3 = \dot{x}_1,\) and \(x_4 = \dot{x}_2,\) we can write this as a first order system,
\[ \frac{\mathrm{d}\mathbf{x}}{\mathrm{d}t} = F \mathbf{x} + L \mathbf{w} , \quad \mathbf{x}(0) = \mathbf{x}_0,\]
where
\[ \mathbf{x}=\begin{bmatrix}x_1\\ x_2 \\ x_3 \\ x_4 \end{bmatrix},\quad F=\begin{bmatrix} 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \end{bmatrix},\quad L=\begin{bmatrix} 0 & 0 \\ 0 & 0 \\ 1 & 0 \\ 0 &1 \end{bmatrix},\quad \mathbf{w}= \begin{bmatrix} w_1 \\ w_2 \end{bmatrix}. \]
More generally, we can consider a \(d\)-dimensional (\(d=1,2,3),\) continuous-time, constant-velocity model for the motion of an object in space,
\[ \frac{\mathrm{d}\mathbf{x}}{\mathrm{d}t} = \begin{bmatrix}0 & I\\ 0 & 0 \end{bmatrix} \mathbf{x} + \begin{bmatrix}0\\ I \end{bmatrix} \mathbf{w} , \]
which just denotes that the derivaitve of the position is the velocity, and that the derivative of the velocity is the process noise. We can now proceed to discretizing this system, as derived in [4], to obtain
\[ \mathbf{x}_{k+1 } = F \mathbf{x}_{k} + \mathbf{q}_{k}, \]
where
\[ F = \begin{bmatrix} 1 & 0 & \Delta t & 0\\ 0 & 1 & 0 & \Delta t \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}, \]
and \(\mathbf{q}_{k}\) is a discrete-time, Gaussian noise process with mean zero and covariance
\[ Q = \begin{bmatrix} q_1 \Delta t^3 / 3 & 0 & q_1 \Delta t^2 / 2 & 0 \\ 0 & q_2 \Delta t^3 / 3 & 0 & q_2 \Delta t^2 / 2 \\ q_1 \Delta t^2 / 2 & 0 & q_1 \Delta t & 0 \\ 0 & q_2 \Delta t^2 / 2 & 0 & q_2 \Delta t \end{bmatrix}. \]
7.1 State-space model
Assuming the above dynamics, and adding a noisy position measurement model, we obtain the linear state-space model
\[\begin{align} \mathbf{x}_{k+1 } &= F \mathbf{x}_{k} + \mathbf{q}_{k}, \quad \mathbf{q}_{k} \sim \mathcal{N} (0, Q), \\ \mathbf{y}_{k+1 } &= H \mathbf{x}_{k} + \mathbf{r}_{k}, \quad \mathbf{r}_{k} \sim \mathcal{N} (0, R), \end{align}\]
with
\[ H = \begin{bmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0 \end{bmatrix}, \quad R = \begin{bmatrix} \sigma_1^2 & 0 \\ 0 & \sigma_2^2 \end{bmatrix} . \]
We are now ready to simulate the Kalman filter.
- definition of all parameters
- initilization of all matrices
- simulation and generate noisy measurements - plot
- Kalman filter
- Kalman smoother
# Simulate trajectory and noisy measurements
= RandomState(6)
random_state = 100
steps
= generate_ssm(x_0, A, Q, H, R, steps, random_state)
states, observations
"Trajectory") plot_car_trajectory(observations, states,
7.2 Kalman filter
def kalman_filter(m_0, P_0, A, Q, H, R, observations):
= m_0.shape[-1]
M = observations.shape
steps, N
= np.empty((steps, M))
kf_m = np.empty((steps, M, M))
kf_P
= m_0
m = P_0
P
for i in range(steps):
= observations[i]
y = A @ m
m = A @ P @ A.T + Q
P
= H @ P @ H.T + R
S # More efficient and stable way of computing K = P @ H.T @ linalg.inv(S)
# This also leverages the fact that S is known to be a positive definite matrix (assume_a="pos")
= linalg.solve(S.T, H @ P, assume_a="pos").T
K
= m + K @ (y - H @ m)
m = P - K @ S @ K.T
P
= m
kf_m[i] = P
kf_P[i] return kf_m, kf_P
= x_0
m_0 = np.array([[1, 0, 0, 0],
P_0 0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]])
[
= kalman_filter(m_0, P_0, A, Q, H, R, observations)
kf_m, kf_P
"Trajectory", kf_m, "Filter Estimate")
plot_car_trajectory(observations, states,
= rmse(states[:, :2], observations)
rmse_raw = rmse(kf_m[:, :2], states[:, :2])
rmse_kf print(f"RAW RMSE: {rmse_raw}")
print(f"KF RMSE: {rmse_kf}")
RAW RMSE: 0.7131995943918173
KF RMSE: 0.3746597043548562
7.3 Kalman smoother
The RTS smoother requires a forward run of the Kalman filter that provides the state and the covariance matrix, for all time steps.
def rts_smoother(kf_m, kf_P, A, Q):
= kf_m.shape
steps, M
= np.empty((steps, M))
rts_m = np.empty((steps, M, M))
rts_P
= kf_m[-1]
m = kf_P[-1]
P
-1] = m
rts_m[-1] = P
rts_P[
for i in range(steps-2, -1, -1):
= kf_m[i]
filtered_m = kf_P[i]
filtered_P
= A @ filtered_m
mp = A @ filtered_P @ A.T + Q
Pp
# More efficient and stable way of computing Gk = filtered_P @ A.T @ linalg.inv(Pp)
# This also leverages the fact that Pp is known to be a positive definite matrix (assume_a="pos")
= linalg.solve(Pp, A @ filtered_P, assume_a="pos").T
Gk
= filtered_m + Gk @ (m - mp)
m = filtered_P + Gk @ (P - Pp) @ Gk.T
P
= m
rts_m[i] = P
rts_P[i]
return rts_m, rts_P
= rts_smoother(kf_m, kf_P, A, Q)
rts_m, rts_P
"Trajectory", rts_m, "Smoother Estimate")
plot_car_trajectory(observations, states,
= rmse(states[:, :2], rts_m[:, :2])
rmse_rts
print(f"RAW RMSE: {rmse_raw}")
print(f"KF RMSE: {rmse_kf}")
print(f"RTS RMSE: {rmse_rts}")
RAW RMSE: 0.7131995943918173
KF RMSE: 0.3746597043548562
RTS RMSE: 0.1857332232186917
7.4 Conclusions on Kalman Filters
- workhorse for all linear, Gaussian problems
- cases covered here:
- track a constant
- track a random walk
- movement tracking: scalar consatnt velocity, 2D and 3D tracking
- 2 basic philosophies:
- use a KF classs
- include KF code each time
- use KF module/function
- Choice: I prefer (2). Since the KF is coded in only 5 lines, there is no real need for a class and the resulting code remains very readable
- Process noise modelling, to design the matrix \(Q,\) is a complex subject. See Saho and references therein.
7.5 References
- K. Law, A Stuart, K. Zygalakis. Data Assimilation. A Mathematical Introduction. Springer. 2015.
- M. Asch, M. Bocquet, M. Nodet. Data Assimilation: Methods, Algorithms and Applications. SIAM. 2016.
- M. Asch. A Toobox for Digital Twins. From Model-Based to Data-Driven. SIAM. 2022
- S. Sarkka, L. Svensson. Bayesian Filtering and Smoothing, 2nd ed., Cambridge University Press. 2023.