/**************************************************************************** * * Copyright (c) 2015 Estimation and Control Library (ECL). 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 ECL 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 heading_fusion.cpp * Magnetometer fusion methods. * Equations generated using EKF/python/ekf_derivation/main.py * * @author Roman Bast * @author Paul Riseborough * */ #include "ekf.h" #include "python/ekf_derivation/generated/compute_mag_innov_innov_var_and_hx.h" #include "python/ekf_derivation/generated/compute_mag_y_innov_var_and_h.h" #include "python/ekf_derivation/generated/compute_mag_z_innov_var_and_h.h" #include "python/ekf_derivation/generated/compute_yaw_321_innov_var_and_h.h" #include "python/ekf_derivation/generated/compute_yaw_312_innov_var_and_h.h" #include "python/ekf_derivation/generated/compute_mag_declination_innov_innov_var_and_h.h" #include bool Ekf::fuseMag(const Vector3f &mag, estimator_aid_source3d_s &aid_src_mag, bool update_all_states) { // XYZ Measurement uncertainty. Need to consider timing errors for fast rotations const float R_MAG = sq(fmaxf(_params.mag_noise, 0.0f)); // calculate intermediate variables used for X axis innovation variance, observation Jacobians and Kalman gains const char* numerical_error_covariance_reset_string = "numerical error - covariance reset"; Vector3f mag_innov; Vector3f innov_var; // Observation jacobian and Kalman gain vectors SparseVector24f<0,1,2,3,16,17,18,19,20,21> Hfusion; Vector24f H; const Vector24f state_vector = getStateAtFusionHorizonAsVector(); sym::ComputeMagInnovInnovVarAndHx(state_vector, P, mag, R_MAG, FLT_EPSILON, &mag_innov, &innov_var, &H); Hfusion = H; innov_var.copyTo(aid_src_mag.innovation_variance); if (aid_src_mag.innovation_variance[0] < R_MAG) { // the innovation variance contribution from the state covariances is negative which means the covariance matrix is badly conditioned _fault_status.flags.bad_mag_x = true; // we need to re-initialise covariances and abort this fusion step resetMagRelatedCovariances(); ECL_ERR("magX %s", numerical_error_covariance_reset_string); return false; } _fault_status.flags.bad_mag_x = false; // check innovation variances for being badly conditioned if (aid_src_mag.innovation_variance[1] < R_MAG) { // the innovation variance contribution from the state covariances is negtive which means the covariance matrix is badly conditioned _fault_status.flags.bad_mag_y = true; // we need to re-initialise covariances and abort this fusion step resetMagRelatedCovariances(); ECL_ERR("magY %s", numerical_error_covariance_reset_string); return false; } _fault_status.flags.bad_mag_y = false; if (aid_src_mag.innovation_variance[2] < R_MAG) { // the innovation variance contribution from the state covariances is negative which means the covariance matrix is badly conditioned _fault_status.flags.bad_mag_z = true; // we need to re-initialise covariances and abort this fusion step resetMagRelatedCovariances(); ECL_ERR("magZ %s", numerical_error_covariance_reset_string); return false; } _fault_status.flags.bad_mag_z = false; // do not use the synthesized measurement for the magnetomter Z component for 3D fusion if (_control_status.flags.synthetic_mag_z) { mag_innov(2) = 0.0f; } for (int i = 0; i < 3; i++) { aid_src_mag.observation[i] = mag(i) - _state.mag_B(i); aid_src_mag.observation_variance[i] = R_MAG; aid_src_mag.innovation[i] = mag_innov(i); } aid_src_mag.fusion_enabled = _control_status.flags.mag_3D && update_all_states; // do not use the synthesized measurement for the magnetomter Z component for 3D fusion if (_control_status.flags.synthetic_mag_z) { aid_src_mag.innovation[2] = 0.0f; } const float innov_gate = math::max(_params.mag_innov_gate, 1.f); setEstimatorAidStatusTestRatio(aid_src_mag, innov_gate); // Perform an innovation consistency check and report the result _innov_check_fail_status.flags.reject_mag_x = (aid_src_mag.test_ratio[0] > 1.f); _innov_check_fail_status.flags.reject_mag_y = (aid_src_mag.test_ratio[1] > 1.f); _innov_check_fail_status.flags.reject_mag_z = (aid_src_mag.test_ratio[2] > 1.f); // if any axis fails, abort the mag fusion if (aid_src_mag.innovation_rejected) { return false; } bool fused[3] {false, false, false}; // update the states and covariance using sequential fusion of the magnetometer components for (uint8_t index = 0; index <= 2; index++) { // Calculate Kalman gains and observation jacobians if (index == 0) { // everything was already computed above } else if (index == 1) { // recalculate innovation variance because state covariances have changed due to previous fusion (linearise using the same initial state for all axes) sym::ComputeMagYInnovVarAndH(state_vector, P, R_MAG, FLT_EPSILON, &aid_src_mag.innovation_variance[index], &H); Hfusion = H; // recalculate innovation using the updated state aid_src_mag.innovation[index] = _state.quat_nominal.rotateVectorInverse(_state.mag_I)(index) + _state.mag_B(index) - mag(index); if (aid_src_mag.innovation_variance[index] < R_MAG) { // the innovation variance contribution from the state covariances is negative which means the covariance matrix is badly conditioned _fault_status.flags.bad_mag_y = true; // we need to re-initialise covariances and abort this fusion step resetMagRelatedCovariances(); ECL_ERR("magY %s", numerical_error_covariance_reset_string); return false; } } else if (index == 2) { // we do not fuse synthesized magnetomter measurements when doing 3D fusion if (_control_status.flags.synthetic_mag_z) { continue; } // recalculate innovation variance because state covariances have changed due to previous fusion (linearise using the same initial state for all axes) sym::ComputeMagZInnovVarAndH(state_vector, P, R_MAG, FLT_EPSILON, &aid_src_mag.innovation_variance[index], &H); Hfusion = H; // recalculate innovation using the updated state aid_src_mag.innovation[index] = _state.quat_nominal.rotateVectorInverse(_state.mag_I)(index) + _state.mag_B(index) - mag(index); if (aid_src_mag.innovation_variance[index] < R_MAG) { // the innovation variance contribution from the state covariances is negative which means the covariance matrix is badly conditioned _fault_status.flags.bad_mag_z = true; // we need to re-initialise covariances and abort this fusion step resetMagRelatedCovariances(); ECL_ERR("magZ %s", numerical_error_covariance_reset_string); return false; } } Vector24f Kfusion = P * Hfusion / aid_src_mag.innovation_variance[index]; if (!update_all_states) { for (unsigned row = 0; row <= 15; row++) { Kfusion(row) = 0.f; } for (unsigned row = 22; row <= 23; row++) { Kfusion(row) = 0.f; } } if (measurementUpdate(Kfusion, Hfusion, aid_src_mag.innovation[index])) { fused[index] = true; limitDeclination(); } else { fused[index] = false; } } _fault_status.flags.bad_mag_x = !fused[0]; _fault_status.flags.bad_mag_y = !fused[1]; _fault_status.flags.bad_mag_z = !fused[2]; if (fused[0] && fused[1] && fused[2]) { aid_src_mag.fused = true; aid_src_mag.time_last_fuse = _imu_sample_delayed.time_us; return true; } aid_src_mag.fused = false; return false; } // update quaternion states and covariances using the yaw innovation and yaw observation variance bool Ekf::fuseYaw(const float innovation, const float variance, estimator_aid_source1d_s& aid_src_status) { aid_src_status.innovation = innovation; Vector24f H_YAW; if (shouldUse321RotationSequence(_R_to_earth)) { sym::ComputeYaw321InnovVarAndH(getStateAtFusionHorizonAsVector(), P, variance, FLT_EPSILON, &aid_src_status.innovation_variance, &H_YAW); } else { sym::ComputeYaw312InnovVarAndH(getStateAtFusionHorizonAsVector(), P, variance, FLT_EPSILON, &aid_src_status.innovation_variance, &H_YAW); } float heading_innov_var_inv = 0.f; // check if the innovation variance calculation is badly conditioned if (aid_src_status.innovation_variance >= variance) { // the innovation variance contribution from the state covariances is not negative, no fault _fault_status.flags.bad_hdg = false; heading_innov_var_inv = 1.f / aid_src_status.innovation_variance; } else { // the innovation variance contribution from the state covariances is negative which means the covariance matrix is badly conditioned _fault_status.flags.bad_hdg = true; // we reinitialise the covariance matrix and abort this fusion step initialiseCovariance(); ECL_ERR("yaw fusion numerical error - covariance reset"); return false; } // calculate the Kalman gains // only calculate gains for states we are using Vector24f Kfusion; for (uint8_t row = 0; row <= 15; row++) { for (uint8_t col = 0; col <= 3; col++) { Kfusion(row) += P(row, col) * H_YAW(col); } Kfusion(row) *= heading_innov_var_inv; } if (_control_status.flags.wind) { for (uint8_t row = 22; row <= 23; row++) { for (uint8_t col = 0; col <= 3; col++) { Kfusion(row) += P(row, col) * H_YAW(col); } Kfusion(row) *= heading_innov_var_inv; } } // define the innovation gate size float gate_sigma = math::max(_params.heading_innov_gate, 1.f); // innovation test ratio setEstimatorAidStatusTestRatio(aid_src_status, gate_sigma); // set the magnetometer unhealthy if the test fails if (aid_src_status.innovation_rejected) { _innov_check_fail_status.flags.reject_yaw = true; // if we are in air we don't want to fuse the measurement // we allow to use it when on the ground because the large innovation could be caused // by interference or a large initial gyro bias if (!_control_status.flags.in_air && isTimedOut(_time_last_in_air, (uint64_t)5e6) && isTimedOut(aid_src_status.time_last_fuse, (uint64_t)1e6) ) { // constrain the innovation to the maximum set by the gate // we need to delay this forced fusion to avoid starting it // immediately after touchdown, when the drone is still armed float gate_limit = sqrtf((sq(gate_sigma) * aid_src_status.innovation_variance)); aid_src_status.innovation = math::constrain(aid_src_status.innovation, -gate_limit, gate_limit); // also reset the yaw gyro variance to converge faster and avoid // being stuck on a previous bad estimate resetZDeltaAngBiasCov(); } else { return false; } } else { _innov_check_fail_status.flags.reject_yaw = false; } SparseVector24f<0,1,2,3> Hfusion(H_YAW); if (measurementUpdate(Kfusion, Hfusion, aid_src_status.innovation)) { _time_last_heading_fuse = _imu_sample_delayed.time_us; aid_src_status.time_last_fuse = _imu_sample_delayed.time_us; aid_src_status.fused = true; _fault_status.flags.bad_hdg = false; return true; } // otherwise aid_src_status.fused = false; _fault_status.flags.bad_hdg = true; return false; } bool Ekf::fuseDeclination(float decl_sigma) { // observation variance (rad**2) const float R_DECL = sq(decl_sigma); Vector24f H; float innovation; float innovation_variance; sym::ComputeMagDeclinationInnovInnovVarAndH(getStateAtFusionHorizonAsVector(), P, getMagDeclination(), R_DECL, FLT_EPSILON, &innovation, &innovation_variance, &H); if (innovation_variance < R_DECL) { // variance calculation is badly conditioned return false; } SparseVector24f<16,17> Hfusion(H); // Calculate the Kalman gains Vector24f Kfusion = P * Hfusion / innovation_variance; const bool is_fused = measurementUpdate(Kfusion, Hfusion, innovation); _fault_status.flags.bad_mag_decl = !is_fused; if (is_fused) { limitDeclination(); } return is_fused; } void Ekf::limitDeclination() { // get a reference value for the earth field declinaton and minimum plausible horizontal field strength // set to 50% of the horizontal strength from geo tables if location is known float decl_reference; float h_field_min = 0.001f; if (_params.mag_declination_source & GeoDeclinationMask::USE_GEO_DECL) { // use parameter value until GPS is available, then use value returned by geo library if (_NED_origin_initialised || PX4_ISFINITE(_mag_declination_gps)) { decl_reference = _mag_declination_gps; h_field_min = fmaxf(h_field_min, 0.5f * _mag_strength_gps * cosf(_mag_inclination_gps)); } else { decl_reference = math::radians(_params.mag_declination_deg); } } else { // always use the parameter value decl_reference = math::radians(_params.mag_declination_deg); } // do not allow the horizontal field length to collapse - this will make the declination fusion badly conditioned // and can result in a reversal of the NE field states which the filter cannot recover from // apply a circular limit float h_field = sqrtf(_state.mag_I(0) * _state.mag_I(0) + _state.mag_I(1) * _state.mag_I(1)); if (h_field < h_field_min) { if (h_field > 0.001f * h_field_min) { const float h_scaler = h_field_min / h_field; _state.mag_I(0) *= h_scaler; _state.mag_I(1) *= h_scaler; } else { // too small to scale radially so set to expected value const float mag_declination = getMagDeclination(); _state.mag_I(0) = 2.0f * h_field_min * cosf(mag_declination); _state.mag_I(1) = 2.0f * h_field_min * sinf(mag_declination); } h_field = h_field_min; } // do not allow the declination estimate to vary too much relative to the reference value constexpr float decl_tolerance = 0.5f; const float decl_max = decl_reference + decl_tolerance; const float decl_min = decl_reference - decl_tolerance; const float decl_estimate = atan2f(_state.mag_I(1), _state.mag_I(0)); if (decl_estimate > decl_max) { _state.mag_I(0) = h_field * cosf(decl_max); _state.mag_I(1) = h_field * sinf(decl_max); } else if (decl_estimate < decl_min) { _state.mag_I(0) = h_field * cosf(decl_min); _state.mag_I(1) = h_field * sinf(decl_min); } } float Ekf::calculate_synthetic_mag_z_measurement(const Vector3f &mag_meas, const Vector3f &mag_earth_predicted) { // theoretical magnitude of the magnetometer Z component value given X and Y sensor measurement and our knowledge // of the earth magnetic field vector at the current location const float mag_z_abs = sqrtf(math::max(sq(mag_earth_predicted.length()) - sq(mag_meas(0)) - sq(mag_meas(1)), 0.0f)); // calculate sign of synthetic magnetomter Z component based on the sign of the predicted magnetomer Z component const float mag_z_body_pred = mag_earth_predicted.dot(_R_to_earth.col(2)); return (mag_z_body_pred < 0) ? -mag_z_abs : mag_z_abs; }