My Project 1.0
Holistic Fusion: A Generic Framework for Multimodal Sensor Fusion
Loading...
Searching...
No Matches
test_graph_config.cpp
Go to the documentation of this file.
1#include <cmath>
2#include <iostream>
3#include <memory>
4#include <stdexcept>
5#include <string>
6
7#include <gtsam/slam/PriorFactor.h>
8
15
16namespace {
17
18class TestTransforms final : public holistic_fusion::StaticTransforms {
19 public:
20 TestTransforms() {
21 setImuFrame("imu");
22 setInitializationFrame("imu");
23 setBaseLinkFrame("imu");
24 setWorldFrame("world");
25 setOdomFrame("odom");
26 }
27
28 bool findTransformations() override { return true; }
29};
30
32 public:
34};
35
36void require(bool condition, const std::string& message) {
37 if (!condition) {
38 throw std::runtime_error(message);
39 }
40}
41
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;
51 try {
52 holistic_fusion::GraphManager manager(config, "imu", "world");
53 } catch (const std::invalid_argument& error) {
54 rejected = std::string(error.what()).find("relinearizeSkip") != std::string::npos;
55 }
56 require(rejected, "Invalid ISAM2 relinearizeSkip was accepted");
57 }
58 }
59}
60
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";
65 if (state_count) {
66 config->createStateEveryNthImuMeasurement_ = 0;
67 } else {
68 config->additionalOptimizationIterations_ = -1;
69 }
70 TestEstimator estimator;
71 bool rejected = false;
72 try {
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;
76 }
77 require(rejected, "Invalid " + key + " was accepted");
78 }
79}
80
81struct Covariances {
82 gtsam::Matrix66 pose;
83 gtsam::Matrix33 velocity;
84};
85
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;
92 holistic_fusion::GraphManager manager(config, "imu", "world");
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);
106 }
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()};
111}
112
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");
120}
121
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_;
127 holistic_fusion::GraphManager manager(config, "imu", "world");
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");
131
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");
138}
139
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;
144 if (nonzero_bias) {
145 config->accBiasPrior_ = Eigen::Vector3d(0.1, -0.2, 0.3);
146 config->gyroBiasPrior_ = Eigen::Vector3d(0.01, -0.02, 0.03);
147 }
148 holistic_fusion::GraphManager manager(config, "imu", "world");
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;
158 int index = 0;
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);
163 };
164 for (int step = 1; step <= 10 + partial_steps; ++step) {
165 add_imu(step == 10);
166 }
167
168 manager.updateGraph();
169
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");
178 };
179 for (int repeat = 0; repeat < 2; ++repeat) {
180 add_imu(false);
181 check_state();
182 manager.updateGraph();
183 }
184 add_imu(true);
185 const double next_key_time = 1.0 + 0.01 * index;
186 add_imu(false);
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");
190 add_imu(false);
191 check_state();
192
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);
199 }
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");
204}
205
206void closestImuLookupHandlesBufferBoundaries() {
207 auto config = std::make_shared<holistic_fusion::GraphConfig>();
208 config->useImuSignalLowPassFilter_ = false;
209 config->imuBufferLength_ = 3;
210 holistic_fusion::ImuBuffer buffer(config);
211 double timestamp = -1.0;
213 require(!buffer.getClosestImuMeasurement(timestamp, measurement, 1.0, 1.0), "Empty IMU lookup must fail");
214
215 const auto add = [&](double time) {
216 buffer.addToImuBuffer(time, Eigen::Vector3d::Constant(time), Eigen::Vector3d::Constant(-time));
217 };
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");
223 };
224
225 add(1.0);
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");
231
232 add(2.0);
233 add(3.0);
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");
243
244 add(4.0);
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");
248}
249
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 = [&]() {
269 timestamp += 0.1;
270 estimator.addCoreImuMeasurementAndGetState(Eigen::Vector3d(0.0, 0.0, 10.0), gyro_bias, timestamp,
271 state, optimized_state, measurements);
272 };
273 for (int index = 0; index < 20 && !estimator.areRollAndPitchInited(); ++index) {
274 add_imu();
275 }
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();
280 add_imu();
281 require(estimator.isGraphInited(), "Graph initialization failed");
282
283 add_imu();
284
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");
287}
288
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;
295
296 try {
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;
302 }
303
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");
306}
307
308void disabledMarginalWindowHandlesSingleState() {
309 auto config = std::make_shared<holistic_fusion::GraphConfig>();
310 config->useAdditionalSlowBatchSmootherFlag_ = true;
311 config->slowBatchSmootherUseIsamFlag_ = false;
312 config->useWindowForMarginalsComputationFlag_ = false;
313 holistic_fusion::OptimizerLMBatch optimizer(config);
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);
320
321 const auto covariance = optimizer.calculateMarginalCovarianceMatrixAtKey(0);
322
323 require(std::abs(covariance(0, 0) - 0.25) < 1e-8, "Whole-graph marginal covariance is incorrect");
324}
325
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);
333 holistic_fusion::GraphManager manager(config, "imu", "world");
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);
347 }
348 manager.updateGraph();
349
350 const auto result = manager.optimizeSlowBatchSmoother(10, false);
351
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");
361}
362
363void disabledBatchSmootherReturnsNoResult() {
364 auto config = std::make_shared<holistic_fusion::GraphConfig>();
365 config->useAdditionalSlowBatchSmootherFlag_ = false;
366 holistic_fusion::GraphManager manager(config, "imu", "world");
367
368 require(!manager.optimizeSlowBatchSmoother(10, false).has_value(), "Disabled batch smoother returned a result");
369}
370
371} // namespace
372
373int main() {
374 try {
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);
383 }
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';
392 return 1;
393 }
394 return 0;
395}
const Eigen::Vector3d & getI_v_W_I() const
Definition NavState.h:37
const Eigen::Isometry3d & getT_W_Ik() const
Definition NavState.h:36
int main()