7#include <gtsam/slam/PriorFactor.h>
22 setInitializationFrame(
"imu");
23 setBaseLinkFrame(
"imu");
24 setWorldFrame(
"world");
28 bool findTransformations()
override {
return true; }
36void require(
bool condition,
const std::string& message) {
38 throw std::runtime_error(message);
42void rejectsInvalidIsamRelinearizationInterval() {
43 for (
const bool realtime : {
false,
true}) {
44 for (
const int skip : {0, -1}) {
45 auto config = std::make_shared<holistic_fusion::GraphConfig>();
46 config->realTimeSmootherUseIsamFlag_ = realtime;
47 config->useAdditionalSlowBatchSmootherFlag_ = !realtime;
48 config->slowBatchSmootherUseIsamFlag_ =
true;
49 config->relinearizeSkip_ = skip;
50 bool rejected =
false;
53 }
catch (
const std::invalid_argument& error) {
54 rejected = std::string(error.what()).find(
"relinearizeSkip") != std::string::npos;
56 require(rejected,
"Invalid ISAM2 relinearizeSkip was accepted");
61void rejectsInvalidStateAndOptimizationCounts() {
62 for (
const bool state_count : {
false,
true}) {
63 auto config = std::make_shared<holistic_fusion::GraphConfig>();
64 const std::string key = state_count ?
"createStateEveryNthImuMeasurement" :
"additionalOptimizationIterations";
66 config->createStateEveryNthImuMeasurement_ = 0;
68 config->additionalOptimizationIterations_ = -1;
70 TestEstimator estimator;
71 bool rejected =
false;
73 estimator.setup(config, std::make_shared<TestTransforms>());
74 }
catch (
const std::invalid_argument& error) {
75 rejected = std::string(error.what()).find(key) != std::string::npos;
77 require(rejected,
"Invalid " + key +
" was accepted");
83 gtsam::Matrix33 velocity;
86Covariances integratedCovariances(
double acc_noise,
double gyro_noise) {
87 auto config = std::make_shared<holistic_fusion::GraphConfig>();
88 config->useImuSignalLowPassFilter_ =
false;
89 config->realTimeSmootherUseCholeskyFactorizationFlag_ =
false;
90 config->accNoiseDensity_ = acc_noise;
91 config->gyroNoiseDensity_ = gyro_noise;
93 require(manager.initImuIntegrators(config->gravityMagnitude_),
"IMU integrator initialization failed");
94 require(manager.initPoseVelocityBiasGraph(1.0, gtsam::Pose3(), gtsam::Pose3(), config->gyroBiasPrior_),
95 "Prior graph initialization failed");
96 auto buffer = std::make_shared<holistic_fusion::ImuBuffer>(config);
97 const Eigen::Vector3d acceleration(0.0, 0.0, config->gravityMagnitude_);
98 buffer->addToImuBuffer(0.99, acceleration, Eigen::Vector3d::Zero());
99 buffer->addToImuBuffer(1.0, acceleration, Eigen::Vector3d::Zero());
101 std::shared_ptr<holistic_fusion::SafeNavStateWithCovarianceAndBias> optimized_state;
102 for (
int index = 1; index <= 10; ++index) {
103 const double timestamp = 1.0 + 0.01 * index;
104 buffer->addToImuBuffer(timestamp, acceleration, Eigen::Vector3d::Zero());
105 manager.addImuFactorAndGetState(state, optimized_state, buffer, timestamp, index == 10);
107 manager.updateGraph();
108 const auto& optimized = manager.getOptimizedGraphState();
109 require(std::abs(optimized.ts() - 1.1) < 1e-12,
"IMU factor was not optimized");
110 return {optimized.poseCovariance(), optimized.velocityCovariance()};
113void sensorNoiseChangesActualStateCovariance() {
114 const auto baseline = integratedCovariances(0.01, 0.02);
115 const auto acc = integratedCovariances(0.1, 0.02);
116 const auto gyro = integratedCovariances(0.01, 0.1);
117 require(acc.velocity.trace() > baseline.velocity.trace() + 1e-4,
"Acceleration noise has no covariance effect");
118 require(gyro.pose.topLeftCorner<3, 3>().trace() > baseline.pose.topLeftCorner<3, 3>().trace() + 1e-4,
119 "Gyroscope noise has no covariance effect");
122void initialGraphStateUsesMeasuredAngularVelocity() {
123 auto config = std::make_shared<holistic_fusion::GraphConfig>();
124 config->gyroBiasPrior_ = Eigen::Vector3d(0.01, -0.02, 0.03);
125 const Eigen::Vector3d measured_angular_velocity(0.04, 0.05, -0.06);
126 const Eigen::Vector3d expected = measured_angular_velocity - config->gyroBiasPrior_;
128 require(manager.initImuIntegrators(config->gravityMagnitude_),
"IMU integrator initialization failed");
129 require(manager.initPoseVelocityBiasGraph(1.0, gtsam::Pose3(), gtsam::Pose3(), measured_angular_velocity),
130 "Prior graph initialization failed");
132 require(manager.getOptimizedGraphState().angularVelocityCorrected().isApprox(expected, 1e-12),
133 "Initial angular velocity does not use the measured sample");
134 manager.updateGraph();
135 const auto& optimized = manager.getOptimizedGraphState();
136 require((optimized.angularVelocityCorrected() + optimized.imuBias().gyroscope()).isApprox(measured_angular_velocity, 1e-12),
137 "Initial graph-key angular velocity was replaced by the bias prior");
140void optimizerHandoffPreservesPartialImuInterval(
int partial_steps,
bool nonzero_bias) {
141 auto config = std::make_shared<holistic_fusion::GraphConfig>();
142 config->useImuSignalLowPassFilter_ =
false;
143 config->realTimeSmootherUseCholeskyFactorizationFlag_ =
false;
145 config->accBiasPrior_ = Eigen::Vector3d(0.1, -0.2, 0.3);
146 config->gyroBiasPrior_ = Eigen::Vector3d(0.01, -0.02, 0.03);
149 require(manager.initImuIntegrators(config->gravityMagnitude_),
"IMU integrator initialization failed");
150 require(manager.initPoseVelocityBiasGraph(1.0, gtsam::Pose3(), gtsam::Pose3(), config->gyroBiasPrior_),
151 "Prior graph initialization failed");
152 auto buffer = std::make_shared<holistic_fusion::ImuBuffer>(config);
153 const Eigen::Vector3d acceleration = Eigen::Vector3d(1.0, 0.0, config->gravityMagnitude_) + config->accBiasPrior_;
154 buffer->addToImuBuffer(0.99, acceleration, config->gyroBiasPrior_);
155 buffer->addToImuBuffer(1.0, acceleration, config->gyroBiasPrior_);
157 std::shared_ptr<holistic_fusion::SafeNavStateWithCovarianceAndBias> optimized_state;
159 const auto add_imu = [&](
bool create_state) {
160 const double timestamp = 1.0 + 0.01 * ++index;
161 buffer->addToImuBuffer(timestamp, acceleration, config->gyroBiasPrior_);
162 manager.addImuFactorAndGetState(state, optimized_state, buffer, timestamp, create_state);
164 for (
int step = 1; step <= 10 + partial_steps; ++step) {
168 manager.updateGraph();
170 require(std::abs(manager.getOptimizedGraphState().ts() - 1.1) < 1e-12,
171 "Optimized timestamp does not match its graph key");
172 const auto check_state = [&]() {
173 const double elapsed = 0.01 * index;
174 require(std::abs(state.
getT_W_Ik().translation().x() - 0.5 * elapsed * elapsed) < 1e-7,
175 "Optimizer handoff lost position integration");
176 require(std::abs(state.
getI_v_W_I().x() - elapsed) < 1e-7,
177 "Optimizer handoff lost velocity integration");
179 for (
int repeat = 0; repeat < 2; ++repeat) {
182 manager.updateGraph();
185 const double next_key_time = 1.0 + 0.01 * index;
187 manager.updateGraph();
188 require(std::abs(manager.getOptimizedGraphState().ts() - next_key_time) < 1e-12,
189 "Optimizer timestamp did not advance with the graph key");
193 const Eigen::Vector3d graph_angular_velocity = config->gyroBiasPrior_ + Eigen::Vector3d(0.0, 0.0, 0.1);
194 for (
int step = 1; step <= 2; ++step) {
195 const double timestamp = 1.0 + 0.01 * ++index;
196 const Eigen::Vector3d angular_velocity = config->gyroBiasPrior_ + Eigen::Vector3d(0.0, 0.0, 0.1 * step);
197 buffer->addToImuBuffer(timestamp, acceleration, angular_velocity);
198 manager.addImuFactorAndGetState(state, optimized_state, buffer, timestamp, step == 1);
200 manager.updateGraph();
201 const auto& optimized = manager.getOptimizedGraphState();
202 require((optimized.angularVelocityCorrected() + optimized.imuBias().gyroscope()).isApprox(graph_angular_velocity, 1e-12),
203 "Optimized angular velocity does not match its graph key");
206void closestImuLookupHandlesBufferBoundaries() {
207 auto config = std::make_shared<holistic_fusion::GraphConfig>();
208 config->useImuSignalLowPassFilter_ =
false;
209 config->imuBufferLength_ = 3;
211 double timestamp = -1.0;
213 require(!buffer.getClosestImuMeasurement(timestamp, measurement, 1.0, 1.0),
"Empty IMU lookup must fail");
215 const auto add = [&](
double time) {
216 buffer.addToImuBuffer(time, Eigen::Vector3d::Constant(time), Eigen::Vector3d::Constant(-time));
218 const auto expect = [&](
double query,
double deviation,
double expected) {
219 require(buffer.getClosestImuMeasurement(timestamp, measurement, deviation, query),
"Closest IMU lookup failed");
220 require(timestamp == expected && measurement.
timestamp == expected,
"Closest IMU timestamp is incorrect");
221 require(measurement.
acceleration.isApprox(Eigen::Vector3d::Constant(expected)),
"Closest IMU acceleration is incorrect");
222 require(measurement.
angularVelocity.isApprox(Eigen::Vector3d::Constant(-expected)),
"Closest IMU angular velocity is incorrect");
226 expect(1.0, 0.0, 1.0);
227 expect(0.75, 0.25, 1.0);
228 expect(1.25, 0.25, 1.0);
229 require(!buffer.getClosestImuMeasurement(timestamp, measurement, 0.125, 0.75),
"Early lookup outside tolerance must fail");
230 require(!buffer.getClosestImuMeasurement(timestamp, measurement, 0.125, 1.25),
"Late lookup outside tolerance must fail");
234 expect(1.0, 0.0, 1.0);
235 expect(2.0, 0.0, 2.0);
236 expect(3.0, 0.0, 3.0);
237 expect(0.75, 0.25, 1.0);
238 expect(3.25, 0.25, 3.0);
239 expect(1.25, 0.25, 1.0);
240 expect(1.75, 0.25, 2.0);
241 expect(1.5, 0.5, 2.0);
242 require(!buffer.getClosestImuMeasurement(timestamp, measurement, 0.125, 1.25),
"Interior lookup outside tolerance must fail");
245 expect(2.0, 0.0, 2.0);
246 expect(4.0, 0.0, 4.0);
247 require(!buffer.getClosestImuMeasurement(timestamp, measurement, 0.0, 1.0),
"Evicted IMU measurement must not be returned");
250void stationaryPropagationUsesInitializedGravity(
bool estimate_gravity) {
251 auto config = std::make_shared<holistic_fusion::GraphConfig>();
252 config->imuRate_ = 10.0;
253 config->imuBufferLength_ = 20;
254 config->useImuSignalLowPassFilter_ =
false;
255 config->staticAtStartup_ =
true;
256 config->gyroBiasPrior_ = Eigen::Vector3d::Ones();
257 config->estimateGravityFromImuFlag_ = estimate_gravity;
258 config->gravityMagnitude_ = estimate_gravity ? 9.81 : 10.0;
259 config->minOptimizationFrequency_ = 1e-9;
260 config->maxOptimizationFrequency_ = 1e-9;
261 TestEstimator estimator;
262 estimator.setup(config, std::make_shared<TestTransforms>());
263 std::shared_ptr<holistic_fusion::SafeIntegratedNavState> state;
264 std::shared_ptr<holistic_fusion::SafeNavStateWithCovarianceAndBias> optimized_state;
265 Eigen::Matrix<double, 6, 1> measurements;
266 const Eigen::Vector3d gyro_bias(0.01, -0.02, 0.03);
267 double timestamp = 1.0;
268 const auto add_imu = [&]() {
270 estimator.addCoreImuMeasurementAndGetState(Eigen::Vector3d(0.0, 0.0, 10.0), gyro_bias, timestamp,
271 state, optimized_state, measurements);
273 for (
int index = 0; index < 20 && !estimator.areRollAndPitchInited(); ++index) {
276 require(estimator.areRollAndPitchInited(),
"Stationary IMU alignment failed");
277 require(config->gyroBiasPrior_.isApprox(gyro_bias),
"Static initialization must use the measured gyro-bias mean");
278 require(estimator.initYawAndPositionInWorld(0.0, Eigen::Vector3d::Zero(),
"imu",
"imu"),
"Pose initialization failed");
279 estimator.pretendFirstMeasurementReceived();
281 require(estimator.isGraphInited(),
"Graph initialization failed");
285 require(state !=
nullptr,
"No propagated state returned");
286 require(state->getI_v_W_I().norm() < 1e-8,
"Stationary propagation uses a stale gravity vector");
289void rejectsInMotionInitialization() {
290 auto config = std::make_shared<holistic_fusion::GraphConfig>();
291 config->staticAtStartup_ =
false;
292 config->gyroBiasPrior_ = Eigen::Vector3d::Ones();
293 TestEstimator estimator;
294 bool rejected =
false;
297 estimator.setup(config, std::make_shared<TestTransforms>());
298 }
catch (
const std::logic_error& error) {
299 const std::string message = error.what();
300 rejected = message.find(
"static_at_startup") != std::string::npos &&
301 message.find(
"not implemented") != std::string::npos;
304 require(rejected,
"In-motion initialization must fail explicitly during setup");
305 require(config->gyroBiasPrior_.isZero(),
"In-motion initialization must select a zero gyro-bias mean");
308void disabledMarginalWindowHandlesSingleState() {
309 auto config = std::make_shared<holistic_fusion::GraphConfig>();
310 config->useAdditionalSlowBatchSmootherFlag_ =
true;
311 config->slowBatchSmootherUseIsamFlag_ =
false;
312 config->useWindowForMarginalsComputationFlag_ =
false;
314 gtsam::NonlinearFactorGraph factors;
315 factors.emplace_shared<gtsam::PriorFactor<double>>(0, 2.0, gtsam::noiseModel::Isotropic::Sigma(1, 0.5));
316 gtsam::Values values;
317 values.insert(0, 1.0);
318 optimizer.update(factors, values, {{0, 0.0}});
319 optimizer.optimize(10);
321 const auto covariance = optimizer.calculateMarginalCovarianceMatrixAtKey(0);
323 require(std::abs(covariance(0, 0) - 0.25) < 1e-8,
"Whole-graph marginal covariance is incorrect");
326void batchSmootherReturnsOptimizedNavStates() {
327 auto config = std::make_shared<holistic_fusion::GraphConfig>();
328 config->useImuSignalLowPassFilter_ =
false;
329 config->realTimeSmootherUseCholeskyFactorizationFlag_ =
false;
330 config->useAdditionalSlowBatchSmootherFlag_ =
true;
331 config->accBiasPrior_ = Eigen::Vector3d(0.1, -0.2, 0.3);
332 config->gyroBiasPrior_ = Eigen::Vector3d(0.01, -0.02, 0.03);
334 require(manager.initImuIntegrators(config->gravityMagnitude_),
"IMU integrator initialization failed");
335 require(manager.initPoseVelocityBiasGraph(1.0, gtsam::Pose3(), gtsam::Pose3(), config->gyroBiasPrior_),
336 "Prior graph initialization failed");
337 auto buffer = std::make_shared<holistic_fusion::ImuBuffer>(config);
338 const Eigen::Vector3d acceleration = Eigen::Vector3d(1.0, 0.0, config->gravityMagnitude_) + config->accBiasPrior_;
339 buffer->addToImuBuffer(0.99, acceleration, config->gyroBiasPrior_);
340 buffer->addToImuBuffer(1.0, acceleration, config->gyroBiasPrior_);
342 std::shared_ptr<holistic_fusion::SafeNavStateWithCovarianceAndBias> optimized_state;
343 for (
int index = 1; index <= 20; ++index) {
344 const double timestamp = 1.0 + 0.01 * index;
345 buffer->addToImuBuffer(timestamp, acceleration, config->gyroBiasPrior_);
346 manager.addImuFactorAndGetState(state, optimized_state, buffer, timestamp, index % 10 == 0);
348 manager.updateGraph();
350 const auto result = manager.optimizeSlowBatchSmoother(10,
false);
352 require(result.has_value(),
"Enabled batch smoother returned no result");
353 require(result->navStates.size() == 3,
"Batch result does not hold one nav state per graph state");
354 const auto& last = result->navStates.back();
355 require(std::abs(result->navStates[1].timeK - 1.1) < 1e-12 && std::abs(last.timeK - 1.2) < 1e-12,
356 "Batch nav states are not sorted by their graph-key time");
357 require(std::abs(last.T_W_Ik.translation().x() - 0.5 * 0.2 * 0.2) < 1e-6,
"Batch pose does not match the integrated motion");
358 require(std::abs(last.W_v_W_I.x() - 0.2) < 1e-6,
"Batch velocity does not match the integrated motion");
359 require(last.accelerometerBias.isApprox(config->accBiasPrior_, 1e-6) && last.gyroscopeBias.isApprox(config->gyroBiasPrior_, 1e-6),
360 "Batch bias does not belong to the nav state");
363void disabledBatchSmootherReturnsNoResult() {
364 auto config = std::make_shared<holistic_fusion::GraphConfig>();
365 config->useAdditionalSlowBatchSmootherFlag_ =
false;
368 require(!manager.optimizeSlowBatchSmoother(10,
false).has_value(),
"Disabled batch smoother returned a result");
375 rejectsInvalidIsamRelinearizationInterval();
376 rejectsInvalidStateAndOptimizationCounts();
377 rejectsInMotionInitialization();
378 sensorNoiseChangesActualStateCovariance();
379 initialGraphStateUsesMeasuredAngularVelocity();
380 for (
const bool nonzero_bias : {
false,
true}) {
381 optimizerHandoffPreservesPartialImuInterval(0, nonzero_bias);
382 optimizerHandoffPreservesPartialImuInterval(7, nonzero_bias);
384 closestImuLookupHandlesBufferBoundaries();
385 stationaryPropagationUsesInitializedGravity(
false);
386 stationaryPropagationUsesInitializedGravity(
true);
387 disabledMarginalWindowHandlesSingleState();
388 batchSmootherReturnsOptimizedNavStates();
389 disabledBatchSmootherReturnsNoResult();
390 }
catch (
const std::exception& error) {
391 std::cerr << error.what() <<
'\n';
void pretendFirstMeasurementReceived()
const Eigen::Vector3d & getI_v_W_I() const
const Eigen::Isometry3d & getT_W_Ik() const
Eigen::Vector3d acceleration
Eigen::Vector3d angularVelocity