Skip to content

Commit cbff03c

Browse files
[ImuFactor] Unit test the on manifold ImuFactor
Don't bother reviewing this yet Seperate to allow the ImuFactor to be merged more quickly Topic: on_manifold_imu_factor_test Relative: on_manifold_imu_factor Labels: draft
1 parent cb930c9 commit cbff03c

2 files changed

Lines changed: 332 additions & 0 deletions

File tree

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ foreach(SOURCE_FILE ${SYMFORCE_TEST_CC_SOURCES})
5858
symforce_opt
5959
symforce_examples
6060
symforce_test
61+
symforce_slam
6162
)
6263

6364
list(APPEND SYMFORCE_CC_TEST_TARGETS ${TARGET_NAME})
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
/* ----------------------------------------------------------------------------
2+
* SymForce - Copyright 2022, Skydio, Inc.
3+
* This source code is under the Apache 2.0 license found in the LICENSE file.
4+
* ---------------------------------------------------------------------------- */
5+
6+
#include <algorithm>
7+
#include <iostream>
8+
#include <random>
9+
#include <thread>
10+
11+
#include <Eigen/Dense>
12+
#include <catch2/catch_test_macros.hpp>
13+
14+
#include <sym/factors/imu_manifold_preintegration_update.h>
15+
#include <sym/rot3.h>
16+
#include <sym/util/epsilon.h>
17+
#include <symforce/slam/imu_preintegration/preintegrated_imu_measurements.h>
18+
19+
using Eigen::Ref;
20+
using Eigen::Vector3d;
21+
using sym::Rot3d;
22+
23+
void UpdateState(const Rot3d& DR, const Ref<const Vector3d>& Dv, const Ref<const Vector3d>& Dp,
24+
const Ref<const Vector3d>& gyro, const Ref<const Vector3d>& accel,
25+
const Ref<const Vector3d>& gyro_bias, const Ref<const Vector3d>& accel_bias,
26+
const double dt, const double epsilon, Rot3d& new_DR, Ref<Vector3d> new_Dv,
27+
Ref<Vector3d> new_Dp) {
28+
const auto corrected_accel = accel - accel_bias;
29+
new_DR = DR * Rot3d::FromTangent((gyro - gyro_bias) * dt, epsilon);
30+
new_Dv = Dv + DR * corrected_accel * dt;
31+
new_Dp = Dp + Dv * dt + DR * corrected_accel * (0.5 * dt * dt);
32+
}
33+
34+
TEST_CASE("ImuManifoldPreintegrationUpdate basic tests", "[slam]") {
35+
std::mt19937 gen(1804);
36+
std::normal_distribution<double> dist{0.0, 10.0};
37+
const auto randomM3 = Eigen::Matrix3d::NullaryExpr([&]() { return dist(gen); });
38+
const auto randomV3 = Eigen::Vector3d::NullaryExpr([&]() { return dist(gen); });
39+
const auto randomM9 = Eigen::Matrix<double, 9, 9>::NullaryExpr([&]() { return dist(gen); });
40+
41+
for (int i_ = 0; i_ < 10; i_++) {
42+
const sym::Rot3d DR = sym::Rot3d::Random(gen);
43+
const Eigen::Vector3d Dv = randomV3;
44+
const Eigen::Vector3d Dp = randomV3;
45+
const Eigen::Matrix<double, 9, 9> covariance = randomM9;
46+
const Eigen::Matrix3d DR_D_gyro_bias = randomM3;
47+
const Eigen::Matrix3d Dv_D_gyro_bias = randomM3;
48+
const Eigen::Matrix3d Dv_D_accel_bias = randomM3;
49+
const Eigen::Matrix3d Dp_D_gyro_bias = randomM3;
50+
const Eigen::Matrix3d Dp_D_accel_bias = randomM3;
51+
52+
const Eigen::Vector3d gyro = randomV3;
53+
const Eigen::Vector3d accel = randomV3;
54+
55+
const double dt = 1.24;
56+
57+
const Eigen::Vector3d gyro_bias = randomV3;
58+
const Eigen::Vector3d accel_bias = randomV3;
59+
const Eigen::Matrix3d gyro_cov = randomM3;
60+
const Eigen::Matrix3d accel_cov = randomM3;
61+
62+
sym::Rot3d new_DR;
63+
Eigen::Vector3d new_Dv;
64+
Eigen::Vector3d new_Dp;
65+
Eigen::Matrix<double, 9, 9> new_covarinace;
66+
Eigen::Matrix3d new_DR_D_gyro_bias;
67+
Eigen::Matrix3d new_Dp_D_gyro_bias;
68+
Eigen::Matrix3d new_Dp_D_accel_bias;
69+
Eigen::Matrix3d new_Dv_D_gyro_bias;
70+
Eigen::Matrix3d new_Dv_D_accel_bias;
71+
72+
sym::ImuManifoldPreintegrationUpdate<double>(
73+
DR, Dv, Dp, covariance, DR_D_gyro_bias, Dv_D_gyro_bias, Dv_D_accel_bias, Dp_D_gyro_bias,
74+
Dp_D_accel_bias, gyro_bias, accel_bias, gyro_cov, accel_cov, gyro, accel, dt,
75+
sym::kDefaultEpsilond, &new_DR, &new_Dv, &new_Dp, nullptr, nullptr, nullptr, nullptr,
76+
nullptr, nullptr);
77+
78+
sym::Rot3d expected_DR;
79+
Eigen::Vector3d expected_Dv;
80+
Eigen::Vector3d expected_Dp;
81+
82+
UpdateState(DR, Dv, Dp, gyro, accel, gyro_bias, accel_bias, dt, sym::kDefaultEpsilond,
83+
expected_DR, expected_Dv, expected_Dp);
84+
85+
CHECK(sym::IsClose(new_DR, expected_DR, 1e-14));
86+
CHECK(sym::IsClose(new_Dv, expected_Dv, 1e-14));
87+
CHECK(sym::IsClose(new_Dp, expected_Dp, 1e-14));
88+
}
89+
}
90+
91+
/**
92+
* Helper class to generate samples from a multi-variate normal distribution with
93+
* the same covariance as that passed into the constructor.
94+
*/
95+
class MultiVarNormalDist {
96+
private:
97+
const Eigen::MatrixXd L;
98+
const long int vars;
99+
std::normal_distribution<double> dist;
100+
101+
public:
102+
/**
103+
* covar is the covariance of the desired normal distribution.
104+
* Precondition: covar is a symmetric, positive definite matrix.
105+
*/
106+
MultiVarNormalDist(const Eigen::MatrixXd covar)
107+
: L{covar.llt().matrixL()}, vars{covar.rows()}, dist{} {}
108+
109+
/**
110+
* Returns a matrix whose (count) colums are samples from the distribution.
111+
*/
112+
template <typename Generator>
113+
Eigen::MatrixXd Sample(Generator& gen, const int count) {
114+
Eigen::MatrixXd rand = Eigen::MatrixXd::NullaryExpr(vars, count, [&]() { return dist(gen); });
115+
return L * rand;
116+
}
117+
};
118+
119+
/**
120+
* Calculates the covariance of the columns of samples.
121+
*/
122+
Eigen::MatrixXd Covariance(const Eigen::MatrixXd& samples) {
123+
const long int sample_size = samples.cols();
124+
const Eigen::VectorXd avg_col = samples.rowwise().sum() / sample_size;
125+
const Eigen::MatrixXd meanless_samples = samples.colwise() - avg_col;
126+
return (meanless_samples * meanless_samples.transpose()) / sample_size;
127+
}
128+
129+
TEST_CASE("Test MultiVarNormalDist generates samples with correct covariance", "[slam]") {
130+
std::mt19937 gen(170);
131+
132+
// Create covar, a symmetric positive definite matrix
133+
// NOTE(brad): This matrix is positive semi-definite because the diagonal entries all
134+
// have values of at least 2.0, and the max value of an off-diagonal entry is .2. Thus
135+
// within a row, the sum of the non-diagonal entries is capped by 0.2 * 8 = 1.6. Thus by the
136+
// Gershgorin circle theorem, any eigen value of covar is at least 0.4 (i.e., positive).
137+
std::uniform_real_distribution<double> uniform_dist(0.01, 0.1);
138+
Eigen::MatrixXd covar = Eigen::MatrixXd::NullaryExpr(9, 9, [&]() { return uniform_dist(gen); });
139+
covar += covar.transpose().eval();
140+
covar.diagonal().array() += 2.0;
141+
142+
MultiVarNormalDist dist(covar);
143+
144+
const Eigen::MatrixXd samples = dist.Sample(gen, 2 << 22);
145+
const Eigen::Matrix<double, 9, 9> calculated_covar = Covariance(samples);
146+
147+
for (int col = 0; col < 9; col++) {
148+
for (int row = 0; row < 9; row++) {
149+
const double covar_rc = covar(row, col);
150+
const double difference = covar_rc - calculated_covar(row, col);
151+
// NOTE(brad): trade-off between sample count (time) and accuracy of calculated_covar
152+
CHECK(difference * difference < 9e-3 * covar_rc * covar_rc);
153+
}
154+
}
155+
}
156+
157+
// TODO: finish tuning this
158+
TEST_CASE("Test PreintegratedImuMeasurements.state_covariance", "[slam]") {
159+
// Parameters
160+
const double dt = 1e-3;
161+
162+
const Eigen::Vector3d accel_bias = {3.4, 1.6, -5.9};
163+
const Eigen::Vector3d gyro_bias = {1.2, -2.4, 0.5};
164+
165+
const Eigen::Matrix3d accel_cov =
166+
(Eigen::Matrix3d() << 7e-5, 1e-7, 1e-7, 1e-7, 7e-5, 1e-7, 1e-7, 1e-7, 7e-5).finished();
167+
const Eigen::Matrix3d gyro_cov =
168+
(Eigen::Matrix3d() << 1e-3, 1e-5, 1e-5, 1e-5, 1e-3, 1e-5, 1e-5, 1e-5, 1e-3).finished();
169+
170+
const Eigen::Vector3d true_gyro = Eigen::Vector3d::Constant(1.2);
171+
const Eigen::Vector3d true_accel = Eigen::Vector3d::Constant(4.3);
172+
173+
const int sample_count = 1 << 24;
174+
Eigen::MatrixXd samples(9, sample_count);
175+
const int measurements_per_sample = 5;
176+
177+
// Multithreading
178+
const int thread_count = 16;
179+
const int steps_per_thread = sample_count / thread_count;
180+
// NOTE(brad): I assume I can equally distribute samples among the threads
181+
CHECK(sample_count % thread_count == 0);
182+
std::vector<std::thread> threads;
183+
std::array<Eigen::Matrix<double, 9, 9>, thread_count> covariance_sums;
184+
covariance_sums.fill(Eigen::Matrix<double, 9, 9>::Zero());
185+
186+
for (int i = 0; i < thread_count; i++) {
187+
// Explicit about captures to make checking for thread safety easier
188+
threads.emplace_back([&accel_bias, &gyro_bias, &accel_cov, &gyro_cov, &true_accel, &true_gyro,
189+
&samples, &covariance_sums, dt, i]() {
190+
std::mt19937 gen(1813 + i);
191+
// Each thread needs its own distribution as they are not thread safe
192+
MultiVarNormalDist accel_dist(accel_cov / dt);
193+
MultiVarNormalDist gyro_dist(gyro_cov / dt);
194+
195+
Eigen::MatrixXd accel_noise;
196+
Eigen::MatrixXd gyro_noise;
197+
198+
for (int j = steps_per_thread * i; j < steps_per_thread * (i + 1); j++) {
199+
sym::PreintegratedImuMeasurements<double> pim(accel_bias, gyro_bias);
200+
201+
gyro_noise = gyro_dist.Sample(gen, measurements_per_sample);
202+
accel_noise = accel_dist.Sample(gen, measurements_per_sample);
203+
204+
for (int k = 0; k < measurements_per_sample; k++) {
205+
pim.IntegrateMeasurement(true_accel + accel_noise.col(k), true_gyro + gyro_noise.col(k),
206+
accel_cov, gyro_cov, dt, 0.0);
207+
}
208+
209+
samples.col(j).segment(0, 3) = pim.DR.ToTangent(sym::kDefaultEpsilond);
210+
samples.col(j).segment(3, 3) = pim.Dv;
211+
samples.col(j).segment(6, 3) = pim.Dp;
212+
213+
covariance_sums[i] += pim.state_covariance;
214+
}
215+
});
216+
}
217+
for (std::thread& t : threads) {
218+
t.join();
219+
}
220+
221+
Eigen::Matrix<double, 9, 9> calculated_covariance = Eigen::Matrix<double, 9, 9>::Zero();
222+
for (Eigen::Matrix<double, 9, 9>& cov : covariance_sums) {
223+
calculated_covariance += cov;
224+
}
225+
calculated_covariance *= 1.0 / sample_count;
226+
227+
const Eigen::MatrixXd true_covariance = Covariance(samples);
228+
229+
// TODO change to be absolute value
230+
const double true_covariance_max = true_covariance.maxCoeff();
231+
232+
auto relative_difference = [true_covariance_max](const double x, const double y) -> double {
233+
return std::abs((x - y) / x);
234+
};
235+
236+
const Eigen::MatrixXd cov_relative_error =
237+
true_covariance.binaryExpr(calculated_covariance, relative_difference);
238+
239+
for (int col = 0; col < 9; col++) {
240+
for (int row = 0; row < 9; row++) {
241+
const double abs_coef = std::abs(true_covariance(row, col));
242+
if (abs_coef > 2e-2 * true_covariance_max) {
243+
CHECK(cov_relative_error(row, col) < 0.01);
244+
} else if (abs_coef > 2e-3 * true_covariance_max) {
245+
CHECK(cov_relative_error(row, col) < 0.05);
246+
} else if (abs_coef > 3e-4 * true_covariance_max) {
247+
CHECK(cov_relative_error(row, col) < 0.15);
248+
} else {
249+
CHECK(cov_relative_error(row, col) < 2);
250+
}
251+
}
252+
}
253+
254+
// const Eigen::MatrixXd avg_difference = true_covariance - calculated_covariance;
255+
256+
// std::cout << "true_covariance:\n\n" << true_covariance << std::endl;
257+
// std::cout << "max of true covariance:\n" << true_covariance.maxCoeff() << std::endl;
258+
// std::cout << "average calculated_covariance:\n\n" << calculated_covariance << std::endl;
259+
// std::cout << "avg_relative_difference:\n\n" << cov_relative_error << std::endl;
260+
}
261+
262+
TEST_CASE("New Test bias derivatives", "[slam]") {
263+
using M33 = Eigen::Matrix<double, 3, 3>;
264+
using M96 = Eigen::Matrix<double, 9, 6>;
265+
// Initialize parameters
266+
const Eigen::Vector3d gyro_bias = {1.2, -3.4, 2.2};
267+
const Eigen::Vector3d accel_bias = {5.2, -6.4, 5.2};
268+
269+
const Eigen::Vector3d true_gyro = Eigen::Vector3d::Constant(1.2);
270+
const Eigen::Vector3d true_accel = Eigen::Vector3d::Constant(4.3);
271+
272+
const M33 gyro_cov = (M33() << 1e-3, 1e-5, 1e-5, 1e-5, 1e-3, 1e-5, 1e-5, 1e-5, 1e-3).finished();
273+
const Eigen::Matrix3d accel_cov =
274+
(M33() << 7e-5, 1e-7, 1e-7, 1e-7, 7e-5, 1e-7, 1e-7, 1e-7, 7e-5).finished();
275+
276+
const double dt = 0.001;
277+
278+
const int iterations = 100;
279+
280+
sym::PreintegratedImuMeasurements<double> pim(accel_bias, gyro_bias);
281+
282+
for (int k_ = 0; k_ < iterations; k_++) {
283+
pim.IntegrateMeasurement(true_accel, true_gyro, accel_cov, gyro_cov, dt, sym::kDefaultEpsilond);
284+
}
285+
286+
Eigen::Matrix<double, 9, 6> state_D_bias =
287+
(Eigen::Matrix<double, 9, 6>() << pim.DR_D_gyro_bias, M33::Zero(), pim.Dv_D_gyro_bias,
288+
pim.Dv_D_accel_bias, pim.Dp_D_gyro_bias, pim.Dp_D_accel_bias)
289+
.finished();
290+
291+
// Perturbed inputs and calculate derivatives numerically from them
292+
const double perturbation = 1e-5;
293+
const double inverse_perturbation = 1 / perturbation;
294+
M96 numerical_state_D_bias;
295+
for (int i = 0; i < 6; i++) {
296+
// Create biases and perturb one of their coefficients
297+
Eigen::Vector3d perturbed_gyro_bias = gyro_bias;
298+
Eigen::Vector3d perturbed_accel_bias = accel_bias;
299+
if (i < 3) {
300+
perturbed_gyro_bias[i] += perturbation;
301+
} else {
302+
perturbed_accel_bias[i - 3] += perturbation;
303+
}
304+
305+
sym::PreintegratedImuMeasurements<double> perturbed_pim(perturbed_accel_bias,
306+
perturbed_gyro_bias);
307+
for (int k_ = 0; k_ < iterations; k_++) {
308+
perturbed_pim.IntegrateMeasurement(true_accel, true_gyro, accel_cov, gyro_cov, dt,
309+
sym::kDefaultEpsilond);
310+
}
311+
312+
numerical_state_D_bias.col(i).segment(0, 3) =
313+
inverse_perturbation * pim.DR.LocalCoordinates(perturbed_pim.DR);
314+
numerical_state_D_bias.col(i).segment(3, 3) =
315+
inverse_perturbation * (perturbed_pim.Dv - pim.Dv);
316+
numerical_state_D_bias.col(i).segment(6, 3) =
317+
inverse_perturbation * (perturbed_pim.Dp - pim.Dp);
318+
}
319+
320+
for (int col = 0; col < 6; col++) {
321+
for (int row = 0; row < 9; row++) {
322+
const double numerical_coef = numerical_state_D_bias(row, col);
323+
CHECK(std::abs(state_D_bias(row, col) - numerical_coef) <
324+
1e-1 * std::abs(numerical_coef) + 1e-10);
325+
}
326+
}
327+
// Compare
328+
// std::cout << "Claimed state_D_bias\n\n" << state_D_bias << std::endl;
329+
// std::cout << "numerical_state_D_bias\n\n" << numerical_state_D_bias << std::endl;
330+
// std::cout << "difference\n\n" << (state_D_bias + numerical_state_D_bias) << std::endl;
331+
}

0 commit comments

Comments
 (0)