blob: a341b2aa4971ef5d634146fd7762978877a85511 [file] [log] [blame]
James Kuszmaula60aee12024-02-02 22:57:47 -08001#include "frc971/imu/imu_calibrator.h"
2
3#include <random>
4
5#include "gtest/gtest.h"
6
7#include "frc971/imu/imu_calibrator_solver.h"
8
9namespace frc971::imu::testing {
10class ImuSimulator {
11 public:
12 ImuSimulator(const std::vector<ImuConfig<double>> &imu_configs)
13 : imu_configs_(imu_configs), readings_(imu_configs.size()) {}
14 void SimulateForTime(aos::monotonic_clock::duration duration,
15 aos::monotonic_clock::duration dt,
16 const Eigen::Vector3d &gravity_vector,
17 const Eigen::Vector3d &accel,
18 const Eigen::Vector3d &gyro) {
19 for (const aos::monotonic_clock::time_point end_time = now + duration;
20 now < end_time; now += dt) {
21 for (size_t imu_index = 0; imu_index < imu_configs_.size(); ++imu_index) {
22 const ImuConfig<double> &config = imu_configs_[imu_index];
23 const Eigen::Quaterniond rotation =
24 config.is_origin ? Eigen::Quaterniond::Identity()
25 : config.parameters->rotation.inverse();
26 const std::chrono::nanoseconds time_offset{
27 config.is_origin
28 ? 0
29 : static_cast<uint64_t>(config.parameters->time_offset * 1e9)};
30 readings_[imu_index].emplace_back(
31 now + time_offset,
32 rotation * gyro + config.dynamic_params.gyro_zero + GyroNoise(),
33 rotation *
34 (accel + gravity_vector * config.dynamic_params.gravity) +
35 AccelNoise());
36 }
37 }
38 }
39 Eigen::Vector3d GyroNoise() {
40 return (enable_noise_ ? 1.0 : 0.0) *
41 Eigen::Vector3d(distribution_(generator_), distribution_(generator_),
42 distribution_(generator_));
43 }
44 Eigen::Vector3d AccelNoise() { return GyroNoise() * 2.0; }
45 void set_enable_noise(bool enable) { enable_noise_ = enable; }
46 std::vector<std::vector<RawImuReading>> readings() const {
47 return readings_;
48 };
49
50 private:
51 const std::vector<ImuConfig<double>> imu_configs_;
52 std::vector<std::vector<RawImuReading>> readings_;
53 aos::monotonic_clock::time_point now = aos::monotonic_clock::epoch();
54
55 std::mt19937 generator_;
56 std::normal_distribution<> distribution_{0.0, 0.00025};
57 bool enable_noise_ = false;
58};
59
60namespace {
61void VerifyParameters(const std::vector<ImuConfig<double>> &imus,
62 const AllParameters<double> &params, double eps = 1e-8) {
63 ASSERT_EQ(imus.size(), params.imus.size());
64 for (size_t imu_index = 0; imu_index < imus.size(); ++imu_index) {
65 SCOPED_TRACE(imu_index);
66 const ImuConfig<double> &expected = imus[imu_index];
67 const ImuConfig<double> &calculated = params.imus[imu_index];
68 EXPECT_EQ(expected.parameters.has_value(),
69 calculated.parameters.has_value());
70 EXPECT_NEAR(expected.dynamic_params.gravity,
71 calculated.dynamic_params.gravity, eps);
72 EXPECT_LT((expected.dynamic_params.gyro_zero -
73 calculated.dynamic_params.gyro_zero)
74 .norm(),
75 eps)
76 << expected.dynamic_params.gyro_zero.transpose() << " vs. "
77 << calculated.dynamic_params.gyro_zero.transpose();
78 if (expected.parameters.has_value()) {
79 EXPECT_NEAR(expected.parameters->time_offset,
80 calculated.parameters->time_offset, eps);
81 EXPECT_LT(((expected.parameters->rotation *
82 calculated.parameters->rotation.inverse())
83 .coeffs() -
84 Eigen::Quaterniond::Identity().coeffs())
85 .norm(),
86 eps)
87 << expected.parameters->rotation.coeffs().transpose() << " vs. "
88 << calculated.parameters->rotation.coeffs().transpose();
89 }
90 }
91}
92} // namespace
93
94// Confirms that we can calibrate in a relatively simple scenario where we have
95// some gyro/accelerometer offsets and a small rotation that is not accounted
96// for in the nominal parameters.
97TEST(ImuCalibratorTest, BasicCalibrationTest) {
98 std::vector<ImuConfig<double>> nominal_imus = {
99 ImuConfig<double>{true, std::nullopt},
100 ImuConfig<double>{false, std::make_optional<StaticImuParameters<double>>(
101 Eigen::Quaterniond::Identity(), 0.0)}};
102
103 std::vector<ImuConfig<double>> real_imus = nominal_imus;
104 real_imus[0].dynamic_params.gravity = 1.005;
105 real_imus[0].dynamic_params.gyro_zero << 0.001, 0.002, 0.003;
106 real_imus[1].dynamic_params.gyro_zero << -0.009, -0.007, -0.001;
107 real_imus[1].parameters->rotation =
108 Eigen::AngleAxisd(0.01, Eigen::Vector3d::UnitZ());
109 ImuSimulator simulator(real_imus);
110 simulator.SimulateForTime(std::chrono::seconds(1),
111 std::chrono::milliseconds(1),
112 Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0, 0, 0),
113 Eigen::Vector3d(0.0, 0.0, 0.0));
114 simulator.SimulateForTime(std::chrono::seconds(1),
115 std::chrono::milliseconds(1),
116 Eigen::Vector3d(0, 1, 0), Eigen::Vector3d(0, 0, 0),
117 Eigen::Vector3d(0.0, 0.0, 0.0));
118 simulator.SimulateForTime(std::chrono::seconds(1),
119 std::chrono::milliseconds(1),
120 Eigen::Vector3d(1, 0, 0), Eigen::Vector3d(0, 0, 0),
121 Eigen::Vector3d(0.0, 0.0, 0.0));
122 simulator.SimulateForTime(
123 std::chrono::seconds(1), std::chrono::milliseconds(1),
124 Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0.1, 0.2, 0.3),
125 Eigen::Vector3d(1.0, 0.0, 0.0));
126 simulator.SimulateForTime(
127 std::chrono::seconds(1), std::chrono::milliseconds(1),
128 Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0.1, 0.2, 0.3),
129 Eigen::Vector3d(0.0, 1.0, 0.0));
130 auto params = Solve(simulator.readings(), nominal_imus);
131 LOG(INFO) << params.ToString();
132 LOG(INFO) << real_imus[0].ToString();
133 LOG(INFO) << real_imus[1].ToString();
134 VerifyParameters(real_imus, params);
135}
136
137// Separately test the estimation of the time offset between IMUs.
138// This is done separately because the optimization problem is poorly condition
139// for estimating time offsets when there is just a handful of step changes in
140// the IMU inputs; feeding in a sine wave works much better for allowing the
141// solver to estimate the offset.
142TEST(ImuCalibratorTest, TimeOffsetTest) {
143 gflags::FlagSaver flag_saver;
144
145 std::vector<ImuConfig<double>> nominal_imus = {
146 ImuConfig<double>{true, std::nullopt},
147 ImuConfig<double>{false, std::make_optional<StaticImuParameters<double>>(
148 Eigen::Quaterniond::Identity(), 0.0)}};
149
150 std::vector<ImuConfig<double>> real_imus = nominal_imus;
151 real_imus[1].parameters->time_offset = 0.0255;
152 ImuSimulator simulator(real_imus);
153 // Note on convergence: It is easy to end up in situations where the problem
154 // is not outstandingly well conditioned and we can end up with local minima
155 // where changes to physical calibration attributes can explain the time
156 // offset.
157 simulator.SimulateForTime(std::chrono::seconds(1),
158 std::chrono::milliseconds(1),
159 Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0, 0, 0),
160 Eigen::Vector3d(0.0, 0.0, 0.0));
161 for (size_t ii = 0; ii < 10000; ++ii) {
162 simulator.SimulateForTime(
163 std::chrono::milliseconds(1), std::chrono::milliseconds(1),
164 Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0.0, 0.0, 0.0),
165 Eigen::Vector3d(std::sin(ii / 1000.0), 0.0, 0.0));
166 }
167 auto params = Solve(simulator.readings(), nominal_imus);
168 LOG(INFO) << params.ToString();
169 LOG(INFO) << real_imus[0].ToString();
170 LOG(INFO) << real_imus[1].ToString();
171 VerifyParameters(real_imus, params, 1e-6);
172}
173
174// Test that if we add in some random noise that the solver still behaves
175// itself.
176TEST(ImuCalibratorTest, RandomNoise) {
177 std::vector<ImuConfig<double>> nominal_imus = {
178 ImuConfig<double>{true, std::nullopt},
179 ImuConfig<double>{false, std::make_optional<StaticImuParameters<double>>(
180 Eigen::Quaterniond::Identity(), 0.0)}};
181
182 std::vector<ImuConfig<double>> real_imus = nominal_imus;
183 real_imus[0].dynamic_params.gravity = 0.999;
184 real_imus[0].dynamic_params.gyro_zero << 0.001, 0.002, 0.003;
185 real_imus[1].dynamic_params.gyro_zero << -0.009, -0.007, -0.001;
186 real_imus[1].parameters->rotation =
187 Eigen::AngleAxisd(0.01, Eigen::Vector3d::UnitZ());
188 ImuSimulator simulator(real_imus);
189 simulator.set_enable_noise(true);
190 simulator.SimulateForTime(std::chrono::seconds(1),
191 std::chrono::milliseconds(1),
192 Eigen::Vector3d(0, 0, 1), Eigen::Vector3d(0, 0, 0),
193 Eigen::Vector3d(0.0, 0.0, 0.0));
194 simulator.SimulateForTime(std::chrono::seconds(1),
195 std::chrono::milliseconds(1),
196 Eigen::Vector3d(0, 1, 0), Eigen::Vector3d(0, 0, 0),
197 Eigen::Vector3d(0.0, 0.0, 0.0));
198 simulator.SimulateForTime(std::chrono::seconds(1),
199 std::chrono::milliseconds(1),
200 Eigen::Vector3d(1, 0, 0), Eigen::Vector3d(0, 0, 0),
201 Eigen::Vector3d(0.0, 0.0, 0.0));
202 for (size_t ii = 0; ii < 6000; ++ii) {
203 simulator.SimulateForTime(std::chrono::milliseconds(1),
204 std::chrono::milliseconds(1),
205 Eigen::Vector3d(0, 0, 1),
206 Eigen::Vector3d(std::sin(ii / 1000.0), 0.0, 0.0),
207 Eigen::Vector3d(1.0, 0.0, 0.0));
208 }
209 auto params = Solve(simulator.readings(), nominal_imus);
210 LOG(INFO) << params.ToString();
211 LOG(INFO) << real_imus[0].ToString();
212 LOG(INFO) << real_imus[1].ToString();
213 VerifyParameters(real_imus, params, 1e-4);
214}
215} // namespace frc971::imu::testing