|
| 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 <numeric> |
| 7 | +#include <random> |
| 8 | +#include <thread> |
| 9 | + |
| 10 | +#include <Eigen/Dense> |
| 11 | +#include <catch2/catch_test_macros.hpp> |
| 12 | + |
| 13 | +#include <sym/factors/internal/imu_manifold_preintegration_update.h> |
| 14 | +#include <sym/rot3.h> |
| 15 | +#include <sym/util/epsilon.h> |
| 16 | +#include <symforce/slam/imu_preintegration/imu_preintegrator.h> |
| 17 | + |
| 18 | +using Eigen::Ref; |
| 19 | +using Eigen::Vector3d; |
| 20 | +using sym::Rot3d; |
| 21 | + |
| 22 | +/** |
| 23 | + * Calculates what ImuManifoldPreintegrationUpdate should for the changes in orientation, |
| 24 | + * velocity, and position. |
| 25 | + */ |
| 26 | +void UpdateState(const Rot3d& DR, const Ref<const Vector3d>& Dv, const Ref<const Vector3d>& Dp, |
| 27 | + const Ref<const Vector3d>& gyro, const Ref<const Vector3d>& accel, |
| 28 | + const Ref<const Vector3d>& gyro_bias, const Ref<const Vector3d>& accel_bias, |
| 29 | + const double dt, const double epsilon, Rot3d& new_DR, Ref<Vector3d> new_Dv, |
| 30 | + Ref<Vector3d> new_Dp) { |
| 31 | + const auto corrected_accel = accel - accel_bias; |
| 32 | + new_DR = DR * Rot3d::FromTangent((gyro - gyro_bias) * dt, epsilon); |
| 33 | + new_Dv = Dv + DR * corrected_accel * dt; |
| 34 | + new_Dp = Dp + Dv * dt + DR * corrected_accel * (0.5 * dt * dt); |
| 35 | +} |
| 36 | + |
| 37 | +TEST_CASE("Test ImuManifoldPreintegrationUpdate has correct DR, Dv, & Dp", "[slam]") { |
| 38 | + std::mt19937 gen(1804); |
| 39 | + std::normal_distribution<double> dist{0.0, 10.0}; |
| 40 | + const auto randomM3 = Eigen::Matrix3d::NullaryExpr([&]() { return dist(gen); }); |
| 41 | + const auto randomV3 = Eigen::Vector3d::NullaryExpr([&]() { return dist(gen); }); |
| 42 | + const auto randomM9 = Eigen::Matrix<double, 9, 9>::NullaryExpr([&]() { return dist(gen); }); |
| 43 | + |
| 44 | + for (int i_ = 0; i_ < 10; i_++) { |
| 45 | + const sym::Rot3d DR = sym::Rot3d::Random(gen); |
| 46 | + const Eigen::Vector3d Dv = randomV3; |
| 47 | + const Eigen::Vector3d Dp = randomV3; |
| 48 | + const Eigen::Matrix<double, 9, 9> covariance = randomM9; |
| 49 | + const Eigen::Matrix3d DR_D_gyro_bias = randomM3; |
| 50 | + const Eigen::Matrix3d Dv_D_gyro_bias = randomM3; |
| 51 | + const Eigen::Matrix3d Dv_D_accel_bias = randomM3; |
| 52 | + const Eigen::Matrix3d Dp_D_gyro_bias = randomM3; |
| 53 | + const Eigen::Matrix3d Dp_D_accel_bias = randomM3; |
| 54 | + |
| 55 | + const Eigen::Vector3d gyro = randomV3; |
| 56 | + const Eigen::Vector3d accel = randomV3; |
| 57 | + |
| 58 | + const double dt = 1.24; |
| 59 | + |
| 60 | + const Eigen::Vector3d gyro_bias = randomV3; |
| 61 | + const Eigen::Vector3d accel_bias = randomV3; |
| 62 | + const Eigen::Vector3d gyro_cov = randomV3; |
| 63 | + const Eigen::Vector3d accel_cov = randomV3; |
| 64 | + |
| 65 | + sym::Rot3d new_DR; |
| 66 | + Eigen::Vector3d new_Dv; |
| 67 | + Eigen::Vector3d new_Dp; |
| 68 | + Eigen::Matrix<double, 9, 9> new_covarinace; |
| 69 | + Eigen::Matrix3d new_DR_D_gyro_bias; |
| 70 | + Eigen::Matrix3d new_Dp_D_gyro_bias; |
| 71 | + Eigen::Matrix3d new_Dp_D_accel_bias; |
| 72 | + Eigen::Matrix3d new_Dv_D_gyro_bias; |
| 73 | + Eigen::Matrix3d new_Dv_D_accel_bias; |
| 74 | + |
| 75 | + sym::ImuManifoldPreintegrationUpdate<double>( |
| 76 | + DR, Dv, Dp, covariance, DR_D_gyro_bias, Dv_D_accel_bias, Dv_D_gyro_bias, Dp_D_accel_bias, |
| 77 | + Dp_D_gyro_bias, accel_bias, gyro_bias, accel_cov, gyro_cov, accel, gyro, dt, |
| 78 | + sym::kDefaultEpsilond, &new_DR, &new_Dv, &new_Dp, nullptr, nullptr, nullptr, nullptr, |
| 79 | + nullptr, nullptr); |
| 80 | + |
| 81 | + sym::Rot3d expected_DR; |
| 82 | + Eigen::Vector3d expected_Dv; |
| 83 | + Eigen::Vector3d expected_Dp; |
| 84 | + |
| 85 | + UpdateState(DR, Dv, Dp, gyro, accel, gyro_bias, accel_bias, dt, sym::kDefaultEpsilond, |
| 86 | + expected_DR, expected_Dv, expected_Dp); |
| 87 | + |
| 88 | + CHECK(sym::IsClose(new_DR, expected_DR, 1e-14)); |
| 89 | + CHECK(sym::IsClose(new_Dv, expected_Dv, 1e-14)); |
| 90 | + CHECK(sym::IsClose(new_Dp, expected_Dp, 1e-14)); |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +/** |
| 95 | + * Helper class to generate samples from a multi-variate normal distribution with |
| 96 | + * the same covariance as that passed into the constructor. |
| 97 | + */ |
| 98 | +class MultiVarNormalDist { |
| 99 | + private: |
| 100 | + const Eigen::MatrixXd L; |
| 101 | + std::normal_distribution<double> dist; |
| 102 | + |
| 103 | + public: |
| 104 | + /** |
| 105 | + * covar is the covariance of the desired normal distribution. |
| 106 | + * Precondition: covar is a symmetric, positive definite matrix. |
| 107 | + */ |
| 108 | + MultiVarNormalDist(const Eigen::MatrixXd covar) : L{covar.llt().matrixL()}, dist{} {} |
| 109 | + |
| 110 | + /** |
| 111 | + * Returns a matrix whose (count) colums are samples from the distribution. |
| 112 | + */ |
| 113 | + template <typename Generator> |
| 114 | + Eigen::MatrixXd Sample(Generator& gen, const int count) { |
| 115 | + Eigen::MatrixXd rand = |
| 116 | + Eigen::MatrixXd::NullaryExpr(L.cols(), count, [&]() { return dist(gen); }); |
| 117 | + return L * rand; |
| 118 | + } |
| 119 | +}; |
| 120 | + |
| 121 | +/** |
| 122 | + * Calculates the covariance of the columns of samples. |
| 123 | + */ |
| 124 | +Eigen::MatrixXd Covariance(const Eigen::MatrixXd& samples) { |
| 125 | + const long int sample_size = samples.cols(); |
| 126 | + const Eigen::VectorXd avg_col = samples.rowwise().sum() / sample_size; |
| 127 | + const Eigen::MatrixXd meanless_samples = samples.colwise() - avg_col; |
| 128 | + return (meanless_samples * meanless_samples.transpose()) / (sample_size - 1); |
| 129 | +} |
| 130 | + |
| 131 | +TEST_CASE("Test MultiVarNormalDist generates samples with correct covariance", "[slam]") { |
| 132 | + std::mt19937 gen(170); |
| 133 | + |
| 134 | + // Create covar, a symmetric positive definite matrix |
| 135 | + // NOTE(brad): This matrix is positive semi-definite because the diagonal entries all |
| 136 | + // have values of at least 2.0, and the max value of an off-diagonal entry is .2. Thus |
| 137 | + // within a row, the sum of the non-diagonal entries is capped by 0.2 * 8 = 1.6. Thus by the |
| 138 | + // Gershgorin circle theorem, any eigen value of covar is at least 0.4 (i.e., positive). |
| 139 | + std::uniform_real_distribution<double> uniform_dist(0.01, 0.1); |
| 140 | + Eigen::MatrixXd covar = Eigen::MatrixXd::NullaryExpr(9, 9, [&]() { return uniform_dist(gen); }); |
| 141 | + covar += covar.transpose().eval(); |
| 142 | + covar.diagonal().array() += 2.0; |
| 143 | + |
| 144 | + MultiVarNormalDist dist(covar); |
| 145 | + |
| 146 | + const Eigen::MatrixXd samples = dist.Sample(gen, 1 << 23); |
| 147 | + const Eigen::Matrix<double, 9, 9> calculated_covar = Covariance(samples); |
| 148 | + |
| 149 | + for (int col = 0; col < 9; col++) { |
| 150 | + for (int row = 0; row < 9; row++) { |
| 151 | + const double covar_rc = covar(row, col); |
| 152 | + const double difference = covar_rc - calculated_covar(row, col); |
| 153 | + // NOTE(brad): trade-off between sample count (time) and accuracy of calculated_covar |
| 154 | + CHECK(difference * difference < 9e-3 * covar_rc * covar_rc); |
| 155 | + } |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +namespace example { |
| 160 | +static const Eigen::Vector3d accel_bias = {3.4, 1.6, -5.9}; |
| 161 | +static const Eigen::Vector3d gyro_bias = {1.2, -2.4, 0.5}; |
| 162 | +static const Eigen::Vector3d accel_cov(7e-5, 7e-5, 7e-5); |
| 163 | +static const Eigen::Vector3d gyro_cov(1e-3, 1e-3, 1e-3); |
| 164 | +static const Eigen::Vector3d true_gyro = Eigen::Vector3d::Constant(10.2); |
| 165 | +static const Eigen::Vector3d true_accel = Eigen::Vector3d::Constant(4.3); |
| 166 | +} // namespace example |
| 167 | + |
| 168 | +TEST_CASE("Test ImuPreintegator.covariance", "[slam]") { |
| 169 | + // In order to test that the computed covariance is correct, we sample the values many |
| 170 | + // time to numerically calculate the covariance to compare. |
| 171 | + using M99 = Eigen::Matrix<double, 9, 9>; |
| 172 | + // Parameters |
| 173 | + const double dt = 1e-4; |
| 174 | + const int measurements_per_sample = 130; |
| 175 | + const int sample_count = 1 << 19; |
| 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(M99::Zero()); |
| 185 | + |
| 186 | + Eigen::MatrixXd samples(9, sample_count); |
| 187 | + |
| 188 | + sym::ImuPreintegrator<double> noiseless_integrator(example::accel_bias, example::gyro_bias); |
| 189 | + for (int k = 0; k < measurements_per_sample; k++) { |
| 190 | + noiseless_integrator.IntegrateMeasurement( |
| 191 | + example::accel_bias + example::true_accel, example::gyro_bias + example::true_gyro, |
| 192 | + example::accel_cov, example::gyro_cov, dt, sym::kDefaultEpsilond); |
| 193 | + } |
| 194 | + |
| 195 | + for (int i = 0; i < thread_count; i++) { |
| 196 | + threads.emplace_back([&samples, &covariance_sums, &noiseless_integrator, dt, i]() { |
| 197 | + std::mt19937 gen(1216 + i); |
| 198 | + // Each thread needs its own distribution as they are not thread safe |
| 199 | + MultiVarNormalDist accel_dist(example::accel_cov.asDiagonal() * (1 / dt)); |
| 200 | + MultiVarNormalDist gyro_dist(example::gyro_cov.asDiagonal() * (1 / dt)); |
| 201 | + |
| 202 | + Eigen::MatrixXd accel_noise; |
| 203 | + Eigen::MatrixXd gyro_noise; |
| 204 | + |
| 205 | + for (int j = steps_per_thread * i; j < steps_per_thread * (i + 1); j++) { |
| 206 | + sym::ImuPreintegrator<double> integrator(example::accel_bias, example::gyro_bias); |
| 207 | + |
| 208 | + gyro_noise = gyro_dist.Sample(gen, measurements_per_sample); |
| 209 | + accel_noise = accel_dist.Sample(gen, measurements_per_sample); |
| 210 | + |
| 211 | + for (int k = 0; k < measurements_per_sample; k++) { |
| 212 | + integrator.IntegrateMeasurement( |
| 213 | + example::accel_bias + example::true_accel + accel_noise.col(k), |
| 214 | + example::gyro_bias + example::true_gyro + gyro_noise.col(k), example::accel_cov, |
| 215 | + example::gyro_cov, dt, sym::kDefaultEpsilond); |
| 216 | + } |
| 217 | + |
| 218 | + samples.col(j).segment(0, 3) = |
| 219 | + noiseless_integrator.PreintegratedMeasurements().DR.LocalCoordinates( |
| 220 | + integrator.PreintegratedMeasurements().DR); |
| 221 | + samples.col(j).segment(3, 3) = integrator.PreintegratedMeasurements().Dv; |
| 222 | + samples.col(j).segment(6, 3) = integrator.PreintegratedMeasurements().Dp; |
| 223 | + |
| 224 | + covariance_sums[i] += integrator.Covariance(); |
| 225 | + } |
| 226 | + }); |
| 227 | + } |
| 228 | + for (std::thread& t : threads) { |
| 229 | + t.join(); |
| 230 | + } |
| 231 | + |
| 232 | + const M99 calculated_covariance = |
| 233 | + (1.0 / sample_count) * |
| 234 | + std::accumulate(covariance_sums.begin(), covariance_sums.end(), M99::Zero().eval()); |
| 235 | + |
| 236 | + const Eigen::MatrixXd sampled_covariance = Covariance(samples); |
| 237 | + const double sampled_covariance_max = sampled_covariance.array().abs().maxCoeff(); |
| 238 | + |
| 239 | + const Eigen::MatrixXd weighted_relative_error = sampled_covariance.binaryExpr( |
| 240 | + calculated_covariance, [=](const double x, const double y) -> double { |
| 241 | + // NOTE(brad): 3e-2 is partially arbitrary. Seemed like a reasonable value |
| 242 | + return std::abs((x - y) / (std::abs(x) + 3e-2 * sampled_covariance_max)); |
| 243 | + }); |
| 244 | + |
| 245 | + // NOTE(brad): 0.05 is also somewhat arbitrary. Seemed reasonable. |
| 246 | + CHECK(weighted_relative_error.maxCoeff() < 0.05); |
| 247 | +} |
| 248 | + |
| 249 | +TEST_CASE("Test preintegrated derivatives wrt IMU biases", "[slam]") { |
| 250 | + using M33 = Eigen::Matrix<double, 3, 3>; |
| 251 | + using M96 = Eigen::Matrix<double, 9, 6>; |
| 252 | + |
| 253 | + const double dt = 1e-3; |
| 254 | + const int iterations = 100; |
| 255 | + |
| 256 | + sym::ImuPreintegrator<double> integrator(example::accel_bias, example::gyro_bias); |
| 257 | + |
| 258 | + for (int k_ = 0; k_ < iterations; k_++) { |
| 259 | + integrator.IntegrateMeasurement(example::accel_bias + example::true_accel, |
| 260 | + example::gyro_bias + example::true_gyro, example::accel_cov, |
| 261 | + example::gyro_cov, dt, sym::kDefaultEpsilond); |
| 262 | + } |
| 263 | + |
| 264 | + Eigen::Matrix<double, 9, 6> state_D_bias = |
| 265 | + (Eigen::Matrix<double, 9, 6>() << integrator.PreintegratedMeasurements().DR_D_gyro_bias, |
| 266 | + M33::Zero(), integrator.PreintegratedMeasurements().Dv_D_gyro_bias, |
| 267 | + integrator.PreintegratedMeasurements().Dv_D_accel_bias, |
| 268 | + integrator.PreintegratedMeasurements().Dp_D_gyro_bias, |
| 269 | + integrator.PreintegratedMeasurements().Dp_D_accel_bias) |
| 270 | + .finished(); |
| 271 | + |
| 272 | + // Perturb inputs and calculate derivatives numerically from them |
| 273 | + const double perturbation = 1e-5; |
| 274 | + M96 numerical_state_D_bias; |
| 275 | + for (int i = 0; i < 6; i++) { |
| 276 | + // Create biases and perturb one of their coefficients |
| 277 | + Eigen::Vector3d perturbed_gyro_bias = example::gyro_bias; |
| 278 | + Eigen::Vector3d perturbed_accel_bias = example::accel_bias; |
| 279 | + if (i < 3) { |
| 280 | + perturbed_gyro_bias[i] += perturbation; |
| 281 | + } else { |
| 282 | + perturbed_accel_bias[i - 3] += perturbation; |
| 283 | + } |
| 284 | + |
| 285 | + sym::ImuPreintegrator<double> perturbed_integrator(perturbed_accel_bias, perturbed_gyro_bias); |
| 286 | + for (int k_ = 0; k_ < iterations; k_++) { |
| 287 | + perturbed_integrator.IntegrateMeasurement( |
| 288 | + example::accel_bias + example::true_accel, example::gyro_bias + example::true_gyro, |
| 289 | + example::accel_cov, example::gyro_cov, dt, sym::kDefaultEpsilond); |
| 290 | + } |
| 291 | + |
| 292 | + numerical_state_D_bias.col(i).segment(0, 3) = |
| 293 | + integrator.PreintegratedMeasurements().DR.LocalCoordinates( |
| 294 | + perturbed_integrator.PreintegratedMeasurements().DR) / |
| 295 | + perturbation; |
| 296 | + numerical_state_D_bias.col(i).segment(3, 3) = |
| 297 | + (perturbed_integrator.PreintegratedMeasurements().Dv - |
| 298 | + integrator.PreintegratedMeasurements().Dv) / |
| 299 | + perturbation; |
| 300 | + numerical_state_D_bias.col(i).segment(6, 3) = |
| 301 | + (perturbed_integrator.PreintegratedMeasurements().Dp - |
| 302 | + integrator.PreintegratedMeasurements().Dp) / |
| 303 | + perturbation; |
| 304 | + } |
| 305 | + |
| 306 | + const Eigen::MatrixXd relative_error = |
| 307 | + numerical_state_D_bias.binaryExpr(state_D_bias, [](const double x, const double y) -> double { |
| 308 | + return std::abs((x - y) / (std::abs(x) + 1e-10)); |
| 309 | + }); |
| 310 | + |
| 311 | + CHECK(relative_error.maxCoeff() < 0.05); |
| 312 | +} |
0 commit comments