My Project 1.0
Holistic Fusion: A Generic Framework for Multimodal Sensor Fusion
Loading...
Searching...
No Matches
test_heading_expression.cpp
Go to the documentation of this file.
1#include <algorithm>
2#include <cmath>
3#include <iostream>
4#include <stdexcept>
5#include <string>
6#include <vector>
7
8#include <gtsam/geometry/Pose3.h>
9#include <gtsam/geometry/Rot2.h>
10#include <gtsam/inference/Symbol.h>
11#include <gtsam/nonlinear/ExpressionFactor.h>
12#include <gtsam/nonlinear/Values.h>
13#include <gtsam/nonlinear/expressionTesting.h>
14#include <gtsam/slam/expressions.h>
15
17
18namespace {
19
20void require(bool condition, const std::string& message) {
21 if (!condition) {
22 throw std::runtime_error(message);
23 }
24}
25
26using gtsam::symbol_shorthand::R;
27using gtsam::symbol_shorthand::X;
28
29// Rotation with the x-axis along z, where the Euler yaw is undefined
30const gtsam::Rot3 kXAxisUp = gtsam::Rot3::Ry(-M_PI_2);
31
32// Heading of S, with R_W_S = R_W_I * R_I_S, relative to the measured R_M_Smeas brought to the world through R_W_M
33gtsam::Expression<gtsam::Rot2> headingOfSensor(const gtsam::Rot3& R_I_S, const gtsam::Rot3& R_M_Smeas) {
34 const gtsam::Rot3_ R_W_I = gtsam::rotation(gtsam::Pose3_(X(0)));
35 const gtsam::Rot3_ R_W_M = gtsam::rotation(gtsam::Pose3_(R(0)));
36 return holistic_fusion::headingInWorld(R_W_I * gtsam::Rot3_(R_I_S), R_W_M, R_M_Smeas);
37}
38
39double heading(const gtsam::Rot3& R_W_S, const gtsam::Rot3& R_W_Smeas) {
40 return holistic_fusion::worldRotationErrorAsHeadingError(R_W_S * R_W_Smeas.inverse(), {}).theta();
41}
42
43void headingIsYawDifferenceForEqualTilt() {
44 const std::vector<gtsam::Rot3> tilts{gtsam::Rot3(), gtsam::Rot3::Ry(0.3) * gtsam::Rot3::Rx(-0.2), kXAxisUp, gtsam::Rot3::Rx(M_PI_2)};
45 for (const gtsam::Rot3& tilt : tilts) {
46 const double headingValue = heading(gtsam::Rot3::Rz(0.9) * tilt, gtsam::Rot3::Rz(0.2) * tilt);
47 require(std::abs(headingValue - 0.7) < 1e-9, "Heading is not the yaw difference: " + std::to_string(headingValue));
48 }
49}
50
51void headingOfMeasurementIsTakenInTheWorld() {
52 const gtsam::Rot3 R_I_S = gtsam::Rot3::Rx(M_PI);
53 gtsam::Values values;
54 values.insert(X(0), gtsam::Pose3(gtsam::Rot3::Rz(0.9) * kXAxisUp * R_I_S.inverse(), gtsam::Point3(1.0, 2.0, 3.0)));
55 values.insert(R(0), gtsam::Pose3(gtsam::Rot3::Rz(0.3), gtsam::Point3(-1.0, 0.5, 0.0)));
56
57 const double headingValue = headingOfSensor(R_I_S, gtsam::Rot3::Rz(0.1) * kXAxisUp).value(values).theta();
58
59 require(std::abs(headingValue - 0.5) < 1e-9, "Heading of S in W is wrong: " + std::to_string(headingValue));
60}
61
62// With a tilted fixed frame M, the factor must pull only about the z-axis of the world, for the state and for the alignment R_W_M
63void tiltedFixedFrameOnlyConstrainsRotationAboutWorldZ() {
64 const gtsam::Rot3 R_W_M = gtsam::Rot3::RzRyRx(0.3, -0.25, 0.35);
65 for (const gtsam::Rot3& R_W_S : {gtsam::Rot3::RzRyRx(0.4, -0.3, 0.2), gtsam::Rot3::Rz(1.1) * kXAxisUp}) {
66 gtsam::Values values;
67 values.insert(X(0), gtsam::Pose3(R_W_S, gtsam::Point3(0.5, -1.0, 0.2)));
68 values.insert(R(0), gtsam::Pose3(R_W_M, gtsam::Point3(0.1, 0.2, 0.3)));
69 const gtsam::ExpressionFactor<gtsam::Rot2> factor(gtsam::noiseModel::Isotropic::Sigma(1, 1.0), gtsam::Rot2(),
70 headingOfSensor(gtsam::Rot3(), R_W_M.inverse() * R_W_S));
71 std::vector<gtsam::Matrix> H(2);
72 factor.unwhitenedError(values, H);
73 const gtsam::KeyVector& keys = factor.keys();
74 const gtsam::Matrix& H_state = H[std::find(keys.begin(), keys.end(), X(0)) - keys.begin()];
75 const gtsam::Matrix& H_alignment = H[std::find(keys.begin(), keys.end(), R(0)) - keys.begin()];
76
77 // A rotation about axis a of W is the right perturbation R_W_X^T * a of R_W_X
78 const gtsam::Matrix13 H_state_aboutAxesOfW = H_state.leftCols<3>() * R_W_S.matrix().transpose();
79 const gtsam::Matrix13 H_alignment_aboutAxesOfW = H_alignment.leftCols<3>() * R_W_M.matrix().transpose();
80
81 require(H_state_aboutAxesOfW.isApprox(gtsam::Matrix13(0.0, 0.0, 1.0), 1e-9), "State is not pulled only about z of the world");
82 require(H_alignment_aboutAxesOfW.isApprox(gtsam::Matrix13(0.0, 0.0, -1.0), 1e-9), "Alignment is not pulled only about z of the world");
83 require(H_state.rightCols<3>().isZero(1e-12) && H_alignment.rightCols<3>().isZero(1e-12), "Heading depends on a position");
84 }
85}
86
87void jacobiansMatchNumericalDerivatives() {
88 const gtsam::Rot3 R_I_S = gtsam::Rot3::RzRyRx(2.9, -0.3, 0.4);
89 const std::vector<gtsam::Rot3> sensorAttitudes{gtsam::Rot3::RzRyRx(0.2, 0.1, -2.5), gtsam::Rot3::Rz(0.7) * kXAxisUp};
90 for (const gtsam::Rot3& R_W_S : sensorAttitudes) {
91 gtsam::Values values;
92 values.insert(X(0), gtsam::Pose3(R_W_S * R_I_S.inverse(), gtsam::Point3(0.5, -1.0, 0.2)));
93 values.insert(R(0), gtsam::Pose3(gtsam::Rot3::RzRyRx(0.05, -0.02, 0.4), gtsam::Point3(0.1, 0.2, 0.3)));
94 const gtsam::Rot3 R_M_Smeas = gtsam::Rot3::Rz(-1.2) * R_W_S * gtsam::Rot3::Rx(0.1);
95
96 const gtsam::ExpressionFactor<gtsam::Rot2> factor(gtsam::noiseModel::Isotropic::Sigma(1, 0.1), gtsam::Rot2(),
97 headingOfSensor(R_I_S, R_M_Smeas));
98
99 require(gtsam::internal::testFactorJacobians("heading", factor, values, 1e-7, 1e-5), "Heading expression Jacobians are wrong");
100 }
101}
102
103void errorWrapsAroundPi() {
104 gtsam::Values values;
105 values.insert(X(0), gtsam::Pose3(gtsam::Rot3::Rz(-M_PI + 0.01), gtsam::Point3()));
106 values.insert(R(0), gtsam::Pose3());
107 const gtsam::ExpressionFactor<gtsam::Rot2> factor(gtsam::noiseModel::Isotropic::Sigma(1, 1.0), gtsam::Rot2(),
108 headingOfSensor(gtsam::Rot3(), gtsam::Rot3::Rz(M_PI - 0.01)));
109
110 const double error = factor.unwhitenedError(values)(0);
111
112 require(std::abs(std::abs(error) - 0.02) < 1e-9, "Heading error does not wrap around pi: " + std::to_string(error));
113}
114
115} // namespace
116
117int main() {
118 try {
119 headingIsYawDifferenceForEqualTilt();
120 headingOfMeasurementIsTakenInTheWorld();
121 tiltedFixedFrameOnlyConstrainsRotationAboutWorldZ();
122 jacobiansMatchNumericalDerivatives();
123 errorWrapsAroundPi();
124 } catch (const std::exception& error) {
125 std::cerr << error.what() << '\n';
126 return 1;
127 }
128 return 0;
129}
gtsam::Rot2 worldRotationErrorAsHeadingError(const gtsam::Rot3 &R_W_Wmeas, gtsam::OptionalJacobian< 1, 3 > H)
gtsam::Expression< gtsam::Rot2 > headingInWorld(const gtsam::Rot3_ &exp_R_W_S, const gtsam::Rot3_ &exp_R_W_M, const gtsam::Rot3 &R_M_Smeas)