State Estimation
Wolfgang Hönig
January 10, 2025
% vectors % custom math commands %
We have:
State estimation: find a mapping from \(\{\mathbf{o}_i \}_{i=1}^n \mapsto \mathbf{x}\)
What sensors do you know? What are the observations?
\[ \begin{align} \mathbf{v}_m = \mathbf{q}\odot \begin{pmatrix} \frac{\mathbf{p}_z \theta_p}{N_p} \frac{\Delta n_x}{\Delta t}\\ \frac{\mathbf{p}_z \theta_p}{N_p} \frac{\Delta n_y}{\Delta t}\\ 0 \end{pmatrix} \end{align} \]
where \(N_p = 350\), \(\theta_p = 0.71674\), and \(\Delta t\) is the time since last sensor data
Multirotor case: \[\begin{align} \mathbf{p}_z &= \alpha_z \mathbf{p}_z + (1 - \alpha_z) \mathbf{o}_z,\quad \alpha_z \in [0,1]\\ \mathbf{v}&= \alpha_{xy} \mathbf{v}+ (1 - \alpha_{xy}) \mathbf{v}_m,\quad \alpha_{xy} \in [0,1] \end{align}\]
where \(\mathbf{o}_z\) is the measurement of the height sensor and \(\mathbf{v}_m\) the (computed) measurement of the flow sensor.
measured values are: \(\mathbf{o}_g\) (gyroscope) and \(\mathbf{o}_a\) (accelerometer)
Use \(n(\mathbf{x}) = \frac{\mathbf{x}}{\| \mathbf{x}\|}\), and \(k_p, k_I \in \mathbb R\) tuning gains
Each step, compute: \[ \begin{align} \mathbf{e}&= n(\mathbf{o}_a) \times \left(\mathbf{q}\odot \mathbf{e}_3 \right) & \delta &= k_p \mathbf{e}+ k_I \int \mathbf{e}\\ \dot{\mathbf{q}}_{next} &= \frac{1}{2} \mathbf{q}\otimes \overline{\mathbf{o}_g + \delta} & \mathbf{q}_{next} &= n(\mathbf{q}+ \dot{\mathbf{q}}_{next} \Delta t) \end{align} \]
In words: Define the error as the cross product between measured gravity from the accelerometer and current estimate; use a PI controller to drive the error to zero.
Sample implementation is here
Here, \(\mathbf{a}= n(\mathbf{o}_a)\)
filter_madgwick.py
1D Multirotor
\[ \begin{align} \mathbf{\dot x} = f(\mathbf x, \mathbf u) = \begin{pmatrix} \dot z\\ \frac{f_1}{m} - g \end{pmatrix} \end{align} \]
1D Multirotor
\[ \begin{align} \mathbf{\dot x} &= f(\mathbf x, \mathbf u) = \begin{pmatrix} \dot z\\ \frac{f_1}{m} - g \end{pmatrix}\\ \mathbf{x}_{t+1} &= \mathbf{x}_t+\dot{\mathbf{x}}_t \Delta t = \begin{pmatrix} z\\ \dot z\end{pmatrix}+\begin{pmatrix} \dot z\\ \frac{f_1}{m} - g \end{pmatrix} \Delta t\\ \mathbf{x}_{t+1} &= \begin{pmatrix}1 & \Delta t \\ 0 & 1 \end{pmatrix} \mathbf{x}_t + \begin{pmatrix}0\\ \frac{\Delta t}{m} \end{pmatrix}\mathbf{u}_t + \begin{pmatrix}0 \\ -g \end{pmatrix}\\ \mathbf{o}_t &= \begin{pmatrix}1 & 0 \end{pmatrix} \mathbf{x}_t \end{align} \]
Goal: Gaussian estimate of the state, i.e. the mean \(\mu\) and covariance \(\Sigma\).
KF has two parts:
\[\begin{align} \hat{\mathbf{x}}_{t+1} &= \mathbf{A}\hat{\mathbf{x}}_t + \mathbf{B}\mathbf{u}_t\\ \Sigma_{t+1} &= \mathbf{A}\Sigma_t \mathbf{A}^\top + \mathbf{R} \end{align}\]
\[\begin{align} \mathbf{K}&= \Sigma_t \mathbf{C}^\top \left(\mathbf{C}\Sigma_t \mathbf{C}^\top + \mathbf{Q}\right)^{-1}\\ \hat{\mathbf{x}}_{t+1} &= \hat{\mathbf{x}}_t + \mathbf{K}(\mathbf{o}_t - \mathbf{C}\hat{\mathbf{x}}_t)\\ \Sigma_{t+1} &= (\mathbf{I}- \mathbf{K}\mathbf{C}) \Sigma_t \end{align}\]
How could one apply KF here?
Sympy demo: ekf.py
\[ \begin{align} \mathbf{q}&= \mathbf{q}_{ref} \otimes \mathbf{q}(\boldsymbol{\delta}) \\ \mathbf{q}(\boldsymbol{\delta}) &= \begin{pmatrix} \cos(\| \boldsymbol{\delta}\| /2)\\ \frac{\boldsymbol{\delta}}{\| \boldsymbol{\delta}\|} \sin (\| \boldsymbol{\delta}\| /2) \end{pmatrix} \approx \begin{pmatrix} 1.0 - \| \boldsymbol{\delta}\|^2 / 8\\ \boldsymbol{\delta}/2 \end{pmatrix} \end{align} \]
The name comes from quaternion multiplication \(\otimes\). Others refer to it as “indirect EKF” (Trawny and Roumeliotis 2005)
Benefits of using IMU as actions rather than measurements?
\[ \begin{align} \hat{\mathbf{x}}_{t+1} &= g(\hat{\mathbf{x}}_t, \mathbf{u}_t)\\ \mathbf{G}&= \frac{\partial g(\mathbf{x}, \mathbf{u})}{\partial \mathbf{x}} \vert_{\mathbf{x}=\hat{\mathbf{x}}_t, \mathbf{u}=\mathbf{u}_t}\\ \Sigma_{t+1} &= \mathbf{G}\Sigma_t \mathbf{G}^\top + \mathbf{R} \end{align} \]
\(G\) can be computed analytically or by numerical approximation:
\[\begin{align} G \approx \begin{pmatrix} \frac{\partial g_1(\mathbf{x}, \mathbf{u})}{\partial \mathbf{x}_1} & \ldots & \frac{\partial g_1(\mathbf{x}, \mathbf{u})}{\partial \mathbf{x}_N}\\ \vdots & \ldots & \vdots\\ \frac{\partial g_N(\mathbf{x}, \mathbf{u})}{\partial \mathbf{x}_1} & \ldots & \frac{\partial g_N(\mathbf{x}, \mathbf{u})}{\partial \mathbf{x}_N} \end{pmatrix} \end{align}\]
\(G\) becomes an easy expression, if
just using \(\mathbf{q}_{ref}\). (Demo:
mekf.py
)
\[ \begin{align} \mathbf{q}_{ref} &= \mathbf{q}_{ref} \otimes \mathbf{q}(\boldsymbol{\delta}) \\ \boldsymbol{\delta}&= \mathbf{0} \end{align} \]
Optional: rotate \(\Sigma\) (Mueller, Hehn, and D’Andrea 2017)
\[ \begin{align} \mathbf{H}&= \frac{\partial h(\mathbf{x})}{\partial \mathbf{x}} \vert_{\mathbf{x}=\hat{\mathbf{x}}_t}\\ &= \begin{pmatrix} 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \end{pmatrix} \end{align} \]
\[ \begin{align} \mathbf{K}&= \Sigma_t \mathbf{H}^\top \left(\mathbf{H}\Sigma_t \mathbf{H}^\top + \mathbf{Q}\right)^{-1}\\ \hat{\mathbf{x}}_{t+1} &= \hat{\mathbf{x}}_t + \mathbf{K}(\mathbf{o}_z - h(\hat{\mathbf{x}}_t))\\ \Sigma_{t+1} &= (\mathbf{I}- \mathbf{K}\mathbf{H}) \Sigma_t \end{align} \]
sympy demo: mekf_measurement_height.py
\[ \begin{align} \mathbf{v}_m = \mathbf{q}\odot \begin{pmatrix} \frac{\mathbf{p}_z \theta_p}{N_p} \frac{\Delta n_x}{\Delta t}\\ \frac{\mathbf{p}_z \theta_p}{N_p} \frac{\Delta n_y}{\Delta t}\\ 0 \end{pmatrix} \end{align} \]
If the filter tracks the velocity in body frame, this becomes much simpler.
\[ \begin{align} \mathbf{H}&= \frac{\partial h(\mathbf{x})}{\partial \mathbf{x}} \vert_{\mathbf{x}=\hat{\mathbf{x}}_t}\\ &= \begin{pmatrix} 0 & 0 & \frac{-N_p \mathbf{b}_x \Delta t}{\mathbf{p}_z^2 \theta_p} & \frac{N_p \Delta t}{\mathbf{p}_z \theta} & 0 & 0 & 0 & 0 & 0\\ 0 & 0 & \frac{-N_p \mathbf{b}_y \Delta t}{\mathbf{p}_z^2 \theta_p} & 0 & \frac{N_p \Delta t}{\mathbf{p}_z \theta} & 0 & 0 & 0 & 0 \end{pmatrix} \end{align} \]
\[ \begin{align} \mathbf{K}&= \Sigma_t \mathbf{H}^\top \left(\mathbf{H}\Sigma_t \mathbf{H}^\top + \mathbf{Q}\right)^{-1}\\ \hat{\mathbf{x}}_{t+1} &= \hat{\mathbf{x}}_t + \mathbf{K}(\mathbf{o}_{xy} - h(\hat{\mathbf{x}}_t))\\ \Sigma_{t+1} &= (\mathbf{I}- \mathbf{K}\mathbf{H}) \Sigma_t \end{align} \]
sympy demo: mekf_measurement_flow.py
Scalar Update
Apply measurement update for each row of \(\mathbf{H}\) separately. (Justification: the measurement was taken at different times).
Works well in practice, but only for highly observable systems.
Initialize state \(\hat{\mathbf{x}}\), covariance \(\Sigma\)
On IMU update
\[ \begin{align} \mathbf{H}&= \frac{\partial h(\mathbf{x})}{\partial \mathbf{x}} \vert_{\mathbf{x}=\hat{\mathbf{x}}_t}\\ \mathbf{K}&= \Sigma_t \mathbf{H}^\top \left(\mathbf{H}\Sigma_t \mathbf{H}^\top + \mathbf{Q}\right)^{-1}\\ \hat{\mathbf{x}}_{t+1} &= \hat{\mathbf{x}}_t + \mathbf{K}(\mathbf{o}_z - h(\hat{\mathbf{x}}_t))\\ \Sigma_{t+1} &= (\mathbf{I}- \mathbf{K}\mathbf{H}) \Sigma_t \end{align} \]
“In many practical applications, the difference between EKF and UKF is negligible.” (Probabilstic Robotics, page 70)
Implement the MEKF. Validate offline using the provided logging data and compare with the on-board EKF. Execute a physical test flight and qualitatively compare to the on-board EKF.
Variants:
app-config
, add
CONFIG_ESTIMATOR_OOT=y
lib.rs
add estimatorOutOfTreeInit
,
estimatorOutOfTreeTest
, and
estimatorOutOfTree
let mut measurement = measurement_t {
type_: 255,
data: bindings::measurement_t__bindgen_ty_1 {
gyroscope: bindings::gyroscopeMeasurement_t {
gyro: bindings::Axis3f {
axis: [0.0, 0.0, 0.0],
}}}};
loop {
unsafe {
let ok = estimatorDequeue(&mut measurement);
if !ok {
break
}
let mut t = measurement.type_ & 0xFF; // the & 0xFF is important
pub struct Mat<const Rows: usize, const Columns: usize> {
pub m: [[f32; Columns]; Rows],
}
// Rows1xColumns1 X Rows2 x Columns2
// where columns1 == rows2
impl<const R1: usize, const C1: usize,
const R2: usize, const C2: usize>
Mul<Mat<R2, C2>> for Mat<R1, C1> {
type Output = Mat<R1, C2>;
fn mul(self, other: Mat<R2, C2>) -> Mat<R1, C2> {
// ...
}
}
?