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>
20void require(
bool condition,
const std::string& message) {
22 throw std::runtime_error(message);
26using gtsam::symbol_shorthand::R;
27using gtsam::symbol_shorthand::X;
30const gtsam::Rot3 kXAxisUp = gtsam::Rot3::Ry(-M_PI_2);
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)));
39double heading(
const gtsam::Rot3& R_W_S,
const gtsam::Rot3& R_W_Smeas) {
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));
51void headingOfMeasurementIsTakenInTheWorld() {
52 const gtsam::Rot3 R_I_S = gtsam::Rot3::Rx(M_PI);
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)));
57 const double headingValue = headingOfSensor(R_I_S, gtsam::Rot3::Rz(0.1) * kXAxisUp).value(values).theta();
59 require(std::abs(headingValue - 0.5) < 1e-9,
"Heading of S in W is wrong: " + std::to_string(headingValue));
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}) {
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()];
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();
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");
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) {
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);
96 const gtsam::ExpressionFactor<gtsam::Rot2> factor(gtsam::noiseModel::Isotropic::Sigma(1, 0.1), gtsam::Rot2(),
97 headingOfSensor(R_I_S, R_M_Smeas));
99 require(gtsam::internal::testFactorJacobians(
"heading", factor, values, 1e-7, 1e-5),
"Heading expression Jacobians are wrong");
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)));
110 const double error = factor.unwhitenedError(values)(0);
112 require(std::abs(std::abs(error) - 0.02) < 1e-9,
"Heading error does not wrap around pi: " + std::to_string(error));
119 headingIsYawDifferenceForEqualTilt();
120 headingOfMeasurementIsTakenInTheWorld();
121 tiltedFixedFrameOnlyConstrainsRotationAboutWorldZ();
122 jacobiansMatchNumericalDerivatives();
123 errorWrapsAroundPi();
124 }
catch (
const std::exception& error) {
125 std::cerr << error.what() <<
'\n';
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)