A Smoother Derivatives Filter Design

Problem Statement

Usually, we could calulate the first derivative by $$\dot{x} = \frac{x[t]-x[t-dt]}{dt} \hspace{30mm} (1)$$ Below is the raw encoder counts data and the rotation speed (counts/s) calculated by the above equation:

# Import packages
from filterpy.common import Q_discrete_white_noise
from filterpy.kalman import KalmanFilter

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

# read data
df = pd.read_csv(r'C:\Users\Abelb\OneDrive - Florida State Students\Attachments\Tallahassee\At FSU\projects\Automation System\Qcar\Sensor_fusion\Data\turn\speed_0_dot_1\turn_0_dot_1.csv',sep=',',header=None)
df = df.values
time = df[:,0]
counts = df[:,11]

# normalization
counts_max = 8390000
d = counts/counts_max
t = time
t.reshape([t.shape[0],1])
d.reshape([t.shape[0],1])

# Calculated first derivative
cal_counts_per_sec = np.zeros(t.shape)
for iter in range(0,d.shape[0]):
    if iter>0:
        dt = time[iter]-time[iter-1]
        cal_counts_per_sec[iter] = counts_max*(d[iter]-d[iter-1])/dt

# Plotting
plt.plot(t, counts,'k', label='Raw counts')
plt.legend()
plt.show()
plt.plot(t, cal_counts_per_sec,'k.', label='calculated counts/second')
plt.legend()
plt.show()

image

The above figure shows how noisy is the derivative calculated by equation (1) even if the raw data is clean. The noise in derivative is brought by the step time obatined from software timer.
To obtain the clearn and accurate derivative, two filter designs are introduced.

Solution 1: Kalman filter

From the raw data, a quadratic function could be enough to describe the relationship between time and raw counts. Thus, we could assume the acceleration (counts/s^2) is constant governed by noise. The pure state propogation model used in Kalman filter is
$x[k] = x[k-1] + v[k-1]dt$
$ v[k] = v[k-1] + a[k-1]dt \hspace{6mm} (2)$
$ a[k] = a[k-1]$
then the state propogation matrix is $$ F = \begin{bmatrix} 1 & dt & 0 \\ 0 & 1 & dt \\ 0 & 0 & 1 \end{bmatrix}. $$ The measurement is the ecoder counts (x), thus, the measurement matrix is: $$ H = \begin{bmatrix} 1 & 0 & 0 \end{bmatrix} $$ The process noise covariance Q and measurement noise R are chosen as follows $$ Q = \begin{bmatrix} 0.01^2 & & \\ & 0.01^2 & \\ & & 0.01^2 \end{bmatrix}, R = 0.15 $$

## Initialization
f = KalmanFilter (dim_x=3, dim_z=1)
f.x = np.array([[0.],    # position
                [0.],    # velocity
                [0.]])   # acceleration
f.H = np.array([[1.,0.,0.]])
f.R = 0.15
dt = 0.1

## perform prediction and update
xs = []
T = []
for iter in range(0,d.shape[0]):
    if iter> 0:
        dt = time[iter]-time[iter-1]
    
    f.F = np.array([[1.,dt,0],
                    [0.,1.,dt],
                    [0.,0.,1.]])
    f.Q = Q_discrete_white_noise(dim=3, dt=dt, var=1e-2)

    z = d[iter]

    f.predict()
    f.update(z)

    states = f.x.copy()

    xs.append(states[1]*counts_max)
    T.append(t[iter])
    
    # print(f.x, 'log-likelihood', f.log_likelihood)
    
## Plotting
plt.plot(t, cal_counts_per_sec,'k.', label='calculated counts/second')
plt.plot(T,xs,'r-',label='KF estimated counts/second')
plt.legend()
plt.show()

png

Solution 2: Moving-horizon Quadratic filter with low pass filter

