You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
11 KiB
366 lines
11 KiB
/**************************************************************************** |
|
* |
|
* Copyright (c) 2018 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. |
|
* |
|
****************************************************************************/ |
|
|
|
/** |
|
* @file WindEstimator.cpp |
|
* A wind and airspeed scale estimator. |
|
*/ |
|
|
|
#include "WindEstimator.hpp" |
|
|
|
bool |
|
WindEstimator::initialise(const matrix::Vector3f &velI, const matrix::Vector2f &velIvar, const float tas_meas) |
|
{ |
|
// do no initialise if ground velocity is low |
|
// this should prevent the filter from initialising on the ground |
|
if (sqrtf(velI(0) * velI(0) + velI(1) * velI(1)) < 3.0f) { |
|
return false; |
|
} |
|
|
|
const float v_n = velI(0); |
|
const float v_e = velI(1); |
|
|
|
// estimate heading from ground velocity |
|
const float heading_est = atan2f(v_e, v_n); |
|
|
|
// initilaise wind states assuming zero side slip and horizontal flight |
|
_state(INDEX_W_N) = velI(INDEX_W_N) - tas_meas * cosf(heading_est); |
|
_state(INDEX_W_E) = velI(INDEX_W_E) - tas_meas * sinf(heading_est); |
|
_state(INDEX_TAS_SCALE) = 1.0f; |
|
|
|
// compute jacobian of states wrt north/each earth velocity states and true airspeed measurement |
|
float L0 = v_e * v_e; |
|
float L1 = v_n * v_n; |
|
float L2 = L0 + L1; |
|
float L3 = tas_meas / (L2 * sqrtf(L2)); |
|
float L4 = L3 * v_e * v_n + 1.0f; |
|
float L5 = 1.0f / sqrtf(L2); |
|
float L6 = -L5 * tas_meas; |
|
|
|
matrix::Matrix3f L; |
|
L.setZero(); |
|
L(0, 0) = L4; |
|
L(0, 1) = L0 * L3 + L6; |
|
L(1, 0) = L1 * L3 + L6; |
|
L(1, 1) = L4; |
|
L(2, 2) = 1.0f; |
|
|
|
// get an estimate of the state covariance matrix given the estimated variance of ground velocity |
|
// and measured airspeed |
|
_P.setZero(); |
|
_P(INDEX_W_N, INDEX_W_N) = velIvar(0); |
|
_P(INDEX_W_E, INDEX_W_E) = velIvar(1); |
|
_P(INDEX_TAS_SCALE, INDEX_TAS_SCALE) = 0.0001f; |
|
|
|
_P = L * _P * L.transpose(); |
|
|
|
// reset the timestamp for measurement rejection |
|
_time_rejected_tas = 0; |
|
_time_rejected_beta = 0; |
|
|
|
_wind_estimator_reset = true; |
|
|
|
return true; |
|
} |
|
|
|
void |
|
WindEstimator::update(uint64_t time_now) |
|
{ |
|
if (!_initialised) { |
|
return; |
|
} |
|
|
|
// set reset state to false (is set to true when initialise fuction is called later) |
|
_wind_estimator_reset = false; |
|
|
|
// run covariance prediction at 1Hz |
|
if (time_now - _time_last_update < 1000 * 1000 || _time_last_update == 0) { |
|
if (_time_last_update == 0) { |
|
_time_last_update = time_now; |
|
} |
|
|
|
return; |
|
} |
|
|
|
float dt = (float)(time_now - _time_last_update) * 1e-6f; |
|
_time_last_update = time_now; |
|
|
|
float q_w = _wind_p_var; |
|
float q_k_tas = _tas_scale_p_var; |
|
|
|
float SPP0 = dt * dt; |
|
float SPP1 = SPP0 * q_w; |
|
float SPP2 = SPP1 + _P(0, 1); |
|
|
|
matrix::Matrix3f P_next; |
|
|
|
P_next(0, 0) = SPP1 + _P(0, 0); |
|
P_next(0, 1) = SPP2; |
|
P_next(0, 2) = _P(0, 2); |
|
P_next(1, 0) = SPP2; |
|
P_next(1, 1) = SPP1 + _P(1, 1); |
|
P_next(1, 2) = _P(1, 2); |
|
P_next(2, 0) = _P(0, 2); |
|
P_next(2, 1) = _P(1, 2); |
|
P_next(2, 2) = SPP0 * q_k_tas + _P(2, 2); |
|
_P = P_next; |
|
} |
|
|
|
void |
|
WindEstimator::fuse_airspeed(uint64_t time_now, const float true_airspeed, const matrix::Vector3f &velI, |
|
const matrix::Vector2f &velIvar) |
|
{ |
|
matrix::Vector2f velIvar_constrained = { math::max(0.01f, velIvar(0)), math::max(0.01f, velIvar(1)) }; |
|
|
|
if (!_initialised) { |
|
// try to initialise |
|
_initialised = initialise(velI, velIvar_constrained, true_airspeed); |
|
return; |
|
} |
|
|
|
// don't fuse faster than 10Hz |
|
if (time_now - _time_last_airspeed_fuse < 100 * 1000) { |
|
return; |
|
} |
|
|
|
_time_last_airspeed_fuse = time_now; |
|
|
|
// assign helper variables |
|
const float v_n = velI(0); |
|
const float v_e = velI(1); |
|
const float v_d = velI(2); |
|
|
|
const float k_tas = _state(INDEX_TAS_SCALE); |
|
|
|
// compute kalman gain K |
|
const float HH0 = sqrtf(v_d * v_d + (v_e - _state(INDEX_W_E)) * (v_e - _state(INDEX_W_E)) + (v_n - _state(INDEX_W_N)) * (v_n - _state(INDEX_W_N))); |
|
const float HH1 = k_tas / HH0; |
|
|
|
matrix::Matrix<float, 1, 3> H_tas; |
|
H_tas(0, 0) = HH1 * (-v_n + _state(INDEX_W_N)); |
|
H_tas(0, 1) = HH1 * (-v_e + _state(INDEX_W_E)); |
|
H_tas(0, 2) = HH0; |
|
|
|
matrix::Matrix<float, 3, 1> K = _P * H_tas.transpose(); |
|
|
|
const matrix::Matrix<float, 1, 1> S = H_tas * _P * H_tas.transpose() + _tas_var; |
|
|
|
K /= S(0,0); |
|
// compute innovation |
|
const float airspeed_pred = k_tas * sqrtf((v_n - _state(INDEX_W_N)) * (v_n - _state(INDEX_W_N)) + (v_e - _state(INDEX_W_E)) * |
|
(v_e - _state(INDEX_W_E)) + v_d * v_d); |
|
|
|
_tas_innov = true_airspeed - airspeed_pred; |
|
|
|
// innovation variance |
|
_tas_innov_var = S(0,0); |
|
|
|
bool reinit_filter = false; |
|
bool meas_is_rejected = false; |
|
|
|
meas_is_rejected = check_if_meas_is_rejected(time_now, _tas_innov, _tas_innov_var, _tas_gate, _time_rejected_tas, reinit_filter); |
|
|
|
reinit_filter |= _tas_innov_var < 0.0f; |
|
|
|
if (meas_is_rejected || reinit_filter) { |
|
if (reinit_filter) { |
|
_initialised = initialise(velI, matrix::Vector2f(0.1f, 0.1f), true_airspeed); |
|
} |
|
|
|
// we either did a filter reset or the current measurement was rejected so do not fuse |
|
return; |
|
} |
|
|
|
// apply correction to state |
|
_state(INDEX_W_N) += _tas_innov * K(0, 0); |
|
_state(INDEX_W_E) += _tas_innov * K(1, 0); |
|
_state(INDEX_TAS_SCALE) += _tas_innov * K(2, 0); |
|
|
|
// update covariance matrix |
|
_P = _P - K * H_tas * _P; |
|
|
|
run_sanity_checks(); |
|
} |
|
|
|
void |
|
WindEstimator::fuse_beta(uint64_t time_now, const matrix::Vector3f &velI, const matrix::Quatf &q_att) |
|
{ |
|
if (!_initialised) { |
|
_initialised = initialise(velI, matrix::Vector2f(0.1f, 0.1f), velI.length()); |
|
return; |
|
} |
|
|
|
// don't fuse faster than 10Hz |
|
if (time_now - _time_last_beta_fuse < 100 * 1000) { |
|
return; |
|
} |
|
|
|
_time_last_beta_fuse = time_now; |
|
|
|
const float v_n = velI(0); |
|
const float v_e = velI(1); |
|
const float v_d = velI(2); |
|
|
|
// compute sideslip observation vector |
|
float HB0 = 2.0f * q_att(0); |
|
float HB1 = HB0 * q_att(3); |
|
float HB2 = 2.0f * q_att(1); |
|
float HB3 = HB2 * q_att(2); |
|
float HB4 = v_e - _state(INDEX_W_E); |
|
float HB5 = HB1 + HB3; |
|
float HB6 = v_n - _state(INDEX_W_N); |
|
float HB7 = q_att(0) * q_att(0); |
|
float HB8 = q_att(3) * q_att(3); |
|
float HB9 = HB7 - HB8; |
|
float HB10 = q_att(1) * q_att(1); |
|
float HB11 = q_att(2) * q_att(2); |
|
float HB12 = HB10 - HB11; |
|
float HB13 = HB12 + HB9; |
|
float HB14 = HB13 * HB6 + HB4 * HB5 + v_d * (-HB0 * q_att(2) + HB2 * q_att(3)); |
|
float HB15 = 1.0f / HB14; |
|
float HB16 = (HB4 * (-HB10 + HB11 + HB9) + HB6 * (-HB1 + HB3) + v_d * (HB0 * q_att(1) + 2.0f * q_att(2) * q_att(3))) / |
|
(HB14 * HB14); |
|
|
|
matrix::Matrix<float, 1, 3> H_beta; |
|
H_beta(0, 0) = HB13 * HB16 + HB15 * (HB1 - HB3); |
|
H_beta(0, 1) = HB15 * (HB12 - HB7 + HB8) + HB16 * HB5; |
|
H_beta(0, 2) = 0; |
|
|
|
// compute kalman gain |
|
matrix::Matrix<float, 3, 1> K = _P * H_beta.transpose(); |
|
|
|
const matrix::Matrix<float, 1, 1> S = H_beta * _P * H_beta.transpose() + _beta_var; |
|
|
|
K /= S(0,0); |
|
|
|
// compute predicted side slip angle |
|
matrix::Vector3f rel_wind(velI(0) - _state(INDEX_W_N), velI(1) - _state(INDEX_W_E), velI(2)); |
|
matrix::Dcmf R_body_to_earth(q_att); |
|
rel_wind = R_body_to_earth.transpose() * rel_wind; |
|
|
|
if (fabsf(rel_wind(0)) < 0.1f) { |
|
return; |
|
} |
|
|
|
// use small angle approximation, sin(x) = x for small x |
|
const float beta_pred = rel_wind(1) / rel_wind(0); |
|
|
|
_beta_innov = 0.0f - beta_pred; |
|
_beta_innov_var = S(0,0); |
|
|
|
bool reinit_filter = false; |
|
bool meas_is_rejected = false; |
|
|
|
meas_is_rejected = check_if_meas_is_rejected(time_now, _beta_innov, _beta_innov_var, _beta_gate, _time_rejected_beta, |
|
reinit_filter); |
|
|
|
reinit_filter |= _beta_innov_var < 0.0f; |
|
|
|
if (meas_is_rejected || reinit_filter) { |
|
if (reinit_filter) { |
|
_initialised = initialise(velI, matrix::Vector2f(0.1f, 0.1f), velI.length()); |
|
} |
|
|
|
// we either did a filter reset or the current measurement was rejected so do not fuse |
|
return; |
|
} |
|
|
|
// apply correction to state |
|
_state(INDEX_W_N) += _beta_innov * K(0, 0); |
|
_state(INDEX_W_E) += _beta_innov * K(1, 0); |
|
_state(INDEX_TAS_SCALE) += _beta_innov * K(2, 0); |
|
|
|
// update covariance matrix |
|
_P = _P - K * H_beta * _P; |
|
|
|
run_sanity_checks(); |
|
} |
|
|
|
void |
|
WindEstimator::run_sanity_checks() |
|
{ |
|
for (unsigned i = 0; i < 3; i++) { |
|
if (_P(i, i) < 0.0f) { |
|
// ill-conditioned covariance matrix, reset filter |
|
_initialised = false; |
|
return; |
|
} |
|
|
|
// limit covariance diagonals if they grow too large |
|
if (i < 2) { |
|
_P(i, i) = _P(i, i) > 25.0f ? 25.0f : _P(i, i); |
|
|
|
} else if (i == 2) { |
|
_P(i, i) = _P(i, i) > 0.1f ? 0.1f : _P(i, i); |
|
} |
|
} |
|
|
|
if (!ISFINITE(_state(INDEX_W_N)) || !ISFINITE(_state(INDEX_W_E)) || !ISFINITE(_state(INDEX_TAS_SCALE))) { |
|
_initialised = false; |
|
return; |
|
} |
|
|
|
// check if we should inhibit learning of airspeed scale factor and rather use a pre-set value. |
|
// airspeed scale factor errors arise from sensor installation which does not change and only needs |
|
// to be computed once for a perticular installation. |
|
if (_enforced_airspeed_scale < 0) { |
|
_state(INDEX_TAS_SCALE) = math::max(0.0f, _state(INDEX_TAS_SCALE)); |
|
} else { |
|
_state(INDEX_TAS_SCALE) = _enforced_airspeed_scale; |
|
} |
|
|
|
// attain symmetry |
|
for (unsigned row = 0; row < 3; row++) { |
|
for (unsigned column = 0; column < row; column++) { |
|
float tmp = (_P(row, column) + _P(column, row)) / 2; |
|
_P(row, column) = tmp; |
|
_P(column, row) = tmp; |
|
} |
|
} |
|
} |
|
|
|
bool |
|
WindEstimator::check_if_meas_is_rejected(uint64_t time_now, float innov, float innov_var, uint8_t gate_size, |
|
uint64_t &time_meas_rejected, bool &reinit_filter) |
|
{ |
|
if (innov * innov > gate_size * gate_size * innov_var) { |
|
time_meas_rejected = time_meas_rejected == 0 ? time_now : time_meas_rejected; |
|
|
|
} else { |
|
time_meas_rejected = 0; |
|
} |
|
|
|
reinit_filter = time_now - time_meas_rejected > 5 * 1000 * 1000 && time_meas_rejected != 0; |
|
|
|
return time_meas_rejected != 0; |
|
}
|
|
|