blob: 67431069658dd8731a4e41fb07bc9455ffae2a8a [file] [log] [blame]
James Kuszmaula60aee12024-02-02 22:57:47 -08001#include "frc971/imu/imu_calibrator.h"
2#include "frc971/math/interpolate.h"
3
4DECLARE_int32(imu_zeroing_buffer);
5
6namespace frc971::imu {
7
8inline constexpr double kGravityGs = 1.0;
9// rad / sec
10inline constexpr double kGyroMaxZeroingValue = 0.1;
11
12template <typename Scalar>
13void 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
75template <typename Scalar>
76void 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.
158namespace internal {
159template <typename Scalar>
160std::span<Scalar> SerializeScalar(Scalar value, std::span<Scalar> out) {
161 DCHECK(!out.empty());
162 out[0] = value;
163 return out.subspan(1);
164}
165template <typename Scalar, int kSize>
166std::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}
174template <typename Scalar>
175std::span<Scalar> SerializeParams(const DynamicImuParameters<Scalar> &params,
176 std::span<Scalar> out) {
177 return SerializeVector(params.gyro_zero,
178 SerializeScalar(params.gravity, out));
179}
180inline constexpr int kResidualsPerReading = 10u;
181} // namespace internal
182
183template <typename Scalar>
184void 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
238template <typename Scalar>
239size_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