James Kuszmaul | a60aee1 | 2024-02-02 22:57:47 -0800 | [diff] [blame] | 1 | #include "frc971/imu/imu_calibrator.h" |
| 2 | #include "frc971/math/interpolate.h" |
| 3 | |
| 4 | DECLARE_int32(imu_zeroing_buffer); |
| 5 | |
| 6 | namespace frc971::imu { |
| 7 | |
| 8 | inline constexpr double kGravityGs = 1.0; |
| 9 | // rad / sec |
| 10 | inline constexpr double kGyroMaxZeroingValue = 0.1; |
| 11 | |
| 12 | template <typename Scalar> |
| 13 | void ImuCalibrator<Scalar>::InsertImu(size_t imu_index, |
| 14 | const RawImuReading &reading) { |
| 15 | CHECK_LT(imu_index, imu_readings_.size()); |
| 16 | std::vector<ImuReading> &readings = imu_readings_[imu_index]; |
| 17 | if (readings.size() > 0u) { |
| 18 | CHECK_LT(readings.back().capture_time_raw, reading.capture_time) |
| 19 | << ": Readings must be inserted in time order per IMU."; |
| 20 | } |
| 21 | // Execute the stationary logic. We identify if this reading is plausibly |
| 22 | // stationary, then if it is not stationary, we go back in time to any |
| 23 | // potentially relevant readings and mark them as not stationary. Finally, we |
| 24 | // go through and as values exit the FLAGS_imu_zeroing_buffer window we do any |
| 25 | // processing that we can given that we know it must be stationary. |
| 26 | const bool plausibly_stationary = |
| 27 | reading.gyro.squaredNorm() < kGyroMaxZeroingValue * kGyroMaxZeroingValue; |
| 28 | bool stationary = plausibly_stationary; |
| 29 | int earliest_affected_index = readings.size() - FLAGS_imu_zeroing_buffer; |
| 30 | for (size_t index = std::max(0, earliest_affected_index); |
| 31 | index < readings.size(); ++index) { |
| 32 | if (!plausibly_stationary) { |
| 33 | readings[index].stationary = false; |
| 34 | } |
| 35 | if (!readings[index].plausibly_stationary) { |
| 36 | stationary = false; |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | // Since we don't have data from before the start, assume that we may have |
| 41 | // been moving. |
| 42 | if (earliest_affected_index < 0) { |
| 43 | stationary = false; |
| 44 | } |
| 45 | |
| 46 | if (earliest_affected_index >= 0) { |
| 47 | ImuReading &earliest_reading = readings[earliest_affected_index]; |
| 48 | // The stationary flag for this reading can no longer change, so we can |
| 49 | // start to do things based on it. |
| 50 | earliest_reading.stationary_is_locked = true; |
| 51 | if (earliest_reading.stationary) { |
| 52 | earliest_reading.parameter_residuals.gravity = |
| 53 | earliest_reading.accel.norm() - kGravityGs; |
| 54 | earliest_reading.parameter_residuals.gyro_zero = earliest_reading.gyro; |
| 55 | LOG(INFO) << earliest_reading.gyro.transpose(); |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | const ImuConfig<Scalar> &config = imu_configs_[imu_index]; |
| 60 | Scalar capture_time_adjusted = |
| 61 | static_cast<Scalar>(aos::time::DurationInSeconds( |
| 62 | reading.capture_time.time_since_epoch())) - |
| 63 | (config.parameters.has_value() ? config.parameters->time_offset |
| 64 | : static_cast<Scalar>(0.0)); |
| 65 | |
| 66 | imu_readings_[imu_index].emplace_back( |
| 67 | reading.capture_time, capture_time_adjusted, |
| 68 | reading.gyro - config.dynamic_params.gyro_zero, |
| 69 | reading.accel / config.dynamic_params.gravity, |
| 70 | DynamicImuParameters<Scalar>{static_cast<Scalar>(0.0), |
| 71 | Eigen::Matrix<Scalar, 3, 1>::Zero()}, |
| 72 | plausibly_stationary, stationary, false, std::nullopt, std::nullopt); |
| 73 | } |
| 74 | |
| 75 | template <typename Scalar> |
| 76 | void ImuCalibrator<Scalar>::EvaluateRelativeResiduals() { |
| 77 | for (const auto &readings : imu_readings_) { |
| 78 | CHECK_LT(static_cast<size_t>(FLAGS_imu_zeroing_buffer * 2), readings.size()) |
| 79 | << ": Insufficient readings to perform calibration."; |
| 80 | } |
| 81 | Scalar base_clock = imu_readings_[origin_index_][0].capture_time_adjusted; |
| 82 | // Current index corresponding to the base_clock time. |
| 83 | std::vector<size_t> reading_indices(imu_configs_.size(), 0); |
| 84 | // The for loops are set up so that we: |
| 85 | // 1. Iterate over every pair of readings from the origin/base IMU. |
| 86 | // 2. For each other IMU, we identify 0 or 1 readings that fall between those |
| 87 | // two readings of the origin IMU. We then calculate the residuals for |
| 88 | // that IMU relative to the origin IMU, linearly interpolating between |
| 89 | // the pair of readings from (1) (by doing a linear interpolation, we can |
| 90 | // get sub-cycle accuracy on time offsets). |
| 91 | for (; |
| 92 | reading_indices[origin_index_] < imu_readings_[origin_index_].size() - 1; |
| 93 | ++reading_indices[origin_index_]) { |
| 94 | const ImuReading &base_reading = |
| 95 | imu_readings_[origin_index_][reading_indices[origin_index_]]; |
| 96 | const ImuReading &next_base_reading = |
| 97 | imu_readings_[origin_index_][reading_indices[origin_index_] + 1]; |
| 98 | base_clock = base_reading.capture_time_adjusted; |
| 99 | const Scalar next_base_clock = next_base_reading.capture_time_adjusted; |
| 100 | for (size_t imu_index = 0; imu_index < imu_configs_.size(); ++imu_index) { |
| 101 | const ImuConfig<Scalar> &imu_config = imu_configs_[imu_index]; |
| 102 | // We don't care about calculating the offsets from the origin IMU to |
| 103 | // itself... |
| 104 | if (imu_config.is_origin) { |
| 105 | continue; |
| 106 | } |
| 107 | auto &readings = imu_readings_[imu_index]; |
| 108 | bool done_with_imu = false; |
| 109 | // This will put the index for the current IMU just past the base_clock |
| 110 | // timepoint, allowing us to interpolate between |
| 111 | // reading_indices[origin_index_] and reading_indices[origin_index_] + 1. |
| 112 | while (readings[reading_indices[imu_index]].capture_time_adjusted < |
| 113 | base_clock) { |
| 114 | if (reading_indices[imu_index] == imu_readings_[imu_index].size() - 1) { |
| 115 | done_with_imu = true; |
| 116 | break; |
| 117 | } |
| 118 | ++reading_indices[imu_index]; |
| 119 | } |
| 120 | // If we've run out of readings on this imu, stop doing anything. |
| 121 | if (done_with_imu) { |
| 122 | continue; |
| 123 | } |
| 124 | ImuReading &reading = readings[reading_indices[imu_index]]; |
| 125 | const Scalar reading_time = reading.capture_time_adjusted; |
| 126 | if (reading_time >= next_base_clock) { |
| 127 | // There is a gap in readings for this imu; we can't meaningfully |
| 128 | // populate the residuals. |
| 129 | continue; |
| 130 | } |
| 131 | // Sanity check the above logic. |
| 132 | CHECK_LE(base_clock, reading_time); |
| 133 | CHECK_LT(reading_time, next_base_clock); |
| 134 | CHECK(imu_config.parameters.has_value()); |
| 135 | reading.gyro_residual = |
| 136 | imu_config.parameters.value().rotation * reading.gyro - |
| 137 | frc971::math::Interpolate<Eigen::Matrix<Scalar, 3, 1>, Scalar>( |
| 138 | base_clock, next_base_clock, base_reading.gyro, |
| 139 | next_base_reading.gyro, reading_time); |
| 140 | if (!reading.stationary_is_locked || !reading.stationary) { |
| 141 | continue; |
| 142 | } |
| 143 | // In order to calculate the accelerometer residual, we are assuming that |
| 144 | // the two IMUs "should" produce identical accelerations. This is only |
| 145 | // true when not rotating. Future changes may account for coriolis |
| 146 | // effects. |
| 147 | reading.accel_residual = |
| 148 | imu_config.parameters.value().rotation * reading.accel - |
| 149 | frc971::math::Interpolate(base_clock, next_base_clock, |
| 150 | base_reading.accel, next_base_reading.accel, |
| 151 | reading_time); |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | // Helpers to accommodate serializing residuals into the ceres buffer. These |
| 157 | // helpers all return a buffer that points to the next value to be populated. |
| 158 | namespace internal { |
| 159 | template <typename Scalar> |
| 160 | std::span<Scalar> SerializeScalar(Scalar value, std::span<Scalar> out) { |
| 161 | DCHECK(!out.empty()); |
| 162 | out[0] = value; |
| 163 | return out.subspan(1); |
| 164 | } |
| 165 | template <typename Scalar, int kSize> |
| 166 | std::span<Scalar> SerializeVector(const Eigen::Matrix<Scalar, kSize, 1> &value, |
| 167 | std::span<Scalar> out) { |
| 168 | DCHECK_LE(static_cast<size_t>(value.size()), out.size()); |
| 169 | for (int index = 0; index < kSize; ++index) { |
| 170 | out[index] = value(index); |
| 171 | } |
| 172 | return out.subspan(kSize); |
| 173 | } |
| 174 | template <typename Scalar> |
| 175 | std::span<Scalar> SerializeParams(const DynamicImuParameters<Scalar> ¶ms, |
| 176 | std::span<Scalar> out) { |
| 177 | return SerializeVector(params.gyro_zero, |
| 178 | SerializeScalar(params.gravity, out)); |
| 179 | } |
| 180 | inline constexpr int kResidualsPerReading = 10u; |
| 181 | } // namespace internal |
| 182 | |
| 183 | template <typename Scalar> |
| 184 | void ImuCalibrator<Scalar>::CalculateResiduals(std::span<Scalar> residuals) { |
| 185 | EvaluateRelativeResiduals(); |
| 186 | for (size_t imu_index = 0; imu_index < imu_configs_.size(); ++imu_index) { |
| 187 | const auto &readings = imu_readings_[imu_index]; |
| 188 | double valid_gyro_reading_count = 0; |
| 189 | double valid_accel_reading_count = 0; |
| 190 | for (size_t reading_index = 0; reading_index < readings.size(); |
| 191 | ++reading_index) { |
| 192 | const auto &reading = readings[reading_index]; |
| 193 | if (reading.gyro_residual.has_value()) { |
| 194 | ++valid_gyro_reading_count; |
| 195 | } |
| 196 | if (reading.accel_residual.has_value()) { |
| 197 | ++valid_accel_reading_count; |
| 198 | } |
| 199 | } |
| 200 | if (!imu_configs_[imu_index].is_origin) { |
| 201 | CHECK_LT(0, valid_gyro_reading_count); |
| 202 | CHECK_LT(0, valid_accel_reading_count); |
| 203 | } else { |
| 204 | valid_gyro_reading_count = readings.size(); |
| 205 | valid_accel_reading_count = readings.size(); |
| 206 | } |
| 207 | // Adjust the residuals of the readings to ensure that the solver doesn't |
| 208 | // cheat by just making it so that the time-offsets are completely |
| 209 | // misaligned and we can say that all the residuals are "zero". |
| 210 | const Scalar gyro_reading_scalar = |
| 211 | static_cast<Scalar>(readings.size() / valid_gyro_reading_count); |
| 212 | const Scalar accel_reading_scalar = |
| 213 | static_cast<Scalar>(readings.size() / valid_accel_reading_count); |
| 214 | for (size_t reading_index = 0; reading_index < readings.size(); |
| 215 | ++reading_index) { |
| 216 | const auto &reading = readings[reading_index]; |
| 217 | const Scalar *const start_residual = residuals.data(); |
| 218 | // 4 residuals (gravity scalar; gyro zeroes) |
| 219 | residuals = |
| 220 | internal::SerializeParams(reading.parameter_residuals, residuals); |
| 221 | const Eigen::Matrix<Scalar, 3, 1> gyro_residual = |
| 222 | reading.gyro_residual.value_or(Eigen::Matrix<Scalar, 3, 1>::Zero()) * |
| 223 | gyro_reading_scalar; |
| 224 | // 3 residuals |
| 225 | residuals = internal::SerializeVector(gyro_residual, residuals); |
| 226 | const Eigen::Matrix<Scalar, 3, 1> accel_residual = |
| 227 | reading.accel_residual.value_or(Eigen::Matrix<Scalar, 3, 1>::Zero()) * |
| 228 | accel_reading_scalar; |
| 229 | // 3 residuals |
| 230 | residuals = internal::SerializeVector(accel_residual, residuals); |
| 231 | CHECK_EQ(internal::kResidualsPerReading, |
| 232 | residuals.data() - start_residual) |
| 233 | << ": Need to update kResidualsPerReading."; |
| 234 | } |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | template <typename Scalar> |
| 239 | size_t ImuCalibrator<Scalar>::CalculateNumResiduals( |
| 240 | const std::vector<size_t> &num_readings) { |
| 241 | size_t num_residuals = 0; |
| 242 | for (const size_t count : num_readings) { |
| 243 | num_residuals += internal::kResidualsPerReading * count; |
| 244 | } |
| 245 | return num_residuals; |
| 246 | } |
| 247 | |
| 248 | } // namespace frc971::imu |