1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
//! A crate providing an implementation of the unscented Kalman filter algorithm.
//!
//! This implementation follows the steps presented in ['this guide by mathworks']. It is able to support multiple different kinds of measurement updates through separation of the update and innovation steps of the filter.
//!
//! Examples for the usage of this crate can be found in the examples folder.
//!
//! TODO
//! - Change the filter methods to not take control. This can be handled in the provided closures instead. Is a more general interface for systems that do not specifically break out control.
//! - Support no_std
//! - Add some more examples: one with only one measurement update and one that does not require ode_solvers
//!
//! ['this guide by mathworks']: https://www.mathworks.com/help/control/ug/extended-and-unscented-kalman-filter-algorithms-for-online-state-estimation.html
#![feature(generic_const_exprs)]
use nalgebra::{SMatrix, SVector};
/// Type for state vectors of dimension S
pub type State<const S: usize> = SVector<f64, S>;
/// Type for covariance matricies of dimension SxS
pub type Covariance<const S: usize> = SMatrix<f64, S, S>;
/// Type for control vectors of dimension C
pub type Control<const C: usize> = SVector<f64, C>;
/// Type for measurement outputs of dimension Y
pub type Output<const Y: usize> = SVector<f64, Y>;
/// Type for cross covariance matrices of dimension SxY
pub type CovarianceSY<const S: usize, const Y: usize> = SMatrix<f64, S, Y>;
/// Unscented Kalman filter structure. Contains the state of the filter and its covariance as well as a set of weights calculated for the algorithm based on the initialization parameters.
pub struct UnscentedKalmanFilter<const S: usize>
where
[(); 2 * S + 1]:,
{
pub state: State<S>,
pub covariance: Covariance<S>,
wm: [f64; 2 * S + 1],
wc: [f64; 2 * S + 1],
c: f64,
}
impl<const S: usize> UnscentedKalmanFilter<S>
where
[(); 2 * S + 1]:,
{
/// Constructor for the filter. Takes three filter parameters and the initial filter state and its covariance.
///
/// The parameters are alpha, beta, and kappa. Alpha is usually a small positive constant which tunes the spread of the sigma points that the filter calculates. Beta helps incorporate knowledge about the underlying distribution of the filter. In most cases, this will be assumed to be Gaussian, and in this case beta=2.0 is optimal. Kappa is another parameter that tunes the sigma points and can usually be set to zero. Please see ['the description of the algorithm by Mathworks'] for a more in-depth explanation of these parameters.
///
/// ['the description of the algorithm by Mathworks']: https://www.mathworks.com/help/control/ug/extended-and-unscented-kalman-filter-algorithms-for-online-state-estimation.html
pub fn new(alpha: f64, beta: f64, kappa: f64, x0: State<S>, p0: Covariance<S>) -> Self {
// Weights TODO might need to check these with different implementations
let mut wm: [f64; 2 * S + 1] = [0.0; 2 * S + 1];
let mut wc: [f64; 2 * S + 1] = [0.0; 2 * S + 1];
wm[0] = 1.0 - S as f64 / (alpha.powi(2) * (S as f64 + kappa));
wc[0] = (2.0 - alpha.powi(2) + beta) - S as f64 / (alpha.powi(2) * (S as f64 + kappa));
for i in 1..(2 * S + 1) {
wm[i] = 1.0 / (2.0 * alpha.powi(2) + (S as f64 + kappa));
wc[i] = 1.0 / (2.0 * alpha.powi(2) + (S as f64 + kappa));
}
let c = alpha.powi(2) * (S as f64 + kappa);
Self {
state: x0,
covariance: p0,
wm: wm,
wc: wc,
c: c,
}
}
/// Calculate the sigmapoint deltas. Used for adjusting the sigmapoints at every iteration. Takes the current covariance of the filter state.
pub fn calc_sp_deltas(&self, p: &Covariance<S>) -> [State<S>; 2 * S] {
let mut deltas = [State::<S>::zeros(); 2 * S];
let cp = self.c * p;
let lower = cp.cholesky().unwrap().l();
for i in 0..S {
deltas[i].set_column(0, &lower.column(i));
}
for i in S..2 * S {
deltas[i].set_column(0, &lower.column(i - S));
deltas[i] *= -1.0;
}
return deltas;
}
/// Calculate the sigma points using the filter state and its covariance.
pub fn calc_sp(&self, x: &State<S>, p: &Covariance<S>) -> [State<S>; 2 * S + 1] {
let mut sp = [State::<S>::zeros(); 2 * S + 1];
let deltas = self.calc_sp_deltas(p);
sp[0] = *x;
for i in 1..(2 * S + 1) {
sp[i] = sp[0] + deltas[i - 1];
}
return sp;
}
/// Run the filter state update. Takes the filter estimate from timestep k given all measurements up to and including k to timestep k+1 given all measurements up to and including k. Takes the current control of the system, the dynamics function, and the covariance of the assumed zero-mean process noise.
pub fn update<const C: usize, D>(
&mut self,
control_k: &Control<C>,
dt: f64,
mut dynamics_model: D,
dynamics_model_cov: &Covariance<S>,
) where
D: FnMut(&State<S>, &Control<C>, &f64) -> State<S>,
{
// Sigma points for update
let sp_k_k = self.calc_sp(&self.state, &self.covariance);
// Apply dynamics model to the sigma points at k given k
let mut sp_kp1_k = [State::<S>::zeros(); 2 * S + 1];
for i in 0..(2 * S + 1) {
sp_kp1_k[i] = dynamics_model(&sp_k_k[i], &control_k, &dt);
}
// Obtain the state at time k+1 given k
let mut x_kp1_k = State::<S>::zeros();
for i in 0..(2 * S + 1) {
x_kp1_k += self.wm[i] * sp_kp1_k[i];
}
// Compute predicted state covariance at k+1 given k
let mut cov_kp1_k = Covariance::<S>::zeros();
for i in 0..(2 * S + 1) {
cov_kp1_k += self.wc[i] * (sp_kp1_k[i] - x_kp1_k) * (sp_kp1_k[i] - x_kp1_k).transpose();
}
cov_kp1_k += dynamics_model_cov; // Adding additive process noise
self.state = x_kp1_k;
self.covariance = cov_kp1_k;
}
/// Run a filter measurement update. Takes the filter estimate from timestep k given all measurements relevant to the provided measurement function up to and including k-1 to timestep k given all measurements relevant to the provided measurement function up to and including k. Takes the current control of the system, the relevant measurement values (output), the measurement function, and the covariance of the assumed zero-mean measurement noise.
pub fn innovate<const C: usize, const Y: usize, M>(
&mut self,
control_k: &Control<C>,
output_k: &Output<Y>,
mut measurement_model: M,
measurement_model_cov: &Covariance<Y>,
) where
M: FnMut(&State<S>, &Control<C>) -> Output<Y>,
{
// Sigma points for update from measurements at the current step k given k-1
let sp_k_km1 = self.calc_sp(&self.state, &self.covariance);
// Apply measurement model to the sigma points to get predicted measurements at k given k-1
let mut y_k_km1 = [Output::<Y>::zeros(); 2 * S + 1];
for i in 0..(2 * S + 1) {
y_k_km1[i] = measurement_model(&sp_k_km1[i], &control_k);
}
// Combine predicted sigma points to get predicted measurement at k
let mut y_k = Output::<Y>::zeros();
for i in 0..(2 * S + 1) {
y_k += self.wm[i] * y_k_km1[i];
}
// Estimate covariance for the predicted measurement at k
let mut cov_y = Covariance::<Y>::zeros();
for i in 0..(2 * S + 1) {
cov_y += self.wc[i] * (y_k_km1[i] - y_k) * (y_k_km1[i] - y_k).transpose();
}
cov_y += measurement_model_cov; // Additive noise
// Estimate cross covariance between predicted measurement at k and state at k given k-1
let mut p_xy = CovarianceSY::<S, Y>::zeros();
for i in 1..(2 * S + 1) {
p_xy += (sp_k_km1[i] - sp_k_km1[0]) * (y_k_km1[i] - y_k).transpose();
}
p_xy *= self.wm[1];
// Calculate Kalman gain
let k = p_xy * cov_y.try_inverse().unwrap();
// Calculate new state and covariance
self.state = sp_k_km1[0] + k * (output_k - y_k);
self.covariance = self.covariance - k * cov_y * k.transpose();
}
/// A convenience function. This runs both a measurement update and a state update in that order. The arguments are passed directly on to those functions.
///
/// This can be used when there is only one measurement function and therefore no need to run the steps separately.
pub fn progress<const C: usize, const Y: usize, D, M>(
&mut self,
dt: f64,
control_k: Control<C>,
output_k: Output<Y>,
dynamics_model: D,
dynamics_model_cov: &Covariance<S>,
measurement_model: M,
measurement_model_cov: &Covariance<Y>,
) where
D: FnMut(&State<S>, &Control<C>, &f64) -> State<S>,
M: FnMut(&State<S>, &Control<C>) -> Output<Y>,
{
self.innovate(
&control_k,
&output_k,
measurement_model,
measurement_model_cov,
);
self.update(&control_k, dt, dynamics_model, dynamics_model_cov);
}
}