As described before, there exist a quadratic relationship between the raw counts data and the time. Instead of assuming the kinematic model in (2), we could designa filter to estimate the parameter of the quadratic function, and output the estimated rotation speed in the meanwhile.
Let’s assume a quadratic function as the local signal model: $$ x(t) = a_0 + a_1 (t - t_{k-1}) + a_2 (t-t_{k-1})^2 \triangleq y(t)^{\top}\theta $$ where $$y(t)=\begin{bmatrix} 1 \\ t-t_{k-1} \\ \left(t-t_{k-1}\right)^{2} \end{bmatrix}, \hspace{2mm} \theta = \begin{bmatrix}a_0 \\ a_1 \\ a_2 \end{bmatrix}.$$ Then the first derivative and second derivative are given as $$ x^{'}(t_k) = a_1+ 2a_2 (t_k - t_{k-1})$$ $$ x^{''}(t_k) = 2a_2 $$ The parameter estimation is given by solving the following optimization program: $$ \hat{\theta}_k = argmin \bigg\{\gamma \|\theta-\theta_{k-1}\|_2^2 + \frac{1}{2}(x_k-y_k^{\top}\theta)^2+\frac{1}{2}(x_{k-1}-y_{k-1}^{\top}\theta)^2 \bigg\}$$ where the first term controls the convergency of parameter $\theta$, the second term and third term are measurement updates on k-1 and k time steps, and $\gamma$ is a forgetting factor.

By solving the above opimization program and give the closed-form solution, then the Algorithm is designed as following:

Algorithm (Moving-horizon Quadratic Filter)

  • Inputs:
    $\delta_k : \text{time difference from last observation (time-varying)}$
    $x_k: \text{current measurement} $
    $x_{k-1}: \text{pervious measurement}$

  • Initilization
    $$\hat{\theta}_0 = \begin{bmatrix}1 \\ 1 \\ 1\end{bmatrix}$$

# Filter initilization
theta_0   = np.zeros([3,1])         # initialization
theta_k_1 = theta_0                 # cache for k-1 parameter theta_{k-1}
x_k_1     = 0
x_hat_low_k_1 = 0 
  • Packing data
    $$ y_k^{\star} = \begin{bmatrix}1 & 1 \\ 0 & \delta_k \\ 0 & \delta_k^2 \end{bmatrix}, \hspace{2mm} x_k^{\star} = \begin{bmatrix}x_{k-1}\\ x_k \end{bmatrix}$$
  • Computer gain matrix $$ \delta = (\gamma+1)(\gamma+1+\delta_k+\delta_k^2)-1$$ $$D_{k}=\left[\begin{array}{ccc} \frac{1}{\gamma}-\frac{2 \gamma+\delta_{k}+\delta_{k}^{2}}{\gamma \delta} & -\frac{\delta_{k}}{\delta} & \frac{-\delta_{k}^{2}}{\delta} \\
    -\frac{\delta_{k}}{\delta} & \frac{1}{\gamma}-\frac{\delta_{k}^{2}(\gamma+1)}{\gamma \delta} & -\frac{\delta_{k}^{3}(\gamma+1)}{\gamma \delta} \\
    \frac{-\delta_{k}^{2}}{\delta} & -\frac{\delta_{k}^{3}(\gamma+1)}{\gamma \delta} & \frac{1}{\gamma}-\frac{\delta_{k}^{4}(\gamma+1)}{\gamma \delta} \end{array}\right]$$
  • update parameter $$ \hat{\theta}_k = D_k\left(\gamma \hat{\theta}_{k-1} + y_k^{\star} x_k^{\star}\right)$$
  • Model update
    Filtered measurement $ \hat{x}_k = y(t)^{\top}\hat{\theta}_k $
    Filtered fisrt derivative $ \hat{x}_k^{'} = \begin{bmatrix}0 \\ 1 \\ 2\delta_k\end{bmatrix}\hat{\theta}_k $
    Filtered second derivative $ \hat{x}_k^{''} = \begin{bmatrix}0 \\ 0 \\ 2\end{bmatrix}\hat{\theta}_k$
def MH_Quau_filter(Theta_k_1, delta_k, x_k, x_k_1, gamma=1e-4):
    # packing data
    y_k_bar  = np.array([[1, 1], [0, delta_k], [0, delta_k**2]],dtype=object)
    y_k_bar = y_k_bar.reshape((3,2))
    x_k_bar = np.array([x_k_1,x_k],dtype=object).T
    x_k_bar = x_k_bar.reshape((2,1))
    
    # computer gain matrix
    delta = (gamma + 1)*(gamma + 1 + delta_k**2 + delta_k**4) - 1
    D_k   = np.array([[1/gamma - (2*gamma+delta_k**2+delta_k**4)/(gamma*delta), -delta_k/delta, -delta_k**2/delta],
        [-delta_k/delta, 1/gamma - delta_k**2*(gamma+1)/(gamma*delta), -delta_k**3*(gamma+1)/(gamma*delta)],
        [-delta_k**2/delta, -delta_k**3*(gamma+1)/(gamma*delta), 1/gamma - delta_k**4*(gamma+1)/(gamma*delta)]])
    
    # update parameter
    Theta_k = np.dot(D_k,(gamma * Theta_k_1 + np.dot(y_k_bar,x_k_bar)))
    
    # 4. Output
    x_k_hat    = np.dot(np.array([1, delta_k, delta_k**2],dtype=object), Theta_k);  # filtered measurement
    x_k_d_hat  = np.dot(np.array([0, 1, 2*delta_k],dtype=object),Theta_k);          # filtered derivative
    x_k_dd_hat = np.dot(np.array([0, 0, 2],dtype=object),Theta_k);                  # filtered second derivative
    
    return Theta_k, x_k_hat, x_k_d_hat, x_k_dd_hat
## perform prediction and update

# (1) loop Initialization
counts_estimate = np.zeros(d.shape)
counts_per_sec1 = np.zeros(d.shape)  # cache for "without low pass filter"
counts_per_sec2 = np.zeros(d.shape)  # cache for "with low pass filter"

t_previous = 0

tau=1e-1         # Low-pass filter time constant (cutt-off frequency = 1/tau)
gamma = 1e-4     # fogetting factor
    
for iter in range(0,d.shape[0]):
    # (2) signal input
    delta_k = t[iter] - t_previous
        
    # (3) call filter
    Theta_k, x_k_hat, x_k_d_hat, x_k_dd_hat = MH_Quau_filter(theta_k_1, delta_k, d[iter], x_k_1, gamma=gamma)
    
    # (4) Low-pass filter
    x_k_d_low = (1-delta_k/tau) * x_hat_low_k_1 + (delta_k/tau) * x_k_d_hat
    
    counts_estimate[iter] = x_k_hat   * counts_max
    counts_per_sec1[iter] = x_k_d_hat * counts_max
    counts_per_sec2[iter] = x_k_d_low * counts_max
    
    # (5) update cache
    theta_k_1 = Theta_k
    t_previous = t[iter]
    x_k_1 = d[iter]   
    x_hat_low_k_1 = x_k_d_low

# (6) local plotting
plt.plot(t, cal_counts_per_sec,'k.', label='calculated counts/second')
plt.plot(t,counts_per_sec1, label='without low pass filter')
plt.plot(t,counts_per_sec2, label='with low pass filter')
plt.legend()
plt.title('Moving-horizon quadratic filter')
plt.show()

png

Compare results

plt.plot(t, cal_counts_per_sec,'k.', label='calculated')
plt.plot(T,xs,'r-',label='Kalman filter')
plt.plot(t,counts_per_sec2, label='MH_quau_with_low_pass')
plt.legend()
plt.title('counts/second')
plt.show()

png

Compared to Kalman filter, the moving-horizon quadratic filter has smaller peak oscillation at the beginning of stable status.

Yu Zheng
Yu Zheng
Ph.D.

Welcome to the portfolio of my research projects and papers.

Next
Previous