Compare commits

...

23 Commits

Author SHA1 Message Date
Marco Hauswirth c47b3ba1b0 * parameter renaming
* move hor-motion check to method
2025-08-26 15:08:55 +02:00
Marco Hauswirth db468f78a1 set state to unknown during init (re-init) 2025-08-26 15:08:55 +02:00
Marco Hauswirth 18bee5e08b revert unnecessary unit-test changes 2025-08-26 15:08:55 +02:00
Marco Hauswirth 58f63909ca * naming conventions
* add landed-state for range-finder-cons-check
* ajdust unit-tests
2025-08-26 15:08:55 +02:00
Marco Hauswirth 85b116f14e remove terrain checks form baroRef unit-tests, vehicle vertical motion is not defined in test 2025-08-26 15:08:53 +02:00
Marco Hauswirth c83debd64b always run consistency-KF but limit state change to vertical velocity. this fixes behavior for jumps in fixed wing 2025-08-26 15:08:43 +02:00
Marco Hauswirth 9df07a9542 minor cleanup 2025-08-26 15:08:43 +02:00
Marco Hauswirth f2df6c7264 only run range validator on regular data 2025-08-26 15:08:43 +02:00
Marco Hauswirth f89f73cf37 adjust IMU falling detection baro-range unittest 2025-08-26 15:08:43 +02:00
Marco Hauswirth a97cc72791 also run range consistency check while landed 2025-08-26 15:08:43 +02:00
Marco Hauswirth c98a7ef8ae change consistency to only be valid if running for a second 2025-08-26 15:08:43 +02:00
Marco Hauswirth 86725c0491 * clean up method calls
* move P-init to ekf-derivation script
2025-08-26 15:08:43 +02:00
Marco Hauswirth a078d6e306 fix uncertainty initialization 2025-08-26 15:08:43 +02:00
Marco Hauswirth f56c4cc370 clean up range-filter fusion step 2025-08-26 15:08:41 +02:00
Marco Hauswirth bb17333bf4 reset state to unknown on init 2025-08-26 15:08:09 +02:00
Marco Hauswirth 670d19c382 overhaul approach to work on unit tests and snow/blocking/normal flights 2025-08-26 15:08:09 +02:00
Marco Hauswirth ce6fe59c8a adjust threshold unit tests 2025-08-26 15:08:09 +02:00
Marco Hauswirth ba72d37f9b add symforce, adjust smaller things 2025-08-26 15:08:09 +02:00
Marco Hauswirth dade5c3a5c always update lpf 2025-08-26 15:08:09 +02:00
Marco Hauswirth 8c13115e27 fix bugs, rejection was too sensitive 2025-08-26 15:08:08 +02:00
Marco Hauswirth 4e40a86971 fix wrong miniEKF init 2025-08-26 15:07:33 +02:00
Marco Hauswirth 0058953e4f handle noise while hovering 2025-08-26 15:07:31 +02:00
Marco Hauswirth f1f1e6f059 add mini KF to validate range sensor measurements 2025-08-26 15:04:40 +02:00
15 changed files with 393 additions and 114 deletions
+1
View File
@@ -49,6 +49,7 @@ bool cs_valid_fake_pos # 41 - true if a valid constant position is bein
bool cs_constant_pos # 42 - true if the vehicle is at a constant position
bool cs_baro_fault # 43 - true when the current baro has been declared faulty and is no longer being used
bool cs_gnss_vel # 44 - true if GNSS velocity measurement fusion is intended
bool cs_rng_kin_unknown # 45 - true when the range finder kinematic consistency check is not running
# fault status
uint32 fault_status_changes # number of filter fault status (fs) changes
@@ -36,56 +36,157 @@
*/
#include <aid_sources/range_finder/range_finder_consistency_check.hpp>
#include "ekf_derivation/generated/range_validation_filter_h.h"
#include "ekf_derivation/generated/range_validation_filter_P_init.h"
void RangeFinderConsistencyCheck::update(float dist_bottom, float dist_bottom_var, float vz, float vz_var,
bool horizontal_motion, uint64_t time_us)
using namespace matrix;
void RangeFinderConsistencyCheck::init(const float z, const float z_var, const float dist_bottom,
const float dist_bottom_var)
{
if (horizontal_motion) {
_time_last_horizontal_motion = time_us;
}
_P = sym::RangeValidationFilterPInit(z_var, dist_bottom_var);
_Ht = sym::RangeValidationFilterH<float>();
_x(RangeFilter::z.idx) = z;
_x(RangeFilter::terrain.idx) = z - dist_bottom;
_initialized = true;
_test_ratio_lpf.reset(0.f);
_t_since_first_sample = 0.f;
_state = KinematicState::kUnknown;
}
void RangeFinderConsistencyCheck::update(const float z, const float z_var, const float vz, const float vz_var,
const float dist_bottom, const float dist_bottom_var, const uint64_t time_us)
{
const float dt = static_cast<float>(time_us - _time_last_update_us) * 1e-6f;
if ((_time_last_update_us == 0)
|| (dt < 0.001f) || (dt > 0.5f)) {
if (dt > _kTestRatioLpfTimeConstant || _landed) {
_time_last_update_us = time_us;
_dist_bottom_prev = dist_bottom;
_initialized = false;
return;
} else if (!_initialized) {
init(z, z_var, dist_bottom, dist_bottom_var);
return;
}
const float vel_bottom = (dist_bottom - _dist_bottom_prev) / dt;
_innov = -vel_bottom - vz; // vel_bottom is +up while vz is +down
// Variance of the time derivative of a random variable: var(dz/dt) = 2*var(z) / dt^2
const float var = 2.f * dist_bottom_var / (dt * dt);
_innov_var = var + vz_var;
const float normalized_innov_sq = (_innov * _innov) / _innov_var;
_test_ratio = normalized_innov_sq / (_gate * _gate);
_signed_test_ratio_lpf.setParameters(dt, _signed_test_ratio_tau);
const float signed_test_ratio = matrix::sign(_innov) * _test_ratio;
_signed_test_ratio_lpf.update(signed_test_ratio);
updateConsistency(vz, time_us);
// prediction step
_time_last_update_us = time_us;
_dist_bottom_prev = dist_bottom;
_x(RangeFilter::z.idx) -= dt * vz;
_P(RangeFilter::z.idx, RangeFilter::z.idx) += dt * dt * vz_var;
_P(RangeFilter::terrain.idx, RangeFilter::terrain.idx) += _terrain_process_noise;
// iterate through both measurements (z and dist_bottom)
const Vector2f measurements{z, dist_bottom};
for (int measurement_idx = 0; measurement_idx < 2; measurement_idx++) {
// dist_bottom
Vector2f H = _Ht;
float R = dist_bottom_var;
// z, direct state measurement
if (measurement_idx == 0) {
H(RangeFilter::z.idx) = 1.f;
H(RangeFilter::terrain.idx) = 0.f;
R = z_var;
}
// residual
const float measurement_pred = H * _x;
const float y = measurements(measurement_idx) - measurement_pred;
// for H as col-vector:
// innovation variance S = H^T * P * H + R
// kalman gain K = P * H / S
const float S = (H.transpose() * _P * H + R)(0, 0);
Vector2f K = (_P * H / S);
if (measurement_idx == 0) {
K(RangeFilter::z.idx) = 1.f;
} else if (measurement_idx == 1) {
_innov = y;
const float test_ratio = fminf(sq(y) / (sq(_gate) * S), 4.f); // limit to 4 to limit sensitivity to outliers
_test_ratio_lpf.update(test_ratio, dt);
}
// update step
_x(RangeFilter::z.idx) += K(RangeFilter::z.idx) * y;
_x(RangeFilter::terrain.idx) += K(RangeFilter::terrain.idx) * y;
// covariance update with Joseph form:
// P = (I - K H) P (I - K H)^T + K R K^T
Matrix2f I;
I.setIdentity();
Matrix2f IKH = I - K.multiplyByTranspose(H);
_P = IKH * _P * IKH.transpose() + (K * R).multiplyByTranspose(K);
}
evaluateState(dt, vz, vz_var);
}
void RangeFinderConsistencyCheck::updateConsistency(float vz, uint64_t time_us)
void RangeFinderConsistencyCheck::evaluateState(const float dt, const float vz, const float vz_var)
{
if (fabsf(_signed_test_ratio_lpf.getState()) >= 1.f) {
if ((time_us - _time_last_horizontal_motion) > _signed_test_ratio_tau) {
_is_kinematically_consistent = false;
_time_last_inconsistent_us = time_us;
// let the filter settle before starting the consistency check
if (_t_since_first_sample > _kTestRatioLpfTimeConstant) {
if (fabsf(_test_ratio_lpf.getState()) < 1.f) {
const bool vertical_motion = sq(vz) > fmaxf(vz_var, 0.1f);
if (!_horizontal_motion && vertical_motion) {
_state = KinematicState::kConsistent;
} else if (_state == KinematicState::kConsistent || vertical_motion) {
_state = KinematicState::kUnknown;
}
} else {
_t_since_first_sample = 0.f;
_state = KinematicState::kInconsistent;
}
} else {
if ((fabsf(vz) > _min_vz_for_valid_consistency)
&& (_test_ratio < 1.f)
&& ((time_us - _time_last_inconsistent_us) > _consistency_hyst_time_us)
) {
_is_kinematically_consistent = true;
}
_t_since_first_sample += dt;
}
}
void RangeFinderConsistencyCheck::run(const float z, const float z_var, const float vz, const float vz_var,
const float dist_bottom, const float dist_bottom_var, const uint64_t time_us,
const uint8_t current_posD_reset_count
)
{
if (!_initialized || current_posD_reset_count != _last_posD_reset_count) {
_last_posD_reset_count = current_posD_reset_count;
_initialized = false;
}
update(z, z_var, vz, vz_var, dist_bottom, dist_bottom_var, time_us);
}
void RangeFinderConsistencyCheck::setIsLanded(const bool landed)
{
if (landed) {
_state = KinematicState::kUnknown;
}
_landed = landed;
}
void RangeFinderConsistencyCheck::reset()
{
if (_initialized && _state == KinematicState::kConsistent) {
_state = KinematicState::kUnknown;
}
_initialized = false;
}
void RangeFinderConsistencyCheck::setHorizontalMotion(const bool horizontal_motion)
{
if (!horizontal_motion && getHorizontalMotion()) {
reset();
}
_horizontal_motion = horizontal_motion;
}
@@ -48,36 +48,57 @@ public:
RangeFinderConsistencyCheck() = default;
~RangeFinderConsistencyCheck() = default;
void update(float dist_bottom, float dist_bottom_var, float vz, float vz_var, bool horizontal_motion, uint64_t time_us);
enum class KinematicState : int {
kInconsistent = 0,
kConsistent = 1,
kUnknown = 2
};
void setGate(float gate) { _gate = gate; }
float getTestRatioLpf() const { return _initialized ? _test_ratio_lpf.getState() : 0.f; }
float getInnov() const { return _initialized ? _innov : 0.f; }
float getInnovVar() const { return _initialized ? _innov_var : 0.f; }
bool isKinematicallyConsistent() const { return _state == KinematicState::kConsistent; }
bool isNotKinematicallyInconsistent() const { return _state != KinematicState::kInconsistent && (_t_since_first_sample > _kTestRatioLpfTimeConstant || _landed); }
void setGate(const float gate) { _gate = gate; }
void run(float z, float z_var, float vz, float vz_var, float dist_bottom, float dist_bottom_var,
uint64_t time_u, uint8_t current_posD_reset_coun);
void set_terrain_process_noise(float terrain_process_noise) { _terrain_process_noise = terrain_process_noise; }
void reset();
float getTestRatio() const { return _test_ratio; }
float getSignedTestRatioLpf() const { return _signed_test_ratio_lpf.getState(); }
float getInnov() const { return _innov; }
float getInnovVar() const { return _innov_var; }
bool isKinematicallyConsistent() const { return _is_kinematically_consistent; }
void setHorizontalMotion(const bool horizontal_motion);
bool getHorizontalMotion() const { return _horizontal_motion; }
void setIsLanded(bool landed);
private:
void updateConsistency(float vz, uint64_t time_us);
uint64_t _time_last_update_us{};
float _dist_bottom_prev{};
float _test_ratio{};
AlphaFilter<float> _signed_test_ratio_lpf{}; // average signed test ratio used to detect a bias in the data
float _gate{.2f};
float _innov{};
float _innov_var{};
bool _is_kinematically_consistent{true};
uint64_t _time_last_inconsistent_us{};
uint64_t _time_last_horizontal_motion{};
static constexpr float _signed_test_ratio_tau = 2.f;
static constexpr float _min_vz_for_valid_consistency = .5f;
static constexpr uint64_t _consistency_hyst_time_us = 1e6;
void update(float z, float z_var, float vz, float vz_var, float dist_bottom,
float dist_bottom_var, uint64_t time_us);
void init(float z, float z_var, float dist_bottom, float dist_bottom_var);
void evaluateState(float dt, float vz, float vz_var);
float _terrain_process_noise{0.0f};
matrix::SquareMatrix<float, 2> _P{};
matrix::Vector2f _Ht{};
matrix::Vector2f _x{};
bool _initialized{false};
float _innov{0.f};
float _innov_var{0.f};
uint64_t _time_last_update_us{0};
static constexpr float _kTestRatioLpfTimeConstant{1.f};
AlphaFilter<float> _test_ratio_lpf{_kTestRatioLpfTimeConstant};
float _gate{1.0f};
KinematicState _state{KinematicState::kUnknown};
float _t_since_first_sample{0.f};
uint8_t _last_posD_reset_count{0};
bool _horizontal_motion{false};
bool _landed{false};
};
namespace RangeFilter
{
struct IdxDof { unsigned idx; unsigned dof; };
static constexpr IdxDof z{0, 1};
static constexpr IdxDof terrain{1, 1};
static constexpr uint8_t size{2};
}
#endif // !EKF_RANGE_FINDER_CONSISTENCY_CHECK_HPP
@@ -51,45 +51,47 @@ void Ekf::controlRangeHaglFusion(const imuSample &imu_sample)
rng_data_ready = _range_buffer->pop_first_older_than(imu_sample.time_us, _range_sensor.getSampleAddress());
_range_sensor.setDataReadiness(rng_data_ready);
// update range sensor angle parameters in case they have changed
_range_sensor.setPitchOffset(_params.ekf2_rng_pitch);
_range_sensor.setCosMaxTilt(_params.range_cos_max_tilt);
_range_sensor.setQualityHysteresis(_params.ekf2_rng_qlty_t);
_range_sensor.setMaxFogDistance(_params.ekf2_rng_fog);
if (_range_sensor.isDataReady()) {
_range_sensor.runChecks(imu_sample.time_us, _R_to_earth);
_range_sensor.setPitchOffset(_params.ekf2_rng_pitch);
_range_sensor.setCosMaxTilt(_params.range_cos_max_tilt);
_range_sensor.setQualityHysteresis(_params.ekf2_rng_qlty_t);
_range_sensor.setMaxFogDistance(_params.ekf2_rng_fog);
_rng_consistency_check.setGate(_params.ekf2_rng_k_gate);
if (_range_sensor.isDataHealthy()) {
// correct the range data for position offset relative to the IMU
const Vector3f pos_offset_body = _params.rng_pos_body - _params.imu_pos_body;
const Vector3f pos_offset_earth = _R_to_earth * pos_offset_body;
_range_sensor.setRange(_range_sensor.getRange() + pos_offset_earth(2) / _range_sensor.getCosTilt());
_range_sensor.runChecks(imu_sample.time_us, _R_to_earth);
if (_control_status.flags.in_air) {
const bool horizontal_motion = _control_status.flags.fixed_wing
|| (sq(_state.vel(0)) + sq(_state.vel(1)) > fmaxf(P.trace<2>(State::vel.idx), 0.1f));
if (_range_sensor.isDataHealthy()) {
// correct the range data for position offset relative to the IMU
const Vector3f pos_offset_body = _params.rng_pos_body - _params.imu_pos_body;
const Vector3f pos_offset_earth = _R_to_earth * pos_offset_body;
_range_sensor.setRange(_range_sensor.getRange() + pos_offset_earth(2) / _range_sensor.getCosTilt());
const float dist_var = getRngVar();
const bool updated_horizontal_motion = sq(_state.vel(0)) + sq(_state.vel(1)) > fmaxf(P.trace<2>(State::vel.idx), 0.5f);
const float dist_dependant_var = sq(_params.ekf2_rng_sfe * _range_sensor.getDistBottom());
const float var = sq(_params.ekf2_rng_noise) + dist_dependant_var;
if (!updated_horizontal_motion && _rng_consistency_check.getHorizontalMotion()) {
_rng_consistency_check.reset();
}
_rng_consistency_check.setGate(_params.ekf2_rng_k_gate);
_rng_consistency_check.update(_range_sensor.getDistBottom(), math::max(var, 0.001f), _state.vel(2),
P(State::vel.idx + 2, State::vel.idx + 2), horizontal_motion, imu_sample.time_us);
}
} else {
// If we are supposed to be using range finder data but have bad range measurements
// and are on the ground, then synthesise a measurement at the expected on ground value
if (!_control_status.flags.in_air
&& _range_sensor.isRegularlySendingData()
&& _range_sensor.isDataReady()) {
_rng_consistency_check.setIsLanded(!_control_status.flags.in_air);
_rng_consistency_check.setHorizontalMotion(updated_horizontal_motion);
const float z_var = P(State::pos.idx + 2, State::pos.idx + 2);
const float vz_var = P(State::vel.idx + 2, State::vel.idx + 2);
_rng_consistency_check.run(_gpos.altitude(), z_var, _state.vel(2), vz_var, _range_sensor.getDistBottom(),
dist_var, imu_sample.time_us, get_posD_reset_count());
} else if (_range_sensor.isRegularlySendingData() && !_control_status.flags.in_air) {
_range_sensor.setRange(_params.ekf2_min_rng);
_range_sensor.setValidity(true); // bypass the checks
_range_sensor.setValidity(true);
} else {
_rng_consistency_check.reset();
}
}
_control_status.flags.rng_kin_consistent = _rng_consistency_check.isKinematicallyConsistent();
_control_status.flags.rng_kin_unknown = !_rng_consistency_check.isKinematicallyConsistent()
&& _rng_consistency_check.isNotKinematicallyInconsistent();
} else {
return;
@@ -97,7 +99,7 @@ void Ekf::controlRangeHaglFusion(const imuSample &imu_sample)
auto &aid_src = _aid_src_rng_hgt;
if (rng_data_ready && _range_sensor.getSampleAddress()) {
if (_range_sensor.isDataReady() && _range_sensor.getSampleAddress()) {
updateRangeHagl(aid_src);
const bool measurement_valid = PX4_ISFINITE(aid_src.observation) && PX4_ISFINITE(aid_src.observation_variance);
@@ -107,13 +109,12 @@ void Ekf::controlRangeHaglFusion(const imuSample &imu_sample)
&& _control_status.flags.tilt_align
&& measurement_valid
&& _range_sensor.isDataHealthy()
&& _rng_consistency_check.isKinematicallyConsistent();
&& _rng_consistency_check.isNotKinematicallyInconsistent();
const bool starting_conditions_passing = continuing_conditions_passing
&& isNewestSampleRecent(_time_last_range_buffer_push, 2 * estimator::sensor::RNG_MAX_INTERVAL)
&& _range_sensor.isRegularlySendingData();
const bool do_conditional_range_aid = (_control_status.flags.rng_terrain || _control_status.flags.rng_hgt)
&& (_params.ekf2_rng_ctrl == static_cast<int32_t>(RngCtrl::CONDITIONAL))
&& isConditionalRangeAidSuitable();
@@ -137,7 +138,8 @@ void Ekf::controlRangeHaglFusion(const imuSample &imu_sample)
_control_status.flags.rng_hgt = true;
stopRngTerrFusion();
if (!_control_status.flags.opt_flow_terrain && aid_src.innovation_rejected) {
if (!_control_status.flags.opt_flow_terrain && aid_src.innovation_rejected
&& _rng_consistency_check.isKinematicallyConsistent()) {
resetTerrainToRng(aid_src);
resetAidSourceStatusZeroInnovation(aid_src);
}
@@ -163,7 +165,8 @@ void Ekf::controlRangeHaglFusion(const imuSample &imu_sample)
ECL_INFO("starting %s height fusion", HGT_SRC_NAME);
_control_status.flags.rng_hgt = true;
if (!_control_status.flags.opt_flow_terrain && aid_src.innovation_rejected) {
if (!_control_status.flags.opt_flow_terrain && aid_src.innovation_rejected
&& _rng_consistency_check.isKinematicallyConsistent()) {
resetTerrainToRng(aid_src);
resetAidSourceStatusZeroInnovation(aid_src);
}
@@ -200,7 +203,7 @@ void Ekf::controlRangeHaglFusion(const imuSample &imu_sample)
stopRngHgtFusion();
stopRngTerrFusion();
} else {
} else if (_rng_consistency_check.isKinematicallyConsistent()) {
resetTerrainToRng(aid_src);
resetAidSourceStatusZeroInnovation(aid_src);
}
@@ -215,13 +218,13 @@ void Ekf::controlRangeHaglFusion(const imuSample &imu_sample)
} else {
if (starting_conditions_passing) {
if (_control_status.flags.opt_flow_terrain) {
if (!aid_src.innovation_rejected) {
if (!aid_src.innovation_rejected && _rng_consistency_check.isNotKinematicallyInconsistent()) {
_control_status.flags.rng_terrain = true;
fuseHaglRng(aid_src, _control_status.flags.rng_hgt, _control_status.flags.rng_terrain);
}
} else {
if (aid_src.innovation_rejected) {
if (aid_src.innovation_rejected && _rng_consistency_check.isKinematicallyConsistent()) {
resetTerrainToRng(aid_src);
resetAidSourceStatusZeroInnovation(aid_src);
}
@@ -268,11 +271,9 @@ void Ekf::updateRangeHagl(estimator_aid_source1d_s &aid_src)
float Ekf::getRngVar() const
{
return fmaxf(
P(State::pos.idx + 2, State::pos.idx + 2)
+ sq(_params.ekf2_rng_noise)
+ sq(_params.ekf2_rng_sfe * _range_sensor.getRange()),
0.f);
const float dist_dependant_var = sq(_params.ekf2_rng_sfe * _range_sensor.getDistBottom());
const float dist_var = sq(_params.ekf2_rng_noise) + dist_dependant_var;
return dist_var;
}
void Ekf::resetTerrainToRng(estimator_aid_source1d_s &aid_src)
@@ -164,6 +164,9 @@ void SensorRangeFinder::updateFogCheck(const float dist_bottom, const uint64_t t
}
_prev_median_dist = median_dist;
} else {
_prev_median_dist = dist_bottom;
}
}
+3 -1
View File
@@ -425,7 +425,7 @@ struct parameters {
float ekf2_rng_a_vmax{1.0f}; ///< maximum ground velocity for which we allow to use the range finder as height source (if rng_control == 1)
float ekf2_rng_qlty_t{1.0f}; ///< minimum duration during which the reported range finder signal quality needs to be non-zero in order to be declared valid (s)
float range_cos_max_tilt{0.7071f}; ///< cosine of the maximum tilt angle from the vertical that permits use of range finder and flow data
float ekf2_rng_k_gate{1.0f}; ///< gate size used by the range finder kinematic consistency check
float ekf2_rng_k_gate{0.5f}; ///< gate size used by the range finder kinematic consistency check
float ekf2_rng_fog{0.f}; ///< max distance which a blocked range sensor measures (fog, dirt) [m]
Vector3f rng_pos_body{}; ///< xyz position of range sensor in body frame (m)
@@ -608,6 +608,8 @@ uint64_t mag_heading_consistent :
uint64_t gnss_vel : 1; ///< 44 - true if GNSS velocity measurement fusion is intended
uint64_t gnss_fault :
1; ///< 45 - true if GNSS measurements have been declared faulty and are no longer used
uint64_t rng_kin_unknown : 1; ///< 46 - true when the range finder kinematic consistency check is not running
} flags;
uint64_t value;
};
+4
View File
@@ -227,6 +227,10 @@ void Ekf::predictCovariance(const imuSample &imu_delayed)
terrain_process_noise += sq(imu_delayed.delta_vel_dt * _params.ekf2_terr_grad) * (sq(_state.vel(0)) + sq(_state.vel(
1)));
P(State::terrain.idx, State::terrain.idx) += terrain_process_noise;
#if defined(CONFIG_EKF2_RANGE_FINDER)
_rng_consistency_check.set_terrain_process_noise(terrain_process_noise);
#endif // CONFIG_EKF2_RANGE_FINDER
}
#endif // CONFIG_EKF2_TERRAIN
+1 -1
View File
@@ -117,7 +117,7 @@ public:
float getHaglRateInnov() const { return _rng_consistency_check.getInnov(); }
float getHaglRateInnovVar() const { return _rng_consistency_check.getInnovVar(); }
float getHaglRateInnovRatio() const { return _rng_consistency_check.getSignedTestRatioLpf(); }
float getHaglRateInnovRatio() const { return _rng_consistency_check.getTestRatioLpf(); }
#endif // CONFIG_EKF2_RANGE_FINDER
#if defined(CONFIG_EKF2_OPTICAL_FLOW)
@@ -721,6 +721,37 @@ def compute_gravity_z_innov_var_and_h(
return (innov_var, H.T)
def range_validation_filter_h() -> sf.Matrix:
state = Values(
z=sf.Symbol("z"),
terrain=sf.Symbol("terrain")
)
dist_bottom = state["z"] - state["terrain"]
H = sf.M21()
H[:, 0] = sf.V1(dist_bottom).jacobian(state.to_storage(), tangent_space=False).transpose()
return H
def range_validation_filter_P_init(
z_var: sf.Scalar,
dist_bottom_var: sf.Scalar
) -> sf.Matrix:
H = range_validation_filter_h().T
# enforce terrain to match the measurement
K = sf.V2(0, 1/H[1])
R = sf.V1(dist_bottom_var)
# dist_bottom = z - terrain
P = sf.M22.diag([z_var, z_var + dist_bottom_var])
I = sf.M22.eye()
# Joseph form
P = (I - K * H) * P * (I - K * H).T + K * R * K.T
return P
print("Derive EKF2 equations...")
generate_px4_function(predict_covariance, output_names=None)
@@ -752,5 +783,7 @@ generate_px4_function(compute_gravity_z_innov_var_and_h, output_names=["innov_va
generate_px4_function(compute_body_vel_innov_var_h, output_names=["innov_var", "Hx", "Hy", "Hz"])
generate_px4_function(compute_body_vel_y_innov_var, output_names=["innov_var"])
generate_px4_function(compute_body_vel_z_innov_var, output_names=["innov_var"])
generate_px4_function(range_validation_filter_h, output_names=None)
generate_px4_function(range_validation_filter_P_init, output_names=None)
generate_px4_state(State, tangent_idx)
@@ -0,0 +1,46 @@
// -----------------------------------------------------------------------------
// 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: range_validation_filter_p_init
*
* Args:
* z_var: Scalar
* dist_bottom_var: Scalar
*
* Outputs:
* res: Matrix22
*/
template <typename Scalar>
matrix::Matrix<Scalar, 2, 2> RangeValidationFilterPInit(const Scalar z_var,
const Scalar dist_bottom_var) {
// Total ops: 1
// Input arrays
// Intermediate terms (0)
// Output terms (1)
matrix::Matrix<Scalar, 2, 2> _res;
_res(0, 0) = z_var;
_res(1, 0) = z_var;
_res(0, 1) = z_var;
_res(1, 1) = dist_bottom_var + z_var;
return _res;
} // NOLINT(readability/fn_size)
// NOLINTNEXTLINE(readability/fn_size)
} // namespace sym
@@ -0,0 +1,41 @@
// -----------------------------------------------------------------------------
// 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: range_validation_filter_h
*
* Args:
*
* Outputs:
* res: Matrix21
*/
template <typename Scalar>
matrix::Matrix<Scalar, 2, 1> RangeValidationFilterH() {
// Total ops: 0
// Input arrays
// Intermediate terms (0)
// Output terms (1)
matrix::Matrix<Scalar, 2, 1> _res;
_res(0, 0) = 1;
_res(1, 0) = -1;
return _res;
} // NOLINT(readability/fn_size)
// NOLINTNEXTLINE(readability/fn_size)
} // namespace sym
+1
View File
@@ -1935,6 +1935,7 @@ void EKF2::PublishStatusFlags(const hrt_abstime &timestamp)
status_flags.cs_constant_pos = _ekf.control_status_flags().constant_pos;
status_flags.cs_baro_fault = _ekf.control_status_flags().baro_fault;
status_flags.cs_gnss_vel = _ekf.control_status_flags().gnss_vel;
status_flags.cs_rng_kin_unknown = _ekf.control_status_flags().rng_kin_unknown;
status_flags.fault_status_changes = _filter_fault_status_changes;
status_flags.fs_bad_mag_x = _ekf.fault_status_flags().bad_mag_x;
+1 -1
View File
@@ -103,7 +103,7 @@ parameters:
...). Note: tune the range finder noise parameters (EKF2_RNG_NOISE and EKF2_RNG_SFE)
before tuning this gate.'
type: float
default: 1.0
default: 0.5
unit: SD
min: 0.1
max: 5.0
@@ -162,7 +162,7 @@ TEST_F(EkfHeightFusionTest, gpsRef)
_ekf_wrapper.enableBaroHeightFusion();
_ekf_wrapper.enableGpsHeightFusion();
_ekf_wrapper.enableRangeHeightFusion();
_sensor_simulator.runSeconds(1);
_sensor_simulator.runSeconds(2);
EXPECT_TRUE(_ekf->getHeightSensorRef() == HeightSensor::GNSS);
EXPECT_TRUE(_ekf_wrapper.isIntendingBaroHeightFusion());
@@ -215,7 +215,7 @@ TEST_F(EkfHeightFusionTest, gpsRefNoAltFusion)
_ekf_wrapper.enableBaroHeightFusion();
_ekf_wrapper.disableGpsHeightFusion();
_ekf_wrapper.enableRangeHeightFusion();
_sensor_simulator.runSeconds(1);
_sensor_simulator.runSeconds(2);
EXPECT_TRUE(_ekf->getHeightSensorRef() == HeightSensor::BARO); // Fallback to baro as GNSS alt is disabled
EXPECT_TRUE(_ekf_wrapper.isIntendingBaroHeightFusion());
@@ -251,7 +251,7 @@ TEST_F(EkfHeightFusionTest, baroRefFailOver)
EXPECT_TRUE(_ekf->getHeightSensorRef() == HeightSensor::GNSS);
_sensor_simulator.stopGps();
_sensor_simulator.runSeconds(10);
_sensor_simulator.runSeconds(12);
EXPECT_TRUE(_ekf->getHeightSensorRef() == HeightSensor::RANGE);
_sensor_simulator.stopRangeFinder();
+32 -7
View File
@@ -219,11 +219,36 @@ TEST_F(EkfTerrainTest, testRngStartInAir)
{
// GIVEN: rng for terrain but not flow
_ekf_wrapper.disableFlowFusion();
_ekf_wrapper.enableRangeHeightFusion();
const float rng_height = 16.f;
const float rng_height = 18;
const float flow_height = 1.f;
runFlowAndRngScenario(rng_height, flow_height);
_sensor_simulator.startGps();
_ekf->set_min_required_gps_health_time(1e6);
_ekf->set_in_air_status(false);
_ekf->set_vehicle_at_rest(true);
_ekf_wrapper.enableGpsFusion();
_sensor_simulator.runSeconds(1.5); // Run to pass the GPS checks
EXPECT_TRUE(_ekf_wrapper.isIntendingGpsFusion());
const Vector3f simulated_velocity(0.5f, -1.0f, 0.0f);
// Configure GPS simulator data
_sensor_simulator._gps.setVelocity(simulated_velocity);
_sensor_simulator._gps.setPositionRateNED(simulated_velocity);
// Simulate flight above max range finder distance
// run for a while to let terrain uncertainty increase
_sensor_simulator._rng.setData(30.f, 100);
_sensor_simulator._rng.setLimits(0.1f, 20.f);
_sensor_simulator.startRangeFinder();
_ekf->set_in_air_status(true);
_ekf->set_vehicle_at_rest(false);
_sensor_simulator.runSeconds(40);
// quick range finder change to bypass stuck-check
_sensor_simulator._rng.setData(rng_height - 1.f, 100);
_sensor_simulator.runSeconds(1);
_sensor_simulator._rng.setData(rng_height, 100);
_sensor_simulator.runSeconds(10);
// THEN: the terrain should reset using rng
EXPECT_NEAR(rng_height, _ekf->getHagl(), 2e-2f);
@@ -234,13 +259,13 @@ TEST_F(EkfTerrainTest, testRngStartInAir)
const float corr_terrain_vz = P(State::vel.idx + 2,
State::terrain.idx) / sqrtf(_ekf->getVelocityVariance()(2) * var_terrain);
EXPECT_NEAR(corr_terrain_vz, 0.6f, 0.03f);
EXPECT_TRUE(fabsf(corr_terrain_vz) > 0.6f && fabsf(corr_terrain_vz) < 1.f);
const float corr_terrain_z = P(State::pos.idx + 2,
State::terrain.idx) / sqrtf(_ekf->getPositionVariance()(2) * var_terrain);
EXPECT_NEAR(corr_terrain_z, 0.8f, 0.03f);
EXPECT_TRUE(fabsf(corr_terrain_z) > 0.8f && fabsf(corr_terrain_z) < 1.f);
const float corr_terrain_abias_z = P(State::accel_bias.idx + 2,
State::terrain.idx) / sqrtf(_ekf->getAccelBiasVariance()(2) * var_terrain);
EXPECT_NEAR(corr_terrain_abias_z, -0.3f, 0.03f);
EXPECT_TRUE(corr_terrain_abias_z < -0.1f);
}