This notebook is designed to demonstrate how to use the StateSpace.jl package to execute the Unscented Kalman filter with additive noise for a non-linear State Space model. The example that has been used here closely follows the example given in a paper by Rambabu Kandepu, Bjarne Foss and Lars Imsland "Applying the unscented Kalman filter for nonlinear state estimation". The example is the Van der Pol oscillator (Section 4.1 on page 6 of the paper). You can read the paper for full details.
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 that we will consider here is that of predicting the position $\mathbf{x} = (x_1, x_2)$ of a Van der Pol oscillator given some noisy measurements of its position.
The dynamic equations describing the rate of change of position of the oscillator are:
$$ \begin{align} \frac{\mathrm{d}x_1}{\mathrm{d}t} &= -x_2, \\ \frac{\mathrm{d}x_2}{\mathrm{d}t} &= -\mu \left(1 - x_1^2 \right)x_2 + x_1, \\ \end{align} $$where $\mu$ is a scalar parameter that defines the strength of the damping.
These equations can be written in a discrete manner required for solution with the unscented Kalman filter as follows:
where $\mathbf{x}^n = \left( x_1^n, x_2^n \right)$ represents the position of the oscillator at time step $n$ and $\Delta t$ is the time step between consecutive measurements.
In [1]:
using StateSpace
using Distributions
using Gadfly
using DataFrames
using Colors
In [12]:
Δt = 0.1 # Set the time step
#Set the process function
function processFunction(x::Vector, Δt::Float64=0.1, μ::Float64=0.3)
x1 = zeros(x)
x1[1] = x[1] + Δt * -x[2]
x1[2] = x[2] + Δt * (-μ * (1 - x[1]^2) * x[2] + x[1])
x1
end
#Set the observation function.
observationFunction(x::Vector) = x
#Set process noise covariance matrix
processCovariance = 1e-1*[0.1 0.0; 0.0 1e-3]
#Set the observation noise covariance
observationCovariance = 3e-2*eye(2)
#Create additive noise UKF model
ukfStateModel = AdditiveNonLinUKFSSM(processFunction, processCovariance, observationFunction, observationCovariance)
Out[12]:
In [13]:
trueInitialState = [1.4, 0.0] #Set the true initial state of the oscillator
Out[13]:
Now we can define the number of observations and generate the true and noisy observations
In [14]:
#Set the number of observations and generate the true values and the noisy
#observations.
numObs = 200
trueState = zeros(2,numObs)
noisyObs = zeros(2,numObs)
trueState[:,1] = trueInitialState
noisyObs[:,1] = trueInitialState + sqrt(observationCovariance)*randn(2)
for i in 2:numObs
trueState[:,i] = processFunction(trueState[:,i-1])
noisyObs[:,i] = trueState[:,i] + sqrt(observationCovariance)*randn(2)
end
In [15]:
initial_guess = MvNormal([0.0,2.0], 1.0*eye(2))
Out[15]:
Now we have all of the parameters:
We can use the Unscented Kalman Filter to predict the true underlying state (position of the oscillator).
In [16]:
filtered_state = filter(ukfStateModel, noisyObs, initial_guess)
Out[16]:
NOTE: The log likelihood hasn't been implemented for the Unscented Kalman Filter and so 0.0 is just the default value set. It doesn't mean anything yet. We will sort this out soon.
In [17]:
x_data = 1:numObs
x1_array = Vector{Float64}(numObs)
x1Var_array = Vector{Float64}(numObs)
x1_Guess = initial_guess.μ[1]
x1Var_Guess = 2*sqrt(initial_guess.Σ.mat[1,1])
for i in x_data
current_state = filtered_state.state[i]
x1_array[i] = current_state.μ[1]
x1Var_array[i] = 2*sqrt(current_state.Σ.mat[1,1])
end
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 95% confidence interval
In [18]:
df_fs = DataFrame(
x = x_data*Δt,
y = x1_array,
ymin = x1_array - x1Var_array,
ymax = x1_array + x1Var_array,
f = "Filtered values"
)
Out[18]:
Next we create 3 distinguishable colors for the plot
In [19]:
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))
Out[19]:
Finally we display the plot
In [25]:
oscillatorx1_state_plot = plot(
layer(x=x_data*Δt, y=noisyObs[1,:], Geom.point, Theme(default_color=getColors[2])),
layer(x=x_data*Δt, y=trueState[1,:], Geom.line, Theme(default_color=getColors[3])),
layer(df_fs, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon),
Guide.xlabel("Time (seconds)"), Guide.ylabel("x1"),
Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
Guide.title("Unscented Kalman Filter (Additive) Example")
)
display(oscillatorx1_state_plot)
In [21]:
smoothed_state = smooth(ukfStateModel, filtered_state)
Out[21]:
Now we can the results in the same way as above to visualize the smoothed result.
In [23]:
x_data = 1:numObs
x1_array = Vector{Float64}(numObs)
x1Var_array = Vector{Float64}(numObs)
x1_Guess = initial_guess.μ[1]
x1Var_Guess = 2*sqrt(initial_guess.Σ.mat[1,1])
for i in x_data
current_state = smoothed_state.state[i]
x1_array[i] = current_state.μ[1]
x1Var_array[i] = 2*sqrt(current_state.Σ.mat[1,1])
end
df_fs = DataFrame(
x = x_data*Δt,
y = x1_array,
ymin = x1_array - x1Var_array,
ymax = x1_array + x1Var_array,
f = "Smoothed 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))
oscillatorx1_state_plot_smooted = plot(
layer(x=x_data*Δt, y=noisyObs[1,:], Geom.point, Theme(default_color=getColors[2])),
layer(x=x_data*Δt, y=trueState[1,:], Geom.line, Theme(default_color=getColors[3])),
layer(df_fs, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon),
Guide.xlabel("Time (seconds)"), Guide.ylabel("x1"),
Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
Guide.title("Unscented Kalman Smoother (Additive) Example")
)
display(oscillatorx1_state_plot_smooted)
For those that just want the code, without the explanation, you have come to the right place. Here it is:
In [26]:
using StateSpace
using Distributions
using Gadfly
using DataFrames
using Colors
Δt = 0.1 # Set the time step
#Set the process function
function processFunction(x::Vector, Δt::Float64=0.1, μ::Float64=0.3)
x1 = zeros(x)
x1[1] = x[1] + Δt * -x[2]
x1[2] = x[2] + Δt * (-μ * (1 - x[1]^2) * x[2] + x[1])
x1
end
#Set the observation function.
observationFunction(x::Vector) = x
#Set process noise covariance matrix
processCovariance = 1e-1*[0.1 0.0; 0.0 1e-3]
#Set the observation noise covariance
observationCovariance = 3e-2*eye(2)
#Create additive noise UKF model
ukfStateModel = AdditiveNonLinUKFSSM(processFunction, processCovariance, observationFunction, observationCovariance)
trueInitialState = [1.4, 0.0] #Set the true initial state of the oscillator
#Set the number of observations and generate the true values and the noisy
#observations.
numObs = 200
trueState = zeros(2,numObs)
noisyObs = zeros(2,numObs)
trueState[:,1] = trueInitialState
noisyObs[:,1] = trueInitialState + sqrt(observationCovariance)*randn(2)
for i in 2:numObs
trueState[:,i] = processFunction(trueState[:,i-1])
noisyObs[:,i] = trueState[:,i] + sqrt(observationCovariance)*randn(2)
end
initial_guess = MvNormal([0.0,2.0], 1.0*eye(2))
filtered_state = filter(ukfStateModel, noisyObs, initial_guess)
x_data = 1:numObs
x1_array = Vector{Float64}(numObs)
x1Var_array = Vector{Float64}(numObs)
x1_Guess = initial_guess.μ[1]
x1Var_Guess = 2*sqrt(initial_guess.Σ.mat[1,1])
for i in x_data
current_state = filtered_state.state[i]
x1_array[i] = current_state.μ[1]
x1Var_array[i] = 2*sqrt(current_state.Σ.mat[1,1])
end
df_fs = DataFrame(
x = x_data*Δt,
y = x1_array,
ymin = x1_array - x1Var_array,
ymax = x1_array + x1Var_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))
oscillatorx1_state_plot = plot(
layer(x=x_data*Δt, y=noisyObs[1,:], Geom.point, Theme(default_color=getColors[2])),
layer(x=x_data*Δt, y=trueState[1,:], Geom.line, Theme(default_color=getColors[3])),
layer(df_fs, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon),
Guide.xlabel("Time (seconds)"), Guide.ylabel("x1"),
Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
Guide.title("Unscented Kalman Filter (Additive) Example")
)
display(oscillatorx1_state_plot)
#Perform Smoothing of the filtered estimates
smoothed_state = smooth(ukfStateModel, filtered_state)
#Plot the results
x_data = 1:numObs
x1_array = Vector{Float64}(numObs)
x1Var_array = Vector{Float64}(numObs)
x1_Guess = initial_guess.μ[1]
x1Var_Guess = 2*sqrt(initial_guess.Σ.mat[1,1])
for i in x_data
current_state = smoothed_state.state[i]
x1_array[i] = current_state.μ[1]
x1Var_array[i] = 2*sqrt(current_state.Σ.mat[1,1])
end
df_fs = DataFrame(
x = x_data*Δt,
y = x1_array,
ymin = x1_array - x1Var_array,
ymax = x1_array + x1Var_array,
f = "Smoothed 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))
oscillatorx1_state_plot_smooted = plot(
layer(x=x_data*Δt, y=noisyObs[1,:], Geom.point, Theme(default_color=getColors[2])),
layer(x=x_data*Δt, y=trueState[1,:], Geom.line, Theme(default_color=getColors[3])),
layer(df_fs, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon),
Guide.xlabel("Time (seconds)"), Guide.ylabel("x1"),
Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
Guide.title("Unscented Kalman Smoother (Additive) Example")
)
display(oscillatorx1_state_plot_smooted)