Extended Kalman Filter Example

Introduction

This notebook is designed to demonstrate how to use the StateSpace.jl package to execute the Extended Kalman filter for a non-linear State Space model. The example that has been used here closely follows Markus Gesmann's Mages' blog titled Extended Kalman filter example in R which in turn was based on a blog post titled Fun with (Extended Kalman) Filters by Dominic Steinitz.

For those of you that do not need/want the explanation of the model and the code, you can skip right to the end of this notebook where the entire section of code required to run this example is given.

The Problem

The problem that we will consider here is that of predicting the true value of a population (say of bacteria, but it could be anything) from some noisy measurements of the population. We also would like to predict the growth rate of the population, however we are faced with the problem that we do not observe the growth rate

Process Model

It is assumed that the population evolves according to the logistic growth model. This is written mathematically as an ordinary differential equation:

$$\frac{\mathrm{d}p}{\mathrm{d}t} = rp\left(1 - \frac{p}{k} \right),$$

where $p$ is the population number, $t$ is the time, and $k$ is the carrying capacity.
The solution to the above differetial equation is given by

$$p(t) = \frac{kp_0\exp(rt)}{k+p_0[\exp(rt) - 1]},$$

where $p_0$ is the initial population.
This is almost in the form that we need. The only problem is that it needs to be formulated in a discrete manner whereby the population at the current time, $p_n$, is written in terms of the population and growth rate at the previous state, $p_{n-1}$ and $r_{n-1}$ respectively. This can be done like so:

$$p_n = \frac{kp_{n-1}\exp(r_{n-1}\Delta t)}{k+p_{n-1}[\exp(r_{n-1}\Delta t) - 1]},$$

where $\Delta t$ is the change in time from one measurement of the population to the next. This is our transistion (process) equation for the population.

As mentioned previously, we also want to know the growth rate, and hence it is both the population number and the growth rate value together that define the state of our system. Hence we need to also define how the growth rate evolves. Here we will simply assume that the growth rate stays constant. Hence the transition equation for the growth rate will be written as:

$$r_n = r_{n-1}.$$

We will assume that these processes experience some noise. This will be in the form of zero mean Gaussian noise. We'll set the variance at 0.001 for both of these processes and hence we can write the full process equations as:

$$p_n = \frac{kp_{n-1}\exp(r_{n-1}\Delta t)}{k+p_{n-1}[\exp(r_{n-1}\Delta t) - 1]} + V_n \qquad V_n \sim \mathcal{N}(0, 0.001)$$$$r_n = r_{t-n} + W_n \qquad W_n \sim \mathcal{N}(0, 0.001)$$
Observation Model

We assume that we observe the population directly but there is some zero mean Gaussian noise with variance = 25. Hence our observation model is:

$$y_n = p_n + Q_n \qquad Q_n \sim \mathcal{N}(0, 25),$$

where $y_n$ is the observation at the current time step.

