ekf2: move yaw_estimator and derivation to dedicated folder

This commit is contained in:
Daniel Agar
2024-04-18 15:14:59 -04:00
committed by GitHub
parent 2dccd6cacb
commit ec3ceae45e
14 changed files with 126 additions and 61 deletions
@@ -0,0 +1,39 @@
############################################################################
#
# Copyright (c) 2024 PX4 Development Team. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# 3. Neither the name PX4 nor the names of its contributors may be
# used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
############################################################################
add_library(yaw_estimator
EKFGSF_yaw.cpp
EKFGSF_yaw.h
)
add_dependencies(yaw_estimator prebuild_targets)
@@ -0,0 +1,492 @@
/****************************************************************************
*
* Copyright (c) 2020-2024 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
#include "EKFGSF_yaw.h"
#include <cstdlib>
#include <lib/geo/geo.h> // CONSTANTS_ONE_G
#include "derivation/generated/yaw_est_predict_covariance.h"
#include "derivation/generated/yaw_est_compute_measurement_update.h"
using matrix::AxisAnglef;
using matrix::Dcmf;
using matrix::Eulerf;
using matrix::Matrix3f;
using matrix::Quatf;
using matrix::Vector2f;
using matrix::Vector3f;
using matrix::wrap_pi;
using math::Utilities::getEulerYaw;
using math::Utilities::updateYawInRotMat;
EKFGSF_yaw::EKFGSF_yaw()
{
reset();
}
void EKFGSF_yaw::reset()
{
_ekf_gsf_vel_fuse_started = false;
_gsf_yaw_variance = INFINITY;
}
void EKFGSF_yaw::predict(const matrix::Vector3f &delta_ang, const float delta_ang_dt, const matrix::Vector3f &delta_vel,
const float delta_vel_dt, bool in_air)
{
const Vector3f accel = delta_vel / delta_vel_dt;
if (delta_vel_dt > 0.001f) {
// to reduce effect of vibration, filter using an LPF whose time constant is 1/10 of the AHRS tilt correction time constant
const float filter_coef = fminf(10.f * delta_vel_dt * _tilt_gain, 1.f);
_ahrs_accel = _ahrs_accel * (1.f - filter_coef) + accel * filter_coef;
} else {
return;
}
// Initialise states first time
if (!_ahrs_ekf_gsf_tilt_aligned) {
// check for excessive acceleration to reduce likelihood of large initial roll/pitch errors
// due to vehicle movement
const float accel_norm_sq = accel.norm_squared();
const float accel_lpf_norm_sq = _ahrs_accel.norm_squared();
static constexpr float upper_accel_limit = CONSTANTS_ONE_G * 1.1f;
static constexpr float lower_accel_limit = CONSTANTS_ONE_G * 0.9f;
const bool ok_to_align = (accel_norm_sq > sq(lower_accel_limit)) && (accel_norm_sq < sq(upper_accel_limit))
&& (accel_lpf_norm_sq > sq(lower_accel_limit)) && (accel_lpf_norm_sq < sq(upper_accel_limit));
if (ok_to_align) {
ahrsAlignTilt(delta_vel);
_ahrs_ekf_gsf_tilt_aligned = true;
} else {
return;
}
}
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index ++) {
predictEKF(model_index, delta_ang, delta_ang_dt, delta_vel, delta_vel_dt, in_air);
}
}
void EKFGSF_yaw::fuseVelocity(const Vector2f &vel_NE, const float vel_accuracy, const bool in_air)
{
// we don't start running the EKF part of the algorithm until there are regular velocity observations
if (!_ekf_gsf_vel_fuse_started) {
initialiseEKFGSF(vel_NE, vel_accuracy);
ahrsAlignYaw();
// don't start until in air or velocity is not negligible
if (in_air || vel_NE.longerThan(vel_accuracy)) {
_ekf_gsf_vel_fuse_started = true;
}
} else {
bool bad_update = false;
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index++) {
// subsequent measurements are fused as direct state observations
if (!updateEKF(model_index, vel_NE, vel_accuracy)) {
bad_update = true;
}
}
if (!bad_update) {
float total_weight = 0.0f;
// calculate weighting for each model assuming a normal distribution
const float min_weight = 1e-5f;
uint8_t n_weight_clips = 0;
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index ++) {
_model_weights(model_index) = gaussianDensity(model_index) * _model_weights(model_index);
if (_model_weights(model_index) < min_weight) {
n_weight_clips++;
_model_weights(model_index) = min_weight;
}
total_weight += _model_weights(model_index);
}
// normalise the weighting function
if (n_weight_clips < N_MODELS_EKFGSF) {
_model_weights /= total_weight;
} else {
// all weights have collapsed due to excessive innovation variances so reset filters
reset();
}
}
// Calculate a composite yaw vector as a weighted average of the states for each model.
// To avoid issues with angle wrapping, the yaw state is converted to a vector with length
// equal to the weighting value before it is summed.
Vector2f yaw_vector;
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index ++) {
yaw_vector(0) += _model_weights(model_index) * cosf(_ekf_gsf[model_index].X(2));
yaw_vector(1) += _model_weights(model_index) * sinf(_ekf_gsf[model_index].X(2));
}
_gsf_yaw = atan2f(yaw_vector(1), yaw_vector(0));
// calculate a composite variance for the yaw state from a weighted average of the variance for each model
// models with larger innovations are weighted less
_gsf_yaw_variance = 0.0f;
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index ++) {
const float yaw_delta = wrap_pi(_ekf_gsf[model_index].X(2) - _gsf_yaw);
_gsf_yaw_variance += _model_weights(model_index) * (_ekf_gsf[model_index].P(2, 2) + yaw_delta * yaw_delta);
}
}
}
void EKFGSF_yaw::ahrsPredict(const uint8_t model_index, const Vector3f &delta_ang, const float delta_ang_dt)
{
// generate attitude solution using simple complementary filter for the selected model
const Vector3f ang_rate = delta_ang / fmaxf(delta_ang_dt, 0.001f) - _ahrs_ekf_gsf[model_index].gyro_bias;
const Dcmf R_to_body = _ahrs_ekf_gsf[model_index].R.transpose();
const Vector3f gravity_direction_bf = R_to_body.col(2);
const float ahrs_accel_norm = _ahrs_accel.norm();
// gain from accel vector tilt error to rate gyro correction used by AHRS calculation
const float ahrs_accel_fusion_gain = ahrsCalcAccelGain();
// Perform angular rate correction using accel data and reduce correction as accel magnitude moves away from 1 g (reduces drift when vehicle picked up and moved).
// During fixed wing flight, compensate for centripetal acceleration assuming coordinated turns and X axis forward
Vector3f tilt_correction{};
if (ahrs_accel_fusion_gain > 0.f) {
Vector3f accel = _ahrs_accel;
if (PX4_ISFINITE(_true_airspeed) && (_true_airspeed > FLT_EPSILON)) {
// Calculate body frame centripetal acceleration with assumption X axis is aligned with the airspeed vector
// Use cross product of body rate and body frame airspeed vector
const Vector3f centripetal_accel_bf = Vector3f(0.0f, _true_airspeed * ang_rate(2), - _true_airspeed * ang_rate(1));
// correct measured accel for centripetal acceleration
accel -= centripetal_accel_bf;
}
tilt_correction = (gravity_direction_bf % accel) * ahrs_accel_fusion_gain / ahrs_accel_norm;
}
// Gyro bias estimation
constexpr float gyro_bias_limit = 0.05f;
const float spin_rate = ang_rate.length();
if (spin_rate < math::radians(10.f)) {
_ahrs_ekf_gsf[model_index].gyro_bias -= tilt_correction * (_gyro_bias_gain * delta_ang_dt);
_ahrs_ekf_gsf[model_index].gyro_bias = matrix::constrain(_ahrs_ekf_gsf[model_index].gyro_bias,
-gyro_bias_limit, gyro_bias_limit);
}
// delta angle from previous to current frame
const Vector3f delta_angle_corrected = delta_ang
+ (tilt_correction - _ahrs_ekf_gsf[model_index].gyro_bias) * delta_ang_dt;
// Apply delta angle to rotation matrix
_ahrs_ekf_gsf[model_index].R = ahrsPredictRotMat(_ahrs_ekf_gsf[model_index].R, delta_angle_corrected);
}
void EKFGSF_yaw::ahrsAlignTilt(const Vector3f &delta_vel)
{
// Rotation matrix is constructed directly from acceleration measurement and will be the same for
// all models so only need to calculate it once. Assumptions are:
// 1) Yaw angle is zero - yaw is aligned later for each model when velocity fusion commences.
// 2) The vehicle is not accelerating so all of the measured acceleration is due to gravity.
// Calculate earth frame Down axis unit vector rotated into body frame
const Vector3f down_in_bf = -delta_vel.normalized();
// Calculate earth frame North axis unit vector rotated into body frame, orthogonal to 'down_in_bf'
const Vector3f i_vec_bf(1.f, 0.f, 0.f);
Vector3f north_in_bf = i_vec_bf - down_in_bf * (i_vec_bf.dot(down_in_bf));
north_in_bf.normalize();
// Calculate earth frame East axis unit vector rotated into body frame, orthogonal to 'down_in_bf' and 'north_in_bf'
const Vector3f east_in_bf = down_in_bf % north_in_bf;
// Each column in a rotation matrix from earth frame to body frame represents the projection of the
// corresponding earth frame unit vector rotated into the body frame, eg 'north_in_bf' would be the first column.
// We need the rotation matrix from body frame to earth frame so the earth frame unit vectors rotated into body
// frame are copied into corresponding rows instead.
Dcmf R;
R.setRow(0, north_in_bf);
R.setRow(1, east_in_bf);
R.setRow(2, down_in_bf);
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index++) {
_ahrs_ekf_gsf[model_index].R = R;
}
}
void EKFGSF_yaw::ahrsAlignYaw()
{
// Align yaw angle for each model
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index++) {
const float yaw = wrap_pi(_ekf_gsf[model_index].X(2));
const Dcmf R = _ahrs_ekf_gsf[model_index].R;
_ahrs_ekf_gsf[model_index].R = updateYawInRotMat(yaw, R);
}
}
void EKFGSF_yaw::predictEKF(const uint8_t model_index, const Vector3f &delta_ang, const float delta_ang_dt,
const Vector3f &delta_vel, const float delta_vel_dt, bool in_air)
{
// generate an attitude reference using IMU data
ahrsPredict(model_index, delta_ang, delta_ang_dt);
// we don't start running the EKF part of the algorithm until there are regular velocity observations
if (!_ekf_gsf_vel_fuse_started) {
return;
}
// Calculate the yaw state using a projection onto the horizontal that avoids gimbal lock
_ekf_gsf[model_index].X(2) = getEulerYaw(_ahrs_ekf_gsf[model_index].R);
// calculate delta velocity in a horizontal front-right frame
const Vector3f del_vel_NED = _ahrs_ekf_gsf[model_index].R * delta_vel;
const float cos_yaw = cosf(_ekf_gsf[model_index].X(2));
const float sin_yaw = sinf(_ekf_gsf[model_index].X(2));
const float dvx = del_vel_NED(0) * cos_yaw + del_vel_NED(1) * sin_yaw;
const float dvy = - del_vel_NED(0) * sin_yaw + del_vel_NED(1) * cos_yaw;
const float daz = Vector3f(_ahrs_ekf_gsf[model_index].R * delta_ang)(2);
// delta velocity process noise double if we're not in air
const float accel_noise = in_air ? _accel_noise : 2.f * _accel_noise;
const float d_vel_var = sq(accel_noise * delta_vel_dt);
// Use fixed values for delta angle process noise variances
const float d_ang_var = sq(_gyro_noise * delta_ang_dt);
sym::YawEstPredictCovariance(_ekf_gsf[model_index].X, _ekf_gsf[model_index].P,
Vector2f(dvx, dvy), d_vel_var, daz, d_ang_var, &_ekf_gsf[model_index].P);
// covariance matrix is symmetrical, so copy upper half to lower half
_ekf_gsf[model_index].P(1, 0) = _ekf_gsf[model_index].P(0, 1);
_ekf_gsf[model_index].P(2, 0) = _ekf_gsf[model_index].P(0, 2);
_ekf_gsf[model_index].P(2, 1) = _ekf_gsf[model_index].P(1, 2);
// constrain variances
const float min_var = 1e-6f;
for (unsigned index = 0; index < 3; index++) {
_ekf_gsf[model_index].P(index, index) = fmaxf(_ekf_gsf[model_index].P(index, index), min_var);
}
// sum delta velocities in earth frame:
_ekf_gsf[model_index].X(0) += del_vel_NED(0);
_ekf_gsf[model_index].X(1) += del_vel_NED(1);
}
bool EKFGSF_yaw::updateEKF(const uint8_t model_index, const Vector2f &vel_NE, const float vel_accuracy)
{
// set observation variance from accuracy estimate supplied by GPS and apply a sanity check minimum
const float vel_obs_var = sq(fmaxf(vel_accuracy, 0.01f));
// calculate velocity observation innovations
_ekf_gsf[model_index].innov(0) = _ekf_gsf[model_index].X(0) - vel_NE(0);
_ekf_gsf[model_index].innov(1) = _ekf_gsf[model_index].X(1) - vel_NE(1);
matrix::Matrix<float, 3, 2> K;
matrix::SquareMatrix<float, 3> P_new;
sym::YawEstComputeMeasurementUpdate(_ekf_gsf[model_index].P,
vel_obs_var,
FLT_EPSILON,
&_ekf_gsf[model_index].S_inverse,
&_ekf_gsf[model_index].S_det_inverse,
&K,
&P_new);
_ekf_gsf[model_index].P = P_new;
// copy upper to lower diagonal
_ekf_gsf[model_index].P(1, 0) = _ekf_gsf[model_index].P(0, 1);
_ekf_gsf[model_index].P(2, 0) = _ekf_gsf[model_index].P(0, 2);
_ekf_gsf[model_index].P(2, 1) = _ekf_gsf[model_index].P(1, 2);
// constrain variances
const float min_var = 1e-6f;
for (unsigned index = 0; index < 3; index++) {
_ekf_gsf[model_index].P(index, index) = fmaxf(_ekf_gsf[model_index].P(index, index), min_var);
}
// test ratio = transpose(innovation) * inverse(innovation variance) * innovation = [1x2] * [2,2] * [2,1] = [1,1]
const float test_ratio = _ekf_gsf[model_index].innov * (_ekf_gsf[model_index].S_inverse * _ekf_gsf[model_index].innov);
// Perform a chi-square innovation consistency test and calculate a compression scale factor
// that limits the magnitude of innovations to 5-sigma
// If the test ratio is greater than 25 (5 Sigma) then reduce the length of the innovation vector to clip it at 5-Sigma
// This protects from large measurement spikes
const float innov_comp_scale_factor = test_ratio > 25.f ? sqrtf(25.0f / test_ratio) : 1.f;
// Correct the state vector and capture the change in yaw angle
const float oldYaw = _ekf_gsf[model_index].X(2);
_ekf_gsf[model_index].X -= (K * _ekf_gsf[model_index].innov) * innov_comp_scale_factor;
const float yawDelta = _ekf_gsf[model_index].X(2) - oldYaw;
// apply the change in yaw angle to the AHRS
// take advantage of sparseness in the yaw rotation matrix
const float cosYaw = cosf(yawDelta);
const float sinYaw = sinf(yawDelta);
const float R_prev00 = _ahrs_ekf_gsf[model_index].R(0, 0);
const float R_prev01 = _ahrs_ekf_gsf[model_index].R(0, 1);
const float R_prev02 = _ahrs_ekf_gsf[model_index].R(0, 2);
_ahrs_ekf_gsf[model_index].R(0, 0) = R_prev00 * cosYaw - _ahrs_ekf_gsf[model_index].R(1, 0) * sinYaw;
_ahrs_ekf_gsf[model_index].R(0, 1) = R_prev01 * cosYaw - _ahrs_ekf_gsf[model_index].R(1, 1) * sinYaw;
_ahrs_ekf_gsf[model_index].R(0, 2) = R_prev02 * cosYaw - _ahrs_ekf_gsf[model_index].R(1, 2) * sinYaw;
_ahrs_ekf_gsf[model_index].R(1, 0) = R_prev00 * sinYaw + _ahrs_ekf_gsf[model_index].R(1, 0) * cosYaw;
_ahrs_ekf_gsf[model_index].R(1, 1) = R_prev01 * sinYaw + _ahrs_ekf_gsf[model_index].R(1, 1) * cosYaw;
_ahrs_ekf_gsf[model_index].R(1, 2) = R_prev02 * sinYaw + _ahrs_ekf_gsf[model_index].R(1, 2) * cosYaw;
return true;
}
void EKFGSF_yaw::initialiseEKFGSF(const Vector2f &vel_NE, const float vel_accuracy)
{
_gsf_yaw = 0.0f;
_gsf_yaw_variance = sq(M_PI_F / 2.f);
_model_weights.setAll(1.0f / (float)N_MODELS_EKFGSF); // All filter models start with the same weight
const float yaw_increment = 2.f * M_PI_F / (float)N_MODELS_EKFGSF;
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index++) {
_ekf_gsf[model_index] = {};
// evenly space initial yaw estimates in the region between +-Pi
_ekf_gsf[model_index].X(2) = -M_PI_F + (0.5f * yaw_increment) + ((float)model_index * yaw_increment);
// take velocity states and corresponding variance from last measurement
_ekf_gsf[model_index].X(0) = vel_NE(0);
_ekf_gsf[model_index].X(1) = vel_NE(1);
_ekf_gsf[model_index].P(0, 0) = sq(fmaxf(vel_accuracy, 0.01f));
_ekf_gsf[model_index].P(1, 1) = _ekf_gsf[model_index].P(0, 0);
// use half yaw interval for yaw uncertainty
_ekf_gsf[model_index].P(2, 2) = sq(0.5f * yaw_increment);
}
}
float EKFGSF_yaw::gaussianDensity(const uint8_t model_index) const
{
// calculate transpose(innovation) * inv(S) * innovation
const float normDist = _ekf_gsf[model_index].innov.dot(_ekf_gsf[model_index].S_inverse * _ekf_gsf[model_index].innov);
return (1.f / (2.f * M_PI_F)) * sqrtf(_ekf_gsf[model_index].S_det_inverse) * expf(-0.5f * normDist);
}
bool EKFGSF_yaw::getLogData(float *yaw_composite, float *yaw_variance, float yaw[N_MODELS_EKFGSF],
float innov_VN[N_MODELS_EKFGSF], float innov_VE[N_MODELS_EKFGSF], float weight[N_MODELS_EKFGSF]) const
{
if (_ekf_gsf_vel_fuse_started) {
*yaw_composite = _gsf_yaw;
*yaw_variance = _gsf_yaw_variance;
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index++) {
yaw[model_index] = _ekf_gsf[model_index].X(2);
innov_VN[model_index] = _ekf_gsf[model_index].innov(0);
innov_VE[model_index] = _ekf_gsf[model_index].innov(1);
weight[model_index] = _model_weights(model_index);
}
return true;
}
return false;
}
float EKFGSF_yaw::ahrsCalcAccelGain() const
{
// Calculate the acceleration fusion gain using a continuous function that is unity at 1g and zero
// at the min and max g value. Allow for more acceleration when flying as a fixed wing vehicle using centripetal
// acceleration correction as higher and more sustained g will be experienced.
// Use a quadratic instead of linear function to prevent vibration around 1g reducing the tilt correction effectiveness.
// see https://www.desmos.com/calculator/dbqbxvnwfg
float attenuation = 2.f;
const bool centripetal_accel_compensation_enabled = PX4_ISFINITE(_true_airspeed) && (_true_airspeed > FLT_EPSILON);
const float ahrs_accel_norm = _ahrs_accel.norm();
if (centripetal_accel_compensation_enabled && (ahrs_accel_norm > CONSTANTS_ONE_G)) {
attenuation = 1.f;
}
const float delta_accel_g = (ahrs_accel_norm - CONSTANTS_ONE_G) / CONSTANTS_ONE_G;
return _tilt_gain * sq(1.f - math::min(attenuation * fabsf(delta_accel_g), 1.f));
}
Matrix3f EKFGSF_yaw::ahrsPredictRotMat(const Matrix3f &R, const Vector3f &g)
{
Matrix3f ret = R;
ret(0, 0) += R(0, 1) * g(2) - R(0, 2) * g(1);
ret(0, 1) += R(0, 2) * g(0) - R(0, 0) * g(2);
ret(0, 2) += R(0, 0) * g(1) - R(0, 1) * g(0);
ret(1, 0) += R(1, 1) * g(2) - R(1, 2) * g(1);
ret(1, 1) += R(1, 2) * g(0) - R(1, 0) * g(2);
ret(1, 2) += R(1, 0) * g(1) - R(1, 1) * g(0);
ret(2, 0) += R(2, 1) * g(2) - R(2, 2) * g(1);
ret(2, 1) += R(2, 2) * g(0) - R(2, 0) * g(2);
ret(2, 2) += R(2, 0) * g(1) - R(2, 1) * g(0);
// Renormalise rows
for (uint8_t r = 0; r < 3; r++) {
const float rowLengthSq = ret.row(r).norm_squared();
if (rowLengthSq > FLT_EPSILON) {
// Use linear approximation for inverse sqrt taking advantage of the row length being close to 1.0
const float rowLengthInv = 1.5f - 0.5f * rowLengthSq;
ret.row(r) *= rowLengthInv;
}
}
return ret;
}
@@ -0,0 +1,153 @@
/****************************************************************************
*
* Copyright (c) 2020-2024 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
#ifndef EKF_EKFGSF_YAW_H
#define EKF_EKFGSF_YAW_H
#include <lib/mathlib/mathlib.h>
#include <lib/matrix/matrix/math.hpp>
static constexpr uint8_t N_MODELS_EKFGSF = 5;
class EKFGSF_yaw
{
public:
EKFGSF_yaw();
// Update Filter States - this should be called whenever new IMU data is available
void predict(const matrix::Vector3f &delta_ang, const float delta_ang_dt,
const matrix::Vector3f &delta_vel, const float delta_vel_dt,
bool in_air = false);
// vel_NE: NE velocity measurement (m/s)
// vel_accuracy: 1-sigma accuracy of velocity measurement (m/s)
void fuseVelocity(const matrix::Vector2f &vel_NE, float vel_accuracy, bool in_air);
void setTrueAirspeed(float true_airspeed) { _true_airspeed = true_airspeed; }
void setGyroBias(const matrix::Vector3f &imu_gyro_bias, const bool force = false)
{
// Initialise to gyro bias estimate from main filter because there could be a large
// uncorrected rate gyro bias error about the gravity vector
if (!_ahrs_ekf_gsf_tilt_aligned || !_ekf_gsf_vel_fuse_started || force) {
// init gyro bias for each model
for (uint8_t model_index = 0; model_index < N_MODELS_EKFGSF; model_index++) {
_ahrs_ekf_gsf[model_index].gyro_bias = imu_gyro_bias;
}
}
}
// get solution data for logging
bool getLogData(float *yaw_composite,
float *yaw_composite_variance,
float yaw[N_MODELS_EKFGSF],
float innov_VN[N_MODELS_EKFGSF],
float innov_VE[N_MODELS_EKFGSF],
float weight[N_MODELS_EKFGSF]) const;
bool isActive() const { return _ekf_gsf_vel_fuse_started; }
float getYaw() const { return _gsf_yaw; }
float getYawVar() const { return _gsf_yaw_variance; }
void reset();
private:
// Parameters - these could be made tuneable
const float _gyro_noise{1.0e-1f}; // yaw rate noise used for covariance prediction (rad/sec)
const float _accel_noise{2.0f}; // horizontal accel noise used for covariance prediction (m/sec**2)
const float _tilt_gain{0.2f}; // gain from tilt error to gyro correction for complementary filter (1/sec)
const float _gyro_bias_gain{0.04f}; // gain applied to integral of gyro correction for complementary filter (1/sec)
// Declarations used by the bank of N_MODELS_EKFGSF AHRS complementary filters
float _true_airspeed{NAN}; // true airspeed used for centripetal accel compensation (m/s)
struct {
matrix::Dcmf R{matrix::eye<float, 3>()}; // matrix that rotates a vector from body to earth frame
matrix::Vector3f gyro_bias{}; // gyro bias learned and used by the quaternion calculation
} _ahrs_ekf_gsf[N_MODELS_EKFGSF] {};
bool _ahrs_ekf_gsf_tilt_aligned{false}; // true the initial tilt alignment has been calculated
matrix::Vector3f _ahrs_accel{0.f, 0.f, 0.f}; // low pass filtered body frame specific force vector used by AHRS calculation (m/s/s)
// calculate the gain from gravity vector misalingment to tilt correction to be used by all AHRS filters
float ahrsCalcAccelGain() const;
// update specified AHRS rotation matrix using IMU and optionally true airspeed data
void ahrsPredict(const uint8_t model_index, const matrix::Vector3f &delta_ang, const float delta_ang_dt);
// align all AHRS roll and pitch orientations using IMU delta velocity vector
void ahrsAlignTilt(const matrix::Vector3f &delta_vel);
// align all AHRS yaw orientations to initial values
void ahrsAlignYaw();
// Efficient propagation of a delta angle in body frame applied to the body to earth frame rotation matrix
matrix::Matrix3f ahrsPredictRotMat(const matrix::Matrix3f &R, const matrix::Vector3f &g);
// Declarations used by a bank of N_MODELS_EKFGSF EKFs
struct {
matrix::Vector3f X{}; // Vel North (m/s), Vel East (m/s), yaw (rad)s
matrix::SquareMatrix<float, 3> P{}; // covariance matrix
matrix::SquareMatrix<float, 2> S_inverse{}; // inverse of the innovation covariance matrix
float S_det_inverse{}; // inverse of the innovation covariance matrix determinant
matrix::Vector2f innov{}; // Velocity N,E innovation (m/s)
} _ekf_gsf[N_MODELS_EKFGSF] {};
bool _ekf_gsf_vel_fuse_started{}; // true when the EKF's have started fusing velocity data and the prediction and update processing is active
// initialise states and covariance data for the GSF and EKF filters
void initialiseEKFGSF(const matrix::Vector2f &vel_NE, const float vel_accuracy);
// predict state and covariance for the specified EKF using inertial data
void predictEKF(const uint8_t model_index, const matrix::Vector3f &delta_ang, const float delta_ang_dt,
const matrix::Vector3f &delta_vel, const float delta_vel_dt, bool in_air = false);
// update state and covariance for the specified EKF using a NE velocity measurement
// return false if update failed
bool updateEKF(const uint8_t model_index, const matrix::Vector2f &vel_NE, const float vel_accuracy);
inline float sq(float x) const { return x * x; };
// Declarations used by the Gaussian Sum Filter (GSF) that combines the individual EKF yaw estimates
matrix::Vector<float, N_MODELS_EKFGSF> _model_weights{};
float _gsf_yaw{}; // yaw estimate (rad)
float _gsf_yaw_variance{}; // variance of yaw estimate (rad^2)
// return the probability of the state estimate for the specified EKF assuming a gaussian error distribution
float gaussianDensity(const uint8_t model_index) const;
};
#endif // !EKF_EKFGSF_YAW_H
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright (c) 2022-2024 PX4 Development Team
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. Neither the name PX4 nor the names of its contributors may be
used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
File: derivation_yaw_estimator.py
Description:
"""
import symforce
symforce.set_epsilon_to_symbol()
import symforce.symbolic as sf
from symforce.values import Values
# generate_px4_function from derivation_utils in EKF/ekf_derivation/utils
import os, sys
derivation_utils_dir = os.path.dirname(os.path.abspath(__file__)) + "/../../python/ekf_derivation/utils"
sys.path.append(derivation_utils_dir)
import derivation_utils
State = Values(
vel = sf.V2(),
R = sf.Rot2() # 2D rotation to handle angle wrap
)
class VTangent(sf.Matrix):
SHAPE = (State.tangent_dim(), 1)
class MTangent(sf.Matrix):
SHAPE = (State.tangent_dim(), State.tangent_dim())
def rot2_small_angle(angle: sf.V1):
# Approximation for small "delta angles" to avoid trigonometric functions
return sf.Rot2(sf.Complex(1, angle[0]))
def yaw_est_predict_covariance(
state: VTangent,
P: MTangent,
d_vel: sf.V2,
d_vel_var: sf.Scalar,
d_ang: sf.Scalar,
d_ang_var: sf.Scalar,
):
state = State.from_tangent(state)
d_ang = sf.V1(d_ang) # cast to vector to gain group properties (e.g.: to_tangent)
state_error = Values(
vel = sf.V2.symbolic("delta_vel"),
yaw = sf.V1.symbolic("delta_yaw")
)
# True state kinematics
state_t = Values(
vel = state["vel"] + state_error["vel"],
R = state["R"] * rot2_small_angle(state_error["yaw"])
)
noise = Values(
d_vel = sf.V2.symbolic("a_n"),
d_ang = sf.V1.symbolic("w_n"),
)
input_t = Values(
d_vel = d_vel - noise["d_vel"],
d_ang = d_ang - noise["d_ang"]
)
state_t_pred = Values(
vel = state_t["vel"] + state_t["R"] * input_t["d_vel"],
R = state_t["R"] * rot2_small_angle(input_t["d_ang"])
)
# Nominal state kinematics
state_pred = Values(
vel = state["vel"] + state["R"] * d_vel,
R = state["R"] * rot2_small_angle(d_ang)
)
# Error state kinematics
delta_rot = (state_pred["R"].inverse() * state_t_pred["R"])
state_error_pred = Values(
vel = state_t_pred["vel"] - state_pred["vel"],
yaw = sf.simplify(delta_rot.z.imag) # small angle appriximation; use simplify to cancel R.T*R
)
zero_state_error = {state_error[key]: state_error[key].zero() for key in state_error.keys()}
zero_noise = {noise[key]: noise[key].zero() for key in noise.keys()}
# Calculate state transition matrix
F = VTangent(state_error_pred.to_storage()).jacobian(state_error).subs(zero_state_error).subs(zero_noise)
# Derive the covariance prediction equations
# Error growth in the inertial solution is assumed to be driven by 'noise' in the delta angles and
# velocities, after bias effects have been removed.
# derive the control(disturbance) influence matrix from IMU noise to error-state noise
G = VTangent(state_error_pred.to_storage()).jacobian(noise).subs(zero_state_error).subs(zero_noise)
# derive the state error matrix
var_u = sf.Matrix.diag([d_vel_var, d_vel_var, d_ang_var])
Q = G * var_u * G.T
P_new = F * P * F.T + Q
# Generate the equations for the upper triangular matrix and the diagonal only
# Since the matrix is symmetric, the lower triangle does not need to be derived
# and can simply be copied in the implementation
for index in range(State.tangent_dim()):
for j in range(State.tangent_dim()):
if index > j:
P_new[index,j] = 0
return P_new
def yaw_est_compute_measurement_update(
P: MTangent,
vel_obs_var: sf.Scalar,
epsilon : sf.Scalar
):
H = sf.Matrix([[1,0,0],
[0,1,0]])
R = sf.Matrix([[vel_obs_var , 0],
[0 , vel_obs_var]])
S = H * P * H.T + R
S_det = S[0, 0] * S[1, 1] - S[1, 0] * S[0, 1]
S_det_inv = derivation_utils.add_epsilon_sign(1 / S_det, S_det, epsilon)
# Compute inverse using simple formula for 2x2 matrix and using protected division
S_inv = sf.M22([[S[1, 1], -S[0, 1]], [-S[1, 0], S[0, 0]]]) * S_det_inv
K = (P * H.T) * S_inv
P_new = P - K * H * P
# Generate the equations for the upper triangular matrix and the diagonal only
# Since the matrix is symmetric, the lower triangle does not need to be derived
# and can simply be copied in the implementation
for index in range(State.tangent_dim()):
for j in range(State.tangent_dim()):
if index > j:
P_new[index,j] = 0
return (S_inv, S_det_inv, K, P_new)
print("Derive yaw estimator equations...")
derivation_utils.generate_px4_function(yaw_est_predict_covariance, output_names=["P_new"])
derivation_utils.generate_px4_function(yaw_est_compute_measurement_update, output_names=["S_inv", "S_det_inv", "K", "P_new"])
@@ -0,0 +1,102 @@
// -----------------------------------------------------------------------------
// This file was autogenerated by symforce from template:
// function/FUNCTION.h.jinja
// Do NOT modify by hand.
// -----------------------------------------------------------------------------
#pragma once
#include <matrix/math.hpp>
namespace sym {
/**
* This function was autogenerated from a symbolic function. Do not modify by hand.
*
* Symbolic function: yaw_est_compute_measurement_update
*
* Args:
* P: Matrix33
* vel_obs_var: Scalar
* epsilon: Scalar
*
* Outputs:
* S_inv: Matrix22
* S_det_inv: Scalar
* K: Matrix32
* P_new: Matrix33
*/
template <typename Scalar>
void YawEstComputeMeasurementUpdate(const matrix::Matrix<Scalar, 3, 3>& P, const Scalar vel_obs_var,
const Scalar epsilon,
matrix::Matrix<Scalar, 2, 2>* const S_inv = nullptr,
Scalar* const S_det_inv = nullptr,
matrix::Matrix<Scalar, 3, 2>* const K = nullptr,
matrix::Matrix<Scalar, 3, 3>* const P_new = nullptr) {
// Total ops: 60
// Input arrays
// Intermediate terms (15)
const Scalar _tmp0 = P(1, 1) + vel_obs_var;
const Scalar _tmp1 = P(0, 0) + vel_obs_var;
const Scalar _tmp2 = -P(0, 1) * P(1, 0) + _tmp0 * _tmp1;
const Scalar _tmp3 =
Scalar(1.0) /
(_tmp2 + epsilon * (2 * math::min<Scalar>(0, (((_tmp2) > 0) - ((_tmp2) < 0))) + 1));
const Scalar _tmp4 = _tmp0 * _tmp3;
const Scalar _tmp5 = P(1, 0) * _tmp3;
const Scalar _tmp6 = P(0, 1) * _tmp3;
const Scalar _tmp7 = _tmp1 * _tmp3;
const Scalar _tmp8 = -P(0, 1) * _tmp5;
const Scalar _tmp9 = P(0, 0) * _tmp4 + _tmp8;
const Scalar _tmp10 = -P(1, 1) * _tmp5 + _tmp0 * _tmp5;
const Scalar _tmp11 = P(2, 0) * _tmp4 - P(2, 1) * _tmp5;
const Scalar _tmp12 = -P(0, 0) * _tmp6 + _tmp1 * _tmp6;
const Scalar _tmp13 = P(1, 1) * _tmp7 + _tmp8;
const Scalar _tmp14 = -P(2, 0) * _tmp6 + P(2, 1) * _tmp7;
// Output terms (4)
if (S_inv != nullptr) {
matrix::Matrix<Scalar, 2, 2>& _s_inv = (*S_inv);
_s_inv(0, 0) = _tmp4;
_s_inv(1, 0) = -_tmp5;
_s_inv(0, 1) = -_tmp6;
_s_inv(1, 1) = _tmp7;
}
if (S_det_inv != nullptr) {
Scalar& _s_det_inv = (*S_det_inv);
_s_det_inv = _tmp3;
}
if (K != nullptr) {
matrix::Matrix<Scalar, 3, 2>& _k = (*K);
_k(0, 0) = _tmp9;
_k(1, 0) = _tmp10;
_k(2, 0) = _tmp11;
_k(0, 1) = _tmp12;
_k(1, 1) = _tmp13;
_k(2, 1) = _tmp14;
}
if (P_new != nullptr) {
matrix::Matrix<Scalar, 3, 3>& _p_new = (*P_new);
_p_new(0, 0) = -P(0, 0) * _tmp9 + P(0, 0) - P(1, 0) * _tmp12;
_p_new(1, 0) = 0;
_p_new(2, 0) = 0;
_p_new(0, 1) = -P(0, 1) * _tmp9 + P(0, 1) - P(1, 1) * _tmp12;
_p_new(1, 1) = -P(0, 1) * _tmp10 - P(1, 1) * _tmp13 + P(1, 1);
_p_new(2, 1) = 0;
_p_new(0, 2) = -P(0, 2) * _tmp9 + P(0, 2) - P(1, 2) * _tmp12;
_p_new(1, 2) = -P(0, 2) * _tmp10 - P(1, 2) * _tmp13 + P(1, 2);
_p_new(2, 2) = -P(0, 2) * _tmp11 - P(1, 2) * _tmp14 + P(2, 2);
}
} // NOLINT(readability/fn_size)
// NOLINTNEXTLINE(readability/fn_size)
} // namespace sym
@@ -0,0 +1,67 @@
// -----------------------------------------------------------------------------
// This file was autogenerated by symforce from template:
// function/FUNCTION.h.jinja
// Do NOT modify by hand.
// -----------------------------------------------------------------------------
#pragma once
#include <matrix/math.hpp>
namespace sym {
/**
* This function was autogenerated from a symbolic function. Do not modify by hand.
*
* Symbolic function: yaw_est_predict_covariance
*
* Args:
* state: Matrix31
* P: Matrix33
* d_vel: Matrix21
* d_vel_var: Scalar
* d_ang: Scalar
* d_ang_var: Scalar
*
* Outputs:
* P_new: Matrix33
*/
template <typename Scalar>
void YawEstPredictCovariance(const matrix::Matrix<Scalar, 3, 1>& state,
const matrix::Matrix<Scalar, 3, 3>& P,
const matrix::Matrix<Scalar, 2, 1>& d_vel, const Scalar d_vel_var,
const Scalar d_ang, const Scalar d_ang_var,
matrix::Matrix<Scalar, 3, 3>* const P_new = nullptr) {
// Total ops: 39
// Input arrays
// Intermediate terms (8)
const Scalar _tmp0 = std::cos(state(2, 0));
const Scalar _tmp1 = std::sin(state(2, 0));
const Scalar _tmp2 = -_tmp0 * d_vel(1, 0) - _tmp1 * d_vel(0, 0);
const Scalar _tmp3 = P(0, 2) + P(2, 2) * _tmp2;
const Scalar _tmp4 =
std::pow(_tmp0, Scalar(2)) * d_vel_var + std::pow(_tmp1, Scalar(2)) * d_vel_var;
const Scalar _tmp5 = _tmp0 * d_vel(0, 0) - _tmp1 * d_vel(1, 0);
const Scalar _tmp6 = P(1, 2) + P(2, 2) * _tmp5;
const Scalar _tmp7 = std::pow(d_ang, Scalar(2)) + 1;
// Output terms (1)
if (P_new != nullptr) {
matrix::Matrix<Scalar, 3, 3>& _p_new = (*P_new);
_p_new(0, 0) = P(0, 0) + P(2, 0) * _tmp2 + _tmp2 * _tmp3 + _tmp4;
_p_new(1, 0) = 0;
_p_new(2, 0) = 0;
_p_new(0, 1) = P(0, 1) + P(2, 1) * _tmp2 + _tmp3 * _tmp5;
_p_new(1, 1) = P(1, 1) + P(2, 1) * _tmp5 + _tmp4 + _tmp5 * _tmp6;
_p_new(2, 1) = 0;
_p_new(0, 2) = _tmp3 * _tmp7;
_p_new(1, 2) = _tmp6 * _tmp7;
_p_new(2, 2) = P(2, 2) * std::pow(_tmp7, Scalar(2)) + d_ang_var;
}
} // NOLINT(readability/fn_size)
// NOLINTNEXTLINE(readability/fn_size)
} // namespace sym