blob: d8b7aab849590310e63bfd8aebace53e0c5a36dd [file] [log] [blame]
James Kuszmaula60aee12024-02-02 22:57:47 -08001#ifndef FRC971_IMU_IMU_CALIBRATOR_H_
2#define FRC971_IMU_IMU_CALIBRATOR_H_
3
4#include <optional>
5#include <span>
6#include <tuple>
7#include <vector>
8
Austin Schuh99f7c6a2024-06-25 22:07:44 -07009#include "absl/log/check.h"
10#include "absl/log/log.h"
James Kuszmaula60aee12024-02-02 22:57:47 -080011#include "absl/strings/str_format.h"
12#include "absl/strings/str_join.h"
13#include "ceres/ceres.h"
James Kuszmaula60aee12024-02-02 22:57:47 -080014#include <Eigen/Core>
15#include <Eigen/Geometry>
16
17#include "aos/time/time.h"
18
19namespace frc971::imu {
20
21// Contains a reading that comes directly from an IMU.
22// These should not be zeroed or corrected for yet.
23struct RawImuReading {
24 aos::monotonic_clock::time_point capture_time;
25 // gyro readings are in radians / sec; accelerometer readings are in g's.
26 const Eigen::Vector3d gyro;
27 const Eigen::Vector3d accel;
28};
29
30// Note on how we manage populating ceres parameters:
31// * We use dynamically-sized parameter lists for convenience.
32// * For every struct that we have that corresponds to problem parameters,
33// there is a PopulateParameters() method that takes a:
34// * ceres::Problem* that is used for setting parameter constraints.
35// * ceres::DynamicCostFunction* that is used to mark that we have added
36// a parameter block.
37// * A std::vector<double*> that will later be passed to AddResidualBlock.
38// We add any parameter blocks which we add to the problem.
39// * post_populate_methods is a list of std::function's that will get called
40// after everything is populated and added (this is necessary because
41// we cannot add constraints to the Problem until after everything
42// has been added).
43// * Additionally, there is a PopulateFromParameters() method which is used
44// in the ceres cost functor and:
45// * Takes a parameters double-pointer where parameters[X] is the
46// parameter block X.
47// * Returns a pair where the first value is a populated instance of the
48// struct and an updated parameters pointer which points to the next
49// set of pointers.
50// * All the Populate* methods must be called in the same order every
51// time so that they result in the raw pointers getting populated
52// consistently.
53
54// These are the parameters corresponding to things which will vary at every
55// power-on of the IMU.
56template <typename Scalar>
57struct DynamicImuParameters {
58 // A scalar to represent the current magnitude of gravity, in g's.
59 // This currently compensates both for any variations in local gravity as well
60 // as for some amount of variations in the IMU itself. In the future we may
61 // expand this to have a local gravity number that is global to all IMUs while
62 // separately calibrating per-axis information.
63 Scalar gravity;
64 // Current gyro zero, in radians / sec.
65 // These are the per-axis values that the gyro will read when it is sitting
66 // still. Technically these zeroes will drift over time; however, the
67 // time-windows that we expect to use for calibration are short enough that
68 // this should be a non-issue.
69 Eigen::Matrix<Scalar, 3, 1> gyro_zero;
70 void PopulateParameters(
71 ceres::Problem *problem, ceres::DynamicCostFunction *cost_function,
72 std::vector<double *> *parameters,
73 std::vector<std::function<void()>> *post_populate_methods) {
74 cost_function->AddParameterBlock(1);
75 parameters->push_back(&gravity);
76 cost_function->AddParameterBlock(3);
77 parameters->push_back(gyro_zero.data());
78 post_populate_methods->emplace_back([this, problem]() {
79 // Gravity shouldn't vary by much (these bounds are significantly larger
80 // than any real variations which we will experience).
81 problem->SetParameterLowerBound(&gravity, 0, 0.95);
82 problem->SetParameterUpperBound(&gravity, 0, 1.05);
83 for (int i = 0; i < 3; ++i) {
84 problem->SetParameterLowerBound(gyro_zero.data(), i, -0.05);
85 problem->SetParameterUpperBound(gyro_zero.data(), i, 0.05);
86 }
87 });
88 }
89 static std::tuple<DynamicImuParameters<Scalar>, const Scalar *const *>
90 PopulateFromParameters(const Scalar *const *parameters) {
91 const Scalar *const gravity = parameters[0];
92 ++parameters;
93 const Scalar *const gyro = parameters[0];
94 ++parameters;
95 return std::make_tuple(
96 DynamicImuParameters<Scalar>{
97 *gravity, Eigen::Matrix<Scalar, 3, 1>(gyro[0], gyro[1], gyro[2])},
98 parameters);
99 }
100 std::string ToString() const {
101 std::stringstream out;
102 out << "gravity: " << gravity << " gyro_zero: " << gyro_zero.transpose();
103 return out.str();
104 }
105};
106
107// These parameters correspond to IMU parameters which will not vary between
108// boots. Namely, the relative positions and time offsets of the IMU(s).
109template <typename Scalar>
110struct StaticImuParameters {
111 // If you have a point p_imu in this IMU's frame then (rotation *
112 // p_imu + position) will give you that point's position in the board frame
113 // (the "board" frame refers to the frame associated with the entire PCB,
114 // where the PCB itself contains multiple IMUs. The board frame will be
115 // attached to whichever IMU is being treated as the origin).
116 Eigen::Quaternion<Scalar> rotation;
117 // position is currently unused because it is only observeable when there
118 // are coriolis effects, and we currently only make use of accelerometer
119 // readings from when the IMU is sat still.
120 // As such, we currently assume that all position offsets are zero.
121 // Eigen::Matrix<Scalar, 3, 1> position;
122 // The "true" time at which the event occurred is equal to the capture time -
123 // time_offset. I.e., a more positive time offset implies that the there is a
124 // large delay between the imu readings being taken and us observing them.
125 Scalar time_offset;
126
127 void PopulateParameters(
Austin Schuh31f99a22024-06-25 18:30:13 -0700128 ceres::EigenQuaternionManifold *quaternion_local_parameterization,
James Kuszmaula60aee12024-02-02 22:57:47 -0800129 ceres::Problem *problem, ceres::DynamicCostFunction *cost_function,
130 std::vector<double *> *parameters,
131 std::vector<std::function<void()>> *post_populate_methods) {
132 cost_function->AddParameterBlock(4);
133 parameters->push_back(rotation.coeffs().data());
134 cost_function->AddParameterBlock(1);
135 parameters->push_back(&time_offset);
136 post_populate_methods->emplace_back(
137 [this, problem, quaternion_local_parameterization]() {
Austin Schuh31f99a22024-06-25 18:30:13 -0700138 problem->SetManifold(rotation.coeffs().data(),
139 quaternion_local_parameterization);
James Kuszmaula60aee12024-02-02 22:57:47 -0800140 problem->SetParameterLowerBound(&time_offset, 0, -0.03);
141 problem->SetParameterUpperBound(&time_offset, 0, 0.03);
142 });
143 }
144 static std::tuple<StaticImuParameters, const Scalar *const *>
145 PopulateFromParameters(const Scalar *const *parameters) {
146 const Scalar *const quat = parameters[0];
147 ++parameters;
148 const Scalar *const time = parameters[0];
149 ++parameters;
150 return std::make_tuple(
151 StaticImuParameters{
152 Eigen::Quaternion<Scalar>(quat[3], quat[0], quat[1], quat[2]),
153 *time},
154 parameters);
155 }
156 std::string ToString() const {
157 std::stringstream out;
158 out << "quat: " << rotation.coeffs().transpose()
159 << " time_offset: " << time_offset;
160 return out.str();
161 }
162};
163
164// Represents the calibration for a single IMU.
165template <typename Scalar>
166struct ImuConfig {
167 // Set to true if this IMU is to be considered the origin of the coordinate
168 // system. This will also mean that this IMU is treated as the source-of-truth
169 // for clock offsets.
170 bool is_origin;
171 // Will be nullopt if is_origin is true (the corresponding rotations and
172 // offsets will all be the identity/zero as appropriate).
173 std::optional<StaticImuParameters<Scalar>> parameters;
174
175 DynamicImuParameters<Scalar> dynamic_params{
176 .gravity = static_cast<Scalar>(1.0),
177 .gyro_zero = Eigen::Matrix<Scalar, 3, 1>::Zero()};
178 std::string ToString() const {
179 return absl::StrFormat(
180 "is_origin: %d params: %s dynamic: %s", is_origin,
181 parameters.has_value() ? parameters->ToString() : std::string("<null>"),
182 dynamic_params.ToString());
183 }
184};
185
186// Represents all of the configuration parameters for the entire system.
187template <typename Scalar>
188struct AllParameters {
189 // Each entry in the imus vector will be a single imu.
190 std::vector<ImuConfig<Scalar>> imus;
191 std::tuple<std::vector<double *>, std::vector<std::function<void()>>>
192 PopulateParameters(
Austin Schuh31f99a22024-06-25 18:30:13 -0700193 ceres::EigenQuaternionManifold *quaternion_local_parameterization,
James Kuszmaula60aee12024-02-02 22:57:47 -0800194 ceres::Problem *problem, ceres::DynamicCostFunction *cost_function) {
195 std::vector<std::function<void()>> post_populate_methods;
196 std::vector<double *> parameters;
197 for (auto &imu : imus) {
198 if (imu.parameters.has_value()) {
199 imu.parameters.value().PopulateParameters(
200 quaternion_local_parameterization, problem, cost_function,
201 &parameters, &post_populate_methods);
202 }
203 imu.dynamic_params.PopulateParameters(problem, cost_function, &parameters,
204 &post_populate_methods);
205 }
206 return std::make_tuple(parameters, post_populate_methods);
207 }
208 static AllParameters PopulateFromParameters(
209 const std::vector<ImuConfig<double>> &nominal_configs,
210 const Scalar *const *parameters) {
211 std::vector<ImuConfig<Scalar>> result;
212 for (size_t imu_index = 0; imu_index < nominal_configs.size();
213 ++imu_index) {
214 ImuConfig<Scalar> config;
215 config.is_origin = nominal_configs[imu_index].is_origin;
216 if (!config.is_origin) {
217 std::tie(config.parameters, parameters) =
218 StaticImuParameters<Scalar>::PopulateFromParameters(parameters);
219 }
220 std::tie(config.dynamic_params, parameters) =
221 DynamicImuParameters<Scalar>::PopulateFromParameters(parameters);
222 result.emplace_back(std::move(config));
223 }
224 return {.imus = std::move(result)};
225 }
226 std::string ToString() const {
227 std::vector<std::string> imu_strings;
228 for (const auto &imu : imus) {
229 std::vector<std::string> dynamic_params;
230 imu_strings.push_back(absl::StrFormat("config: %s", imu.ToString()));
231 }
232 return absl::StrJoin(imu_strings, "\n");
233 }
234};
235
236// This class does the hard work to support calibrating multiple IMU's
237// orientations relative to one another. It is set up to readily be used with a
238// ceres solver (see imu_calibrator.*).
239//
240// The current theory of operation is to have some number of imus, one of which
241// we will consider to be fixed in position. We have a fixed set of data that we
242// feed into this class, which can be parameterized based on:
243// * The current zeroes for each IMU.
244// * The orientation of each non-fixed IMU.
245// * The time-offset of each non-fixed IMU.
246//
247// When run under ceres, ceres can then vary these parameters to solve for the
248// current calibrations of the IMUs.
249//
250// When solving, the main complexity is that some internal state has to be
251// tracked to try to determine when we should be calculating zeroes and when we
252// can calibrate out the magnitude of gravity. This is done by tracking when the
253// IMUs are "stationary". For a reading to be stationary, all values with
254// FLAGS_imu_zeroing_buffer readings of the reading must be "plausibly
255// stationary". Readings are plausibly stationary if they have sufficiently low
256// gyro values.
257//
258// TODO: Provide some utilities to plot the results of a calibration.
259template <typename Scalar>
260class ImuCalibrator {
261 public:
262 ImuCalibrator(const std::vector<ImuConfig<Scalar>> &imu_configs)
263 : imu_configs_(imu_configs), imu_readings_(imu_configs.size()) {
264 origin_index_ = -1;
265 for (size_t imu_index = 0; imu_index < imu_configs_.size(); ++imu_index) {
266 if (imu_configs_[imu_index].is_origin) {
267 CHECK_EQ(origin_index_, -1)
268 << ": Can't have more than one IMU specified as the origin.";
269 origin_index_ = imu_index;
270 }
271 }
272 CHECK_NE(origin_index_, -1)
273 << ": Must have at least one IMU specified as the origin.";
274 }
275
276 // These gyro readings will be "raw"---i.e., they still need to get
277 // transformed by nominal_transform.
278 // gyro readings are in radians / sec; accelerometer readings are in g's.
279 // Within a given imu, must be called in time order.
280 void InsertImu(size_t imu_index, const RawImuReading &reading);
281
282 // Populates all the residuals that we use for the cost in ceres.
283 void CalculateResiduals(std::span<Scalar> residuals);
284
285 // Returns the total number of residuals that this problem will have, given
286 // the number of readings for each IMU.
287 static size_t CalculateNumResiduals(const std::vector<size_t> &num_readings);
288
289 private:
290 // Represents an imu reading. The values in this are generally already
291 // adjusted for the provided parameters.
292 struct ImuReading {
293 // The "actual" provided capture time. Used for debugging.
294 aos::monotonic_clock::time_point capture_time_raw;
295 // The capture time, adjusted for this IMU's time offset.
296 Scalar capture_time_adjusted;
297 // gyro reading, adjusted for gyro zero but NOT rotation.
298 // In radians / sec.
299 Eigen::Matrix<Scalar, 3, 1> gyro;
300 // accelerometer reading, adjusted for gravity value but NOT rotation.
301 // In g's.
302 Eigen::Matrix<Scalar, 3, 1> accel;
303 // Residuals associated with the DynamicImuParameters for this imu.
304 DynamicImuParameters<Scalar> parameter_residuals;
305 // Set if this measurement *could* be part of a segment of time where the
306 // IMU is stationary.
307 bool plausibly_stationary;
308 // Set to true if all values with FLAGS_imu_zeroing_buffer of this reading
309 // are plausibly_stationary.
310 bool stationary;
311 // We set stationary_is_locked once we have enough readings to either side
312 // of this value to guarantee that it is stationary.
313 bool stationary_is_locked;
314 // Residuals that are used for calibrating the rotation values. These are
315 // nullopt if we can't calibrate for some reason (e.g., this is the origin
316 // IMU, or for the accelerometer residual, we don't populate it if we are
317 // not stationary).
318 std::optional<Eigen::Matrix<Scalar, 3, 1>> gyro_residual;
319 std::optional<Eigen::Matrix<Scalar, 3, 1>> accel_residual;
320 };
321 void EvaluateRelativeResiduals();
322
323 const std::vector<ImuConfig<Scalar>> imu_configs_;
324 // Index of the IMU which is the origin IMU.
325 int origin_index_;
326 std::vector<std::vector<ImuReading>> imu_readings_;
327};
328
329} // namespace frc971::imu
330
331#include "frc971/imu/imu_calibrator-tmpl.h"
332#endif // FRC971_IMU_IMU_CALIBRATOR_H_