We do not observe the growth rate :(.

Setting up the problem

First we'll import the required modules


In [1]:
using StateSpace
using Distributions
using Gadfly
using DataFrames
using Colors
Generate noisy observations

First thing we'll do is create the logistic function required to describe the evolution of the population


In [2]:
function logisticGrowth(r, p, k, t)
    k * p * exp(r*t) / (k + p * (exp(r*t) - 1))
end
logisticGrowth(state) = logisticGrowth(state[1], state[2], k, Δt)


Out[2]:
logisticGrowth (generic function with 2 methods)

The second definition of the logistic function will come in useful when we are defining the process function.

Let's set the values of the parameters that we're going to use for the logistic function


In [3]:
r = 0.2
k = 100.0
p0 = 0.1 * k
Δt = 0.1


Out[3]:
0.1

Now we'll set the variance of the observations (measurement noise)


In [4]:
measurement_noise_variance = 25.0


Out[4]:
25.0

We can now define the number of observations and generate the true and observed population values. Note that we don't actually observe the growth rate value so we'll set the corresponding values in the vector to 'NaN'. For more information on handling missing observations in this package check out the missing observations example.


In [5]:
numObs = 100
true_values = Vector{Float64}(numObs)
population_measurements = Vector{Float64}(numObs)
growth_rate_measurements = Vector{Float64}(numObs)
for i in 1:numObs
    true_values[i] = logisticGrowth(r, p0, k, i*Δt)
    population_measurements[i] = true_values[i] + randn() * sqrt(measurement_noise_variance)
    growth_rate_measurements[i] = NaN
end

Finally we need to concatenate both population and growth rate arrays and ensure that have the correct dimensions


In [6]:
measurements = [growth_rate_measurements population_measurements]'


Out[6]:
2x100 Array{Float64,2}:
 NaN        NaN       NaN        NaN        …  NaN       NaN       NaN     
   4.95685    5.6229    9.28264    6.86086      45.9464   44.1878   44.3974
Define Kalman Filter Parameters

We now need to define the process and observation model according to the model described earlier.


In [7]:
function process_fcn(state)
    predict_growth_rate = state[1]
    predict_population = logisticGrowth(state[1], state[2], k, Δt)
    new_state = [predict_growth_rate, predict_population]
    return new_state
end
process_noise_mat = diagm([0.001, 0.001])

function observation_fcn(state)
    growth_rate_observation = 0.0
    population_observation = 1.0 * state[2]
    observation = [growth_rate_observation, population_observation]
    return observation
end
observation_noise_mat = diagm([1.0, measurement_noise_variance])


Out[7]:
2x2 Array{Float64,2}:
 1.0   0.0
 0.0  25.0

some couple of things to note about the above code:
-The process of the growth and population are assumed uncorrelated, hence the off diagonal terms are zero. This is why the process noise matrix (and the observation noise matrix) are created as diagonal matrices.
-Although the growth rate is not observed, we've still given the observation noise for the growth rate a non-zero value. It could be any non-zero value, I've just picked 1.0 (It shouldn't change the overall results). The problem with setting this value to zero is that you end up getting a singularity and hence the matrix algebra doesn't work and it will crash. So it must be a non zero value.

We can now create an instance of the Nonlinear State Space Model


In [8]:
nonLinSSM = NonlinearGaussianSSM(process_fcn, process_noise_mat, observation_fcn, observation_noise_mat)


Out[8]:
StateSpace.NonlinearGaussianSSM{Float64}(process_fcn,j,2x2 Array{Float64,2}:
 0.001  0.0  
 0.0    0.001,observation_fcn,j,2x2 Array{Float64,2}:
 1.0   0.0
 0.0  25.0)
Initial Guess

Now we can create an initial guess for the growth rate and the initial population along with the corresponding covariance matrix.


In [9]:
initial_guess = MvNormal([0.5, 10], diagm([1.0,20.0]))


Out[9]:
FullNormal(
dim: 2
μ: [0.5,10.0]
Σ: 2x2 Array{Float64,2}:
 1.0   0.0
 0.0  20.0
)
Perform Extended Kalman Filter Algorithm

Now we have all of the parameters:

  1. noisy observations
  2. process (transition) and observation (emission) model paramaters
  3. initial guess of state

We can use the Kalman Filter to predict the true underlying state (growth rate and population).


In [10]:
filtered_state = filter(nonLinSSM, measurements, initial_guess, true)


SmoothedState{Float64}
Out[10]:

The last argument in the filter - 'true' - allows us to use the information we have about the observations that we actually observed. It also estimates the observations that we didn't observe using our process and observation model. For more information on handling missing observations in this package check out the missing observations example.

Plot the results

Now we will plot the results. First we will look at the population estimates

First we extract the filtered population values along with their corresponding $2\sigma$ values (the give the area that we would expect the true value to lie with 95% confidence)


In [11]:
x_data = 1:numObs
population_array = Vector{Float64}(numObs+1)
confidence_array = Vector{Float64}(numObs+1)
population_array[1] = initial_guess.μ[2]
confidence_array[1] = 2*sqrt(initial_guess.Σ.mat[2,2])
for i in x_data
    current_state = filtered_state.state[i]
    population_array[i+1] = current_state.μ[2]
    confidence_array[i+1] = 2*sqrt(current_state.Σ.mat[2,2])
end


101 estimates of 2-D process from 2-D observations
Log-likelihood: -436.93441404047263

Next we will create a dataframe. This is simply so the syntax is simple for plotting the ribbon digram which will represent the state along with the confidence interval


In [12]:
df_fs = DataFrame(
    x = [0;x_data],
    y = population_array,
    ymin = population_array - confidence_array,
    ymax = population_array + confidence_array,
    f = "Filtered values"
    )


Out[12]:
xyyminymaxf
1010.01.055728090000840818.94427190999916Filtered values
2110.01.055728090000840818.94427190999916Filtered values
328.0689125007557611.183712078007327614.954112923504194Filtered values
437.54791311622201461.557504893109513213.538321339334516Filtered values
548.3509763187691142.80856334587558713.89338929166264Filtered values
658.2750716616965972.86174525943957313.68839806395362Filtered values
7611.5117775360000756.15184521926328916.87170985273686Filtered values
8714.3117843342883658.61826087116027820.005307797416453Filtered values
9814.4707721899237968.41510887436958720.526435505478005Filtered values
10913.033296207452936.93963865595153819.126953758954322Filtered values
111012.0179178242840766.15013002555409617.885705623014058Filtered values
121113.197946115122577.61584552917994618.780046701065192Filtered values
131214.502180042209339.06581165002183219.93854843439683Filtered values
141315.3285087009769849.96592805141580120.691089350538167Filtered values
151415.66298786791258610.37021189305070120.95576384277447Filtered values
161518.1917878808385412.9917329870434723.39184277463361Filtered values
171617.00892389292364411.824438100158422.193409685688888Filtered values
181716.23615853687238811.16846462922008621.30385244452469Filtered values
191814.312974430969459.3969314989880419.229017362950863Filtered values
201914.4189774426934299.70827214644598319.129682738940872Filtered values
212012.2965561702689077.74757824353299816.845534097004816Filtered values
222111.2102152723747236.86734573198991415.553084812759533Filtered values
232211.4882799294290747.34325756224324815.6333022966149Filtered values
24239.925202691585895.92424612952910913.92615925364267Filtered values
25249.4839122718084025.64984261902125913.317981924595545Filtered values
26258.729889952118695.04121400854713612.418565895690243Filtered values
27268.635535934100095.08659524341089912.184476624789282Filtered values
28277.7437167985885694.30808053056556711.17935306661157Filtered values
29289.0036182569683845.68756533907908412.319671174857683Filtered values
30299.6271169792052216.36433566863381712.889898289776625Filtered values
&vellip&vellip&vellip&vellip&vellip&vellip

Next we will create separate colours for the observations and the true value. Here we will generate the default distinguishable colors used by Gadfly.


In [13]:
n = 3 #We will require 3 different colors
getColors = distinguishable_colors(n, Color[LCHab(70, 60, 240)],
                                   transform=c -> deuteranopic(c, 0.5),
                                   lchoices=Float64[65, 70, 75, 80],
                                   cchoices=Float64[0, 50, 60, 70],
                                   hchoices=linspace(0, 330, 24))


Out[13]:

Finally we will plot the results of the filtered population estimates


In [14]:
population_state_plot = plot(
    layer(x=0:numObs, y=[p0;measurements[2,:]'], Geom.point, Theme(default_color=getColors[2])),
    layer(x=0:numObs, y=[p0;true_values], Geom.line, Theme(default_color=getColors[3])),
    layer(df_fs, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon),
    Guide.xlabel("Measurement Number"), Guide.ylabel("Population"),
    Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
    Guide.title("Extended Kalman Filter Example")
    )
display(population_state_plot)


Measurement Number -150 -100 -50 0 50 100 150 200 250 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 -100 0 100 200 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 Filtered Estimate Measurements True Value Colour Key -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 -50 -48 -46 -44 -42 -40 -38 -36 -34 -32 -30 -28 -26 -24 -22 -20 -18 -16 -14 -12 -10 -8 -6 -4 -2 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100 -50 0 50 100 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 Population Extended Kalman Filter Example

We can use a similar approach to plot the filtered growth rate values


In [15]:
x_data = 1:numObs
growth_rate_array = Vector{Float64}(numObs+1)
confidence_array = Vector{Float64}(numObs+1)
growth_rate_array[1] = initial_guess.μ[1]
confidence_array[1] = initial_guess.Σ.mat[1,1]
for i in x_data
    current_state = filtered_state.state[i]
    growth_rate_array[i+1] = current_state.μ[1]
    confidence_array[i+1] = 2*sqrt(current_state.Σ.mat[1,1])
end
df_fs = DataFrame(
    x = [0;x_data],
    y = growth_rate_array,
    ymin = growth_rate_array - confidence_array,
    ymax = growth_rate_array + confidence_array,
    f = "Filtered values"
    )

growth_rate_state_plot = plot(
    layer(x=0:numObs, y=ones(numObs+1)*r, Geom.line, Theme(default_color=getColors[3])),
    layer(df_fs, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon),
    Guide.xlabel("Measurement Number"), Guide.ylabel("Growth Rate"),
    Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
    Guide.title("Extended Kalman Filter Example")
    )
display(growth_rate_state_plot)


Measurement Number -150 -100 -50 0 50 100 150 200 250 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 -100 0 100 200 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 Filtered Estimate Measurements True Value Colour Key -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 -7.0 -6.8 -6.6 -6.4 -6.2 -6.0 -5.8 -5.6 -5.4 -5.2 -5.0 -4.8 -4.6 -4.4 -4.2 -4.0 -3.8 -3.6 -3.4 -3.2 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.2 5.4 5.6 5.8 6.0 6.2 6.4 6.6 6.8 7.0 7.2 7.4 7.6 7.8 8.0 -10 -5 0 5 10 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 Growth Rate Extended Kalman Filter Example

For those that just want the code, without the explanation, you have come to the right place. Here it is:


In [1]:
using StateSpace
using Distributions
using Gadfly
using DataFrames
using Colors

r = 0.2 #r is the growth rate
k = 100.0 #k is the carrying capacity
p0 = 0.1 * k # p0 is the initial population
Δt = 0.1 # Δt is the change in time.

#Define the logistic growth function to set the observations
function logisticGrowth(r, p, k, t)
    k * p * exp(r*t) / (k + p * (exp(r*t) - 1))
end
logisticGrowth(state) = logisticGrowth(state[1], state[2], k, Δt)

#Generate noisy measurements
measurement_noise_variance = 25.0
numObs = 100
true_values = Vector{Float64}(numObs)
population_measurements = Vector{Float64}(numObs)
growth_rate_measurements = Vector{Float64}(numObs)
for i in 1:numObs
    true_values[i] = logisticGrowth(r, p0, k, i*Δt)
    population_measurements[i] = true_values[i] + randn() * sqrt(measurement_noise_variance)
    growth_rate_measurements[i] = NaN
end
measurements = [growth_rate_measurements population_measurements]'

#Section: Describe Extended Kalman Filter parameters
function process_fcn(state)
    predict_growth_rate = state[1]
    predict_population = logisticGrowth(state)
    new_state = [predict_growth_rate, predict_population]
    return new_state
end
process_noise_mat = diagm([0.001, 0.001])

function observation_fcn(state)
    growth_rate_observation = 0.0
    population_observation = 1.0 * state[2]
    observation = [growth_rate_observation, population_observation]
    return observation
end
observation_noise_mat = diagm([1.0, measurement_noise_variance])

#Create instance of our EKF model
nonLinSSM = NonlinearGaussianSSM(process_fcn, process_noise_mat, observation_fcn, observation_noise_mat)

initial_guess = MvNormal([0.5, 10], diagm([1.0,20.0])) #Set initial guess of the state

filtered_state = filter(nonLinSSM, measurements, initial_guess, true) #Execute  the Extended Kalman Filter

#Plot Filtered results for population
x_data = 1:numObs
population_array = Vector{Float64}(numObs+1)
confidence_array = Vector{Float64}(numObs+1)
population_array[1] = initial_guess.μ[2]
confidence_array[1] = 2*sqrt(initial_guess.Σ.mat[2,2])
for i in x_data
    current_state = filtered_state.state[i]
    population_array[i+1] = current_state.μ[2]
    confidence_array[i+1] = 2*sqrt(current_state.Σ.mat[2,2])
end
df_fs = DataFrame(
    x = [0;x_data],
    y = population_array,
    ymin = population_array - confidence_array,
    ymax = population_array + confidence_array,
    f = "Filtered values"
    )

n = 3
getColors = distinguishable_colors(n, Color[LCHab(70, 60, 240)],
                                   transform=c -> deuteranopic(c, 0.5),
                                   lchoices=Float64[65, 70, 75, 80],
                                   cchoices=Float64[0, 50, 60, 70],
                                   hchoices=linspace(0, 330, 24))
population_state_plot = plot(
    layer(x=0:numObs, y=[p0;measurements[2,:]'], Geom.point, Theme(default_color=getColors[2])),
    layer(x=0:numObs, y=[p0;true_values], Geom.line, Theme(default_color=getColors[3])),
    layer(df_fs, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon),
    Guide.xlabel("Measurement Number"), Guide.ylabel("Population"),
    Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
    Guide.title("Extended Kalman Filter Example")
    )
display(population_state_plot)

#Plot Filtered results for growth rate
x_data = 1:numObs
growth_rate_array = Vector{Float64}(numObs+1)
confidence_array = Vector{Float64}(numObs+1)
growth_rate_array[1] = initial_guess.μ[1]
confidence_array[1] = initial_guess.Σ.mat[1,1]
for i in x_data
    current_state = filtered_state.state[i]
    growth_rate_array[i+1] = current_state.μ[1]
    confidence_array[i+1] = 2*sqrt(current_state.Σ.mat[1,1])
end
df_fs = DataFrame(
    x = [0;x_data],
    y = growth_rate_array,
    ymin = growth_rate_array - confidence_array,
    ymax = growth_rate_array + confidence_array,
    f = "Filtered values"
    )

growth_rate_state_plot = plot(
    layer(x=0:numObs, y=ones(numObs+1)*r, Geom.line, Theme(default_color=getColors[3])),
    layer(df_fs, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon),
    Guide.xlabel("Measurement Number"), Guide.ylabel("Growth Rate"),
    Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
    Guide.title("Extended Kalman Filter Example")
    )
display(growth_rate_state_plot)


Measurement Number -150 -100 -50 0 50 100 150 200 250 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 -100 0 100 200 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 Filtered Estimate Measurements True Value Colour Key -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 -50 -48 -46 -44 -42 -40 -38 -36 -34 -32 -30 -28 -26 -24 -22 -20 -18 -16 -14 -12 -10 -8 -6 -4 -2 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100 -50 0 50 100 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 Population Extended Kalman Filter Example
Measurement Number -150 -100 -50 0 50 100 150 200 250 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 -100 0 100 200 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 Filtered Estimate Measurements True Value Colour Key -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 -7.0 -6.8 -6.6 -6.4 -6.2 -6.0 -5.8 -5.6 -5.4 -5.2 -5.0 -4.8 -4.6 -4.4 -4.2 -4.0 -3.8 -3.6 -3.4 -3.2 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.2 5.4 5.6 5.8 6.0 6.2 6.4 6.6 6.8 7.0 7.2 7.4 7.6 7.8 8.0 -10 -5 0 5 10 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 Growth Rate Extended Kalman Filter Example