This notebook is designed to demonstrate how to use the StateSpace.jl package to execute the Kalman filter for a linear State Space model. The example that has been used here closely follows the one given on "Greg Czerniak's Website". Namely the voltage example on this page.
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 true value of the voltage given some noisy measurements from a voltmeter. We also are armed with the knowledge that the true voltage remains constant.
We will define our state transistion as
$$x_i = x_{i-1} + v_i$$where $x_i$ is the current state, i.e. the current voltage, $x_{i-1}$ is the previous state (voltage) and $v_i$ is the process noise error.
We will assume that our voltmeter gives us the voltage directly. This means the observation equation is
$$y_i = x_i + w_i$$where $y_i$ is the current observation, i.e. measured voltage, and $w_i$ is the observation error.
First we'll import the modules that we need
In [6]:
using StateSpace
using Distributions
using DataFrames
using Gadfly
using Colors
In [7]:
true_voltage = 1.25
Out[7]:
Now we need to set the noise level for the observations. We'll do this by setting the variance.
In [8]:
measurement_noise_variance = 0.1
Out[8]:
Now we can simulate a set of noisy measurements. We'll do this by adding zero mean Gaussian noise with the variance that we just specified above.
In [9]:
number_of_observations = 60
observations = randn(number_of_observations) * sqrt(measurement_noise_variance) + true_voltage
observations = observations'
Out[9]:
Note that we had to transpose the observations matrix because by default a 1-dimensional array in Julia is a column array. In StateSpace.jl a single column is considered as a single observation (for the case where we have multiple elements that are considered a single observation). This means that the second dimension is interpreted as separate observations.
Let's plot the observations to see what we are dealing with.
In [10]:
plt = plot(x=1:60, y=observations, Geom.point)
Out[10]:
We can define the process and observation parameters according to the model defined above. Since the process doesn't change, the process parameter is equal to 1. Also since the voltage is observed directly, the observation parameter is also equal to 1. Since we are very sure that the voltage is constant we can set the process variance to a small value. We've also already set the measurement variance previously. Therefore we have the following parameters:
In [11]:
process_parameter = 1.0
process_variance = 0.00001
observation_parameter = 1.0
observation_variance = measurement_noise_variance
Out[11]:
We can now create the instance of the Linear State Space Model
In [12]:
linSSM = LinearGaussianSSM(process_parameter, process_variance, observation_parameter, observation_variance)
Out[12]:
In [13]:
initial_guess = MvNormal([3.0], [1.0])
Out[13]:
In [14]:
filtered_state = filter(linSSM, observations, initial_guess)
Out[14]:
And there you have it. You have just used the StateSpace.jl package to obtain a filtered estimate of the state of the State Space system.
There are several plotting packages available to this and you can find out about them here and pick your favourite one. We will demonstrate how to plot the results using Gadfly.
First we extract the filtered state along with their corresponding $2\sigma$ values (the give the area that we would expect the true value to lie with 95% confidence).
In [15]:
x_data = 1:number_of_observations
state_array = Vector{Float64}(number_of_observations+1)
confidence_array = Vector{Float64}(number_of_observations+1)
for i in 1:number_of_observations+1
current_state = filtered_state.state[i]
state_array[i] = current_state.μ[1]
if i != 1
confidence_array[i] = 2*sqrt(current_state.Σ.mat[1])
else
confidence_array[i] = 2*sqrt(current_state.Σ.diag[1])
end
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 confidence interval
In [16]:
df_fs = DataFrame(
x = [0;x_data],
y = state_array,
ymin = state_array - confidence_array,
ymax = state_array + confidence_array,
f = "Filtered values"
)
Out[16]:
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 [17]:
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[17]:
Finally we will plot the results
In [18]:
filtered_state_plot = plot(
layer(x=x_data, y=filtered_state.observations, Geom.point, Theme(default_color=getColors[2])),
layer(x=[0;x_data], y=ones(number_of_observations+1)*true_voltage, 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("Voltage (Volts)"),
Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
Guide.title("Linear Kalman Filter Example")
)
display(filtered_state_plot)
In [19]:
smoothed_state = smooth(linSSM, filtered_state)
Out[19]:
Now we can the results in the same way as above to visualize the smoothed result.
In [20]:
state_array = Vector{Float64}(number_of_observations)
confidence_array = Vector{Float64}(number_of_observations)
for i in x_data
current_state = smoothed_state.state[i]
state_array[i] = current_state.μ[1]
confidence_array[i] = 2*sqrt(current_state.Σ.mat[1])
end
df_ss = DataFrame(
x = x_data,
y = state_array,
ymin = state_array - confidence_array,
ymax = state_array + confidence_array,
f = "Filtered values"
)
smoothed_state_plot = plot(
layer(x=x_data, y=smoothed_state.observations, Geom.point, Theme(default_color=getColors[2], line_width=3px)),
layer(x=x_data, y=ones(number_of_observations)*true_voltage, Geom.line, Theme(default_color=getColors[3], line_width=3px)),
layer(df_ss, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon, Theme(line_width=3px)),
Guide.xlabel("Measurement Number"), Guide.ylabel("Voltage (Volts)"),
Guide.manual_color_key("Colour Key",["Smoothed Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
Guide.title("Linear Kalman Smoother Example"), Theme(major_label_font_size=20px, key_label_font_size=20px)
)
display(smoothed_state_plot)
For those that would like to copy and paste the code. Here is the code in full:
In [21]:
using StateSpace
using Distributions
using DataFrames
using Gadfly
using Colors
#Generate noisy observations
true_voltage = 1.25
measurement_noise_variance = 0.1
number_of_observations = 60
observations = randn(number_of_observations) * sqrt(measurement_noise_variance) + true_voltage
observations = observations'
#Define Filter parameters
process_parameter = 1.0
process_variance = 0.00001
observation_parameter = 1.0
observation_variance = measurement_noise_variance
linSSM = LinearGaussianSSM(process_parameter, process_variance, observation_parameter, observation_variance)
initial_guess = MvNormal([3.0], [1.0]) #Give initial guess parameters
filtered_state = filter(linSSM, observations, initial_guess) #Perform Kalman Filter
smoothed_state = smooth(linSSM, filtered_state) #Perform Kalman Smoother
#Plot the filter results
x_data = 1:number_of_observations
state_array = Vector{Float64}(number_of_observations+1)
confidence_array = Vector{Float64}(number_of_observations+1)
for i in 1:number_of_observations+1
current_state = filtered_state.state[i]
state_array[i] = current_state.μ[1]
if i != 1
confidence_array[i] = 2*sqrt(current_state.Σ.mat[1])
else
confidence_array[i] = 2*sqrt(current_state.Σ.diag[1])
end
end
df_fs = DataFrame(
x = [0;x_data],
y = state_array,
ymin = state_array - confidence_array,
ymax = state_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))
filtered_state_plot = plot(
layer(x=x_data, y=filtered_state.observations, Geom.point, Theme(default_color=getColors[2])),
layer(x=[0;x_data], y=ones(number_of_observations+1)*true_voltage, 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("Voltage (Volts)"),
Guide.manual_color_key("Colour Key",["Filtered Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
Guide.title("Linear Kalman Filter Example")
)
display(filtered_state_plot)
#Plot the smoothed results
state_array = Vector{Float64}(number_of_observations)
confidence_array = Vector{Float64}(number_of_observations)
for i in x_data
current_state = smoothed_state.state[i]
state_array[i] = current_state.μ[1]
confidence_array[i] = 2*sqrt(current_state.Σ.mat[1])
end
df_ss = DataFrame(
x = x_data,
y = state_array,
ymin = state_array - confidence_array,
ymax = state_array + confidence_array,
f = "Filtered values"
)
smoothed_state_plot = plot(
layer(x=x_data, y=smoothed_state.observations, Geom.point, Theme(default_color=getColors[2], line_width=3px)),
layer(x=x_data, y=ones(number_of_observations)*true_voltage, Geom.line, Theme(default_color=getColors[3], line_width=3px)),
layer(df_ss, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon, Theme(line_width=3px)),
Guide.xlabel("Measurement Number"), Guide.ylabel("Voltage (Volts)"),
Guide.manual_color_key("Colour Key",["Smoothed Estimate", "Measurements","True Value "],[getColors[1],getColors[2],getColors[3]]),
Guide.title("Linear Kalman Smoother Example"), Theme(major_label_font_size=20px, key_label_font_size=20px)
)
display(smoothed_state_plot)