Fixing 4.3.1-ros gtsam compatibility (#1783)

* Fixing 4.3.1-ros gtsam compatibility

* javobian fix in SwitchVariable

* Fix GTSAM scalar switch traits, Jacobians and attitude API detection

* Separate sigmoid factor correction from GTSAM compatibility

* Correct sigmoid switch Jacobian and add focused factor regression

* combined gtsam tests in same file

* cmake refactor

---------

Co-authored-by: Frank Dellaert <[email protected]>
This commit is contained in:
matlabbe
2026-09-30 21:03:04 -07:00
committed by GitHub
co-authored by Frank Dellaert
parent 0f98c63e9d
commit 0156bc22ed
8 changed files with 352 additions and 74 deletions
+2 -8
View File
@@ -620,14 +620,7 @@ IF(WITH_GTSAM)
# Force config mode to ignore PCL's findGTSAM.cmake file # Force config mode to ignore PCL's findGTSAM.cmake file
FIND_PACKAGE(GTSAM CONFIG QUIET) FIND_PACKAGE(GTSAM CONFIG QUIET)
IF(GTSAM_FOUND) IF(GTSAM_FOUND)
# For issue https://github.com/introlab/rtabmap/pull/1626 INCLUDE(${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules/CheckGTSAMFeatures.cmake)
FIND_FILE(GTSAM_NOISE_MODEL_FACTOR_N_FILE gtsam/nonlinear/NoiseModelFactorN.h
PATHS ${GTSAM_INCLUDE_DIR}
NO_DEFAULT_PATH)
IF(GTSAM_NOISE_MODEL_FACTOR_N_FILE)
MESSAGE(STATUS "GTSAM with NoiseModelFactorN.h")
ADD_DEFINITIONS("-DGTSAM_WITH_NOISE_MODEL_FACTOR_N")
ENDIF(GTSAM_NOISE_MODEL_FACTOR_N_FILE)
ENDIF(GTSAM_FOUND) ENDIF(GTSAM_FOUND)
ENDIF(WITH_GTSAM) ENDIF(WITH_GTSAM)
@@ -1064,6 +1057,7 @@ IF(NOT MSVC)
ENDIF() ENDIF()
####### OSX BUNDLE CMAKE_INSTALL_PREFIX ####### ####### OSX BUNDLE CMAKE_INSTALL_PREFIX #######
IF(APPLE AND BUILD_AS_BUNDLE) IF(APPLE AND BUILD_AS_BUNDLE)
IF(Qt6_FOUND OR Qt5_FOUND OR (QT4_FOUND AND QT_QTCORE_FOUND AND QT_QTGUI_FOUND)) IF(Qt6_FOUND OR Qt5_FOUND OR (QT4_FOUND AND QT_QTCORE_FOUND AND QT_QTGUI_FOUND))
+27
View File
@@ -0,0 +1,27 @@
# Detects GTSAM API variations that the version number alone can't tell
# apart. Included after FIND_PACKAGE(GTSAM) succeeded.
# Pose3AttitudeFactor has been replaced by AttitudeFactor<Pose3> in 4.3, but
# the 4.3 ROS snapshots share a numeric version (4.3.0) while exposing either
# API, so probe which one compiles. Older versions only have
# Pose3AttitudeFactor. The probe links the imported gtsam target, so it
# inherits GTSAM's usage requirements (including cxx_std_17 for 4.3) and
# doesn't depend on the C++ standard selected later in the main CMakeLists.txt.
IF(GTSAM_VERSION VERSION_GREATER_EQUAL "4.3.0")
INCLUDE(CheckCXXSourceCompiles)
INCLUDE(CMakePushCheckState)
CMAKE_PUSH_CHECK_STATE(RESET)
SET(CMAKE_REQUIRED_LIBRARIES gtsam)
UNSET(RTABMAP_GTSAM_HAS_ATTITUDE_FACTOR_TEMPLATE CACHE)
CHECK_CXX_SOURCE_COMPILES("
#include <gtsam/navigation/AttitudeFactor.h>
int main() {
gtsam::AttitudeFactor<gtsam::Pose3> factor(1, gtsam::Unit3(0,0,1),
gtsam::noiseModel::Isotropic::Sigma(2, 1.0));
return factor.evaluateError(gtsam::Pose3()).size() != 2;
}" RTABMAP_GTSAM_HAS_ATTITUDE_FACTOR_TEMPLATE)
CMAKE_POP_CHECK_STATE()
IF(RTABMAP_GTSAM_HAS_ATTITUDE_FACTOR_TEMPLATE)
ADD_DEFINITIONS(-DRTABMAP_GTSAM_HAS_ATTITUDE_FACTOR_TEMPLATE)
ENDIF()
ENDIF()
+1 -3
View File
@@ -504,9 +504,7 @@ std::map<int, Transform> OptimizerGTSAM::optimize(
gtsam::Unit3 nZ(0,0,1); gtsam::Unit3 nZ(0,0,1);
gtsam::Unit3 bGMeas = nRbMeas.unrotate(nZ); gtsam::Unit3 bGMeas = nRbMeas.unrotate(nZ);
gtsam::SharedNoiseModel model = gtsam::noiseModel::Isotropic::Sigma(2, gravitySigma()); gtsam::SharedNoiseModel model = gtsam::noiseModel::Isotropic::Sigma(2, gravitySigma());
#if GTSAM_VERSION_NUMERIC <= 40300 #ifndef RTABMAP_GTSAM_HAS_ATTITUDE_FACTOR_TEMPLATE
// Note: till 40301 is officially released, version 40300 with "4.3a1" would fail here.
// Just replace "<=" above by "<" to use AttitudeFactor<Pose3> below.
graph.add(gtsam::Pose3AttitudeFactor(iter->first, nZ, model, bGMeas)); graph.add(gtsam::Pose3AttitudeFactor(iter->first, nZ, model, bGMeas));
#else #else
graph.add(gtsam::AttitudeFactor<gtsam::Pose3>(iter->first, nZ, model, bGMeas)); graph.add(gtsam::AttitudeFactor<gtsam::Pose3>(iter->first, nZ, model, bGMeas));
@@ -100,7 +100,8 @@ namespace vertigo {
// handle derivatives // handle derivatives
if (H1) *H1 = *H1 * w; if (H1) *H1 = *H1 * w;
if (H2) *H2 = *H2 * w; if (H2) *H2 = *H2 * w;
if (H3) *H3 = error /* (w*(1.0-w))*/; // sig(x)*(1-sig(x)) is the derivative of sig(x) wrt. x // error already includes w; sigmoid's derivative is w*(1-w).
if (H3) *H3 = error * (1.0-w);
return error; return error;
}; };
@@ -13,6 +13,7 @@
// DerivedValue2.h removed from gtsam repo (Dec 2018): https://github.com/borglab/gtsam/commit/e550f4f2aec423cb3f2791b81cb5858b8826ebac // DerivedValue2.h removed from gtsam repo (Dec 2018): https://github.com/borglab/gtsam/commit/e550f4f2aec423cb3f2791b81cb5858b8826ebac
#include "DerivedValue.h" #include "DerivedValue.h"
#include <gtsam/base/Lie.h> #include <gtsam/base/Lie.h>
#include <gtsam/base/Manifold.h>
#include <gtsam/nonlinear/NonlinearFactor.h> #include <gtsam/nonlinear/NonlinearFactor.h>
namespace vertigo { namespace vertigo {
@@ -45,6 +46,7 @@ namespace vertigo {
} }
// Manifold requirements // Manifold requirements
static constexpr int dimension = 1;
/** Returns dimensionality of the tangent space */ /** Returns dimensionality of the tangent space */
inline size_t dim() const { return 1; } inline size_t dim() const { return 1; }
@@ -61,7 +63,13 @@ namespace vertigo {
} }
/** @return the local coordinates of another object */ /** @return the local coordinates of another object */
inline gtsam::Vector localCoordinates(const SwitchVariableLinear& t2) const { return gtsam::Vector1(t2.value() - value()); } inline gtsam::Vector1 localCoordinates(const SwitchVariableLinear& t2,
gtsam::OptionalJacobian<1, 1> H1 = {},
gtsam::OptionalJacobian<1, 1> H2 = {}) const {
if (H1) *H1 = -gtsam::Matrix11::Identity();
if (H2) *H2 = gtsam::Matrix11::Identity();
return gtsam::Vector1(t2.value() - value());
}
// Group requirements // Group requirements
@@ -108,36 +116,9 @@ namespace vertigo {
} }
namespace gtsam { namespace gtsam {
// Define Key to be Testable by specializing gtsam::traits // Use the scalar manifold's dimension, category and chart operations.
template<typename T> struct traits; template<> struct traits<vertigo::SwitchVariableLinear>
template<> struct traits<vertigo::SwitchVariableLinear> { : internal::Manifold<vertigo::SwitchVariableLinear> {};
static void Print(const vertigo::SwitchVariableLinear& key, const std::string& str = "") {
key.print(str);
}
static bool Equals(const vertigo::SwitchVariableLinear& key1, const vertigo::SwitchVariableLinear& key2, double tol = 1e-8) {
return key1.equals(key2, tol);
}
static int GetDimension(const vertigo::SwitchVariableLinear & key) {return key.Dim();}
typedef OptionalJacobian<3, 3> ChartJacobian;
typedef gtsam::Vector TangentVector;
static TangentVector Local(const vertigo::SwitchVariableLinear& origin, const vertigo::SwitchVariableLinear& other,
#if GTSAM_VERSION_NUMERIC >= 40300
ChartJacobian Horigin = {}, ChartJacobian Hother = {}) {
#else
ChartJacobian Horigin = boost::none, ChartJacobian Hother = boost::none) {
#endif
return origin.localCoordinates(other);
}
static vertigo::SwitchVariableLinear Retract(const vertigo::SwitchVariableLinear& g, const TangentVector& v,
#if GTSAM_VERSION_NUMERIC >= 40300
ChartJacobian H1 = {}, ChartJacobian H2 = {}) {
#else
ChartJacobian H1 = boost::none, ChartJacobian H2 = boost::none) {
#endif
return g.retract(v);
}
};
} }
@@ -13,6 +13,7 @@
// DerivedValue.h removed from gtsam repo (Dec 2018): https://github.com/borglab/gtsam/commit/e550f4f2aec423cb3f2791b81cb5858b8826ebac // DerivedValue.h removed from gtsam repo (Dec 2018): https://github.com/borglab/gtsam/commit/e550f4f2aec423cb3f2791b81cb5858b8826ebac
#include "DerivedValue.h" #include "DerivedValue.h"
#include <gtsam/base/Lie.h> #include <gtsam/base/Lie.h>
#include <gtsam/base/Manifold.h>
#include <gtsam/nonlinear/NonlinearFactor.h> #include <gtsam/nonlinear/NonlinearFactor.h>
namespace vertigo { namespace vertigo {
@@ -45,6 +46,7 @@ namespace vertigo {
} }
// Manifold requirements // Manifold requirements
static constexpr int dimension = 1;
/** Returns dimensionality of the tangent space */ /** Returns dimensionality of the tangent space */
inline size_t dim() const { return 1; } inline size_t dim() const { return 1; }
@@ -61,7 +63,13 @@ namespace vertigo {
} }
/** @return the local coordinates of another object */ /** @return the local coordinates of another object */
inline gtsam::Vector localCoordinates(const SwitchVariableSigmoid& t2) const { return gtsam::Vector1(t2.value() - value()); } inline gtsam::Vector1 localCoordinates(const SwitchVariableSigmoid& t2,
gtsam::OptionalJacobian<1, 1> H1 = {},
gtsam::OptionalJacobian<1, 1> H2 = {}) const {
if (H1) *H1 = -gtsam::Matrix11::Identity();
if (H2) *H2 = gtsam::Matrix11::Identity();
return gtsam::Vector1(t2.value() - value());
}
// Group requirements // Group requirements
@@ -109,36 +117,9 @@ namespace vertigo {
namespace gtsam { namespace gtsam {
// Define Key to be Testable by specializing gtsam::traits // Use the scalar manifold's dimension, category and chart operations.
template<typename T> struct traits; template<> struct traits<vertigo::SwitchVariableSigmoid>
template<> struct traits<vertigo::SwitchVariableSigmoid> { : internal::Manifold<vertigo::SwitchVariableSigmoid> {};
static void Print(const vertigo::SwitchVariableSigmoid& key, const std::string& str = "") {
key.print(str);
}
static bool Equals(const vertigo::SwitchVariableSigmoid& key1, const vertigo::SwitchVariableSigmoid& key2, double tol = 1e-8) {
return key1.equals(key2, tol);
}
static int GetDimension(const vertigo::SwitchVariableSigmoid & key) {return key.Dim();}
typedef OptionalJacobian<3, 3> ChartJacobian;
typedef gtsam::Vector TangentVector;
static TangentVector Local(const vertigo::SwitchVariableSigmoid& origin, const vertigo::SwitchVariableSigmoid& other,
#if GTSAM_VERSION_NUMERIC >= 40300
ChartJacobian Horigin = {}, ChartJacobian Hother = {}) {
#else
ChartJacobian Horigin = boost::none, ChartJacobian Hother = boost::none) {
#endif
return origin.localCoordinates(other);
}
static vertigo::SwitchVariableSigmoid Retract(const vertigo::SwitchVariableSigmoid& g, const TangentVector& v,
#if GTSAM_VERSION_NUMERIC >= 40300
ChartJacobian H1 = {}, ChartJacobian H2 = {}) {
#else
ChartJacobian H1 = boost::none, ChartJacobian H2 = boost::none) {
#endif
return g.retract(v);
}
};
} }
#endif /* SWITCHVARIABLESIGMOID_H_ */ #endif /* SWITCHVARIABLESIGMOID_H_ */
+11
View File
@@ -68,9 +68,20 @@ set(corelib_test_sources
test_sensorcapturethread.cpp #SensorCaptureThread.h test_sensorcapturethread.cpp #SensorCaptureThread.h
) )
# test_optimizer_gtsam.cpp includes the Vertigo GTSAM factors from
# corelib/src/optimizer directly, so it needs GTSAM itself (rtabmap_core links
# it PRIVATE).
IF(GTSAM_FOUND)
list(APPEND corelib_test_sources test_optimizer_gtsam.cpp) #OptimizerGTSAM.h (Vertigo factors, gravity)
ENDIF(GTSAM_FOUND)
add_executable(test_corelib ${corelib_test_sources}) add_executable(test_corelib ${corelib_test_sources})
target_link_libraries(test_corelib gtest_main rtabmap_core) target_link_libraries(test_corelib gtest_main rtabmap_core)
IF(GTSAM_FOUND)
target_link_libraries(test_corelib gtsam)
ENDIF(GTSAM_FOUND)
# test_registrationicp.cpp includes corelib/src/icp/libpointmatcher.h directly # test_registrationicp.cpp includes corelib/src/icp/libpointmatcher.h directly
# to test the LaserScan <-> DataPoints conversions, so it needs the library # to test the LaserScan <-> DataPoints conversions, so it needs the library
# itself (rtabmap_core links it PRIVATE and only re-exports its include dirs). # itself (rtabmap_core links it PRIVATE and only re-exports its include dirs).
+285
View File
@@ -0,0 +1,285 @@
// Tests for the GTSAM pieces used by rtabmap::OptimizerGTSAM, independent of
// the optimizer itself:
// - Vertigo switch variables (linear and sigmoid): scalar manifold traits,
// 1x1 Local Jacobians, priors, and constructor/retract clamping.
// - Switchable between factors: residuals and Jacobians against the
// release's BetweenFactor, the switch derivative against finite
// differences, and linearization.
// - Gravity (attitude) factor API selected by CMake
// (RTABMAP_GTSAM_HAS_ATTITUDE_FACTOR_TEMPLATE).
//
// Background: Optimizer/Robust=true adds, for every loop closure, a switch
// variable s_ij with a prior, and replaces the loop closure's BetweenFactor
// by a switchable one whose residual is weighted by s_ij (linear switch) or
// sigmoid(s_ij) (sigmoid switch). The optimizer can then turn off outlier
// loop closures by driving their weight to 0. These factors live in
// corelib/src/optimizer/vertigo/gtsam and depend on GTSAM internals (traits,
// OptionalJacobian, PriorFactor), which changed across 4.0/4.2/4.3, hence the
// focused checks below. They are meant to pass on every GTSAM version
// rtabmap supports.
#include <gtest/gtest.h>
#include <gtsam/config.h>
#include <gtsam/base/numericalDerivative.h>
#include <gtsam/geometry/Pose2.h>
#include <gtsam/geometry/Pose3.h>
#include <gtsam/slam/BetweenFactor.h>
#include <gtsam/slam/PriorFactor.h>
#include <gtsam/nonlinear/LevenbergMarquardtOptimizer.h>
#include <gtsam/nonlinear/NonlinearFactorGraph.h>
#include <gtsam/nonlinear/Values.h>
#include <gtsam/navigation/AttitudeFactor.h>
#include "../src/optimizer/vertigo/gtsam/betweenFactorSwitchable.h"
#include <cmath>
#include <sstream>
using namespace gtsam;
namespace {
// Compares matrices (or vectors) with the same tolerance everywhere. A size
// mismatch is reported separately: a Jacobian of the wrong size is exactly the
// kind of regression these tests target (e.g., the 3x3 Jacobians the switch
// traits used to declare for a 1-D variable, which made JacobianFactor throw
// InvalidMatrixBlock during linearization).
::testing::AssertionResult near(const Matrix& actual, const Matrix& expected)
{
if(actual.rows() != expected.rows() || actual.cols() != expected.cols())
{
return ::testing::AssertionFailure() << "dimensions " << actual.rows() << "x" << actual.cols()
<< " instead of " << expected.rows() << "x" << expected.cols();
}
if(!actual.allFinite() || (actual - expected).norm() >= 1e-6)
{
std::stringstream ss;
ss << "Actual:\n" << actual << "\nExpected:\n" << expected;
return ::testing::AssertionFailure() << ss.str();
}
return ::testing::AssertionSuccess();
}
// Checks that a switch variable behaves as a 1-D manifold for GTSAM:
// 1. traits<Switch>::dimension is 1 (required by noiseModel::Unit::Create()
// and fixed-size code paths in GTSAM >= 4.3).
// 2. localCoordinates(y) = y - x, with analytical Jacobians -1 (wrt x) and
// +1 (wrt y).
// 3. A PriorFactor on the switch (what OptimizerGTSAM adds for every switch)
// gives a scalar residual x - prior and a 1x1 identity Jacobian, matching
// a numerical derivative, and can be linearized. Depending on the GTSAM
// version/configuration (GTSAM_SLOW_BUT_CORRECT_BETWEENFACTOR), the prior
// takes its Jacobian from traits<Switch>::Local(), so this is where
// incomplete traits show up.
// 4. On GTSAM >= 4.3, the same with a prior built without a noise model,
// which goes through noiseModel::Unit::Create(value) and so needs the
// full manifold traits (dimension, structure_category, ManifoldType).
template<class Switch>
void checkSwitch(double value, double other)
{
static_assert(traits<Switch>::dimension == 1, "scalar tangent");
const Switch x(value), y(other);
// Chart and its Jacobians
Matrix11 h1, h2;
EXPECT_TRUE(near(x.localCoordinates(y, h1, h2), Vector1(other - value)));
EXPECT_TRUE(near(h1, -Matrix11::Identity()));
EXPECT_TRUE(near(h2, Matrix11::Identity()));
// Prior with an explicit noise model, as created by OptimizerGTSAM
const PriorFactor<Switch> prior(1, y, noiseModel::Isotropic::Sigma(1, 1));
Matrix h;
EXPECT_TRUE(near(prior.evaluateError(x, h), Vector1(value - other)));
EXPECT_TRUE(near(h, Matrix11::Identity()));
EXPECT_TRUE(near(h, numericalDerivative11<Vector, Switch>(
[&prior](const Switch& s) { return prior.evaluateError(s); }, x)));
Values values;
values.insert(1, x);
EXPECT_TRUE(bool(prior.linearize(values))) << "explicit prior linearization";
#if GTSAM_VERSION_NUMERIC >= 40300
// Prior with the default (unit) noise model. Default noise was added in
// 4.3; 4.2 requires the explicit model above.
const PriorFactor<Switch> unitPrior(1, y);
EXPECT_TRUE(near(unitPrior.evaluateError(x, h), Vector1(value - other)));
EXPECT_TRUE(near(h, Matrix11::Identity()));
EXPECT_TRUE(bool(unitPrior.linearize(values))) << "default prior linearization";
#endif
}
// Checks BetweenFactorSwitchableLinear, the robust loop closure factor used
// by OptimizerGTSAM: residual = s * BetweenFactor residual.
// - The pose Jacobians must be the regular BetweenFactor Jacobians scaled by
// the switch weight. They are compared against the BetweenFactor of the
// installed GTSAM rather than against numerical derivatives, because older
// GTSAM (without GTSAM_SLOW_BUT_CORRECT_BETWEENFACTOR) uses an approximate
// Local Jacobian for poses. When the exact one is enabled, they are also
// compared against numerical derivatives.
// - The switch Jacobian (d residual / d s = raw residual) is always compared
// against a numerical derivative.
// - The factor, with the two poses and the switch in a Values, must
// linearize (which checks that all Jacobian sizes agree with the residual).
template<class Pose>
void checkBetweenLinear(const Pose& first, const Pose& second, double switchValue)
{
const vertigo::SwitchVariableLinear s(switchValue);
const auto model = noiseModel::Isotropic::Sigma(traits<Pose>::dimension, 1);
const vertigo::BetweenFactorSwitchableLinear<Pose> factor(1, 2, 3, Pose(), model);
Matrix h1, h2, h3;
#if GTSAM_VERSION_NUMERIC >= 40300
const Vector error = factor.evaluateError(first, second, s, &h1, &h2, &h3);
#else
const Vector error = factor.evaluateError(first, second, s, h1, h2, h3);
#endif
// Same measurement without switch: its residual ratio gives the weight
// actually applied, which must also scale the pose Jacobians.
Matrix rawH1, rawH2;
const BetweenFactor<Pose> raw(1, 2, Pose(), model);
const Vector rawError = raw.evaluateError(first, second, rawH1, rawH2);
const double weight = error.norm() / rawError.norm();
EXPECT_TRUE(near(h1, rawH1 * weight));
EXPECT_TRUE(near(h2, rawH2 * weight));
#ifdef GTSAM_SLOW_BUT_CORRECT_BETWEENFACTOR
EXPECT_TRUE(near(h1, numericalDerivative11<Vector, Pose>(
[&](const Pose& p) { return factor.evaluateError(p, second, s); }, first)));
EXPECT_TRUE(near(h2, numericalDerivative11<Vector, Pose>(
[&](const Pose& p) { return factor.evaluateError(first, p, s); }, second)));
#endif
EXPECT_TRUE(near(h3, numericalDerivative11<Vector, vertigo::SwitchVariableLinear>(
[&](const vertigo::SwitchVariableLinear& x) { return factor.evaluateError(first, second, x); }, s)));
EXPECT_TRUE(error.allFinite());
Values values;
values.insert(1, first); values.insert(2, second); values.insert(3, s);
EXPECT_TRUE(bool(factor.linearize(values))) << "switchable linearization";
}
// Checks BetweenFactorSwitchableSigmoid: residual = w * BetweenFactor
// residual, with w = sigmoid(s) = 1/(1+exp(-s)).
// - Residual and pose Jacobians must be the BetweenFactor ones scaled by w.
// - The switch Jacobian must be raw residual * dw/ds = raw * w*(1-w), both
// analytically and by central finite differences. This catches the bug
// where it returned the weighted residual (raw * w), missing the (1-w)
// factor.
// - The factor must linearize.
template<class Pose>
void checkBetweenSigmoid(const Pose& second, double switchValue)
{
const Pose first;
const vertigo::SwitchVariableSigmoid s(switchValue);
const auto model = noiseModel::Isotropic::Sigma(traits<Pose>::dimension, 1);
const vertigo::BetweenFactorSwitchableSigmoid<Pose> factor(1, 2, 3, Pose(), model);
Matrix h1, h2, h3;
#if GTSAM_VERSION_NUMERIC >= 40300
const Vector error = factor.evaluateError(first, second, s, &h1, &h2, &h3);
#else
const Vector error = factor.evaluateError(first, second, s, h1, h2, h3);
#endif
Matrix rawH1, rawH2;
const BetweenFactor<Pose> raw(1, 2, Pose(), model);
const Vector rawError = raw.evaluateError(first, second, rawH1, rawH2);
const double weight = 1.0 / (1.0 + std::exp(-switchValue));
EXPECT_TRUE(near(error, rawError * weight));
EXPECT_TRUE(near(h1, rawH1 * weight));
EXPECT_TRUE(near(h2, rawH2 * weight));
// Analytical: d(sigmoid)/ds = w*(1-w)
EXPECT_TRUE(near(h3, rawError * (weight * (1.0 - weight))));
// Numerical: central difference over the switch value. The switch is
// re-constructed rather than retracted, as retract() clamps it.
const double step = 1e-5;
const Vector plus = factor.evaluateError(first, second,
vertigo::SwitchVariableSigmoid(switchValue + step));
const Vector minus = factor.evaluateError(first, second,
vertigo::SwitchVariableSigmoid(switchValue - step));
EXPECT_TRUE(near(h3, (plus - minus) / (2.0 * step)));
Values values;
values.insert(1, first); values.insert(2, second); values.insert(3, s);
EXPECT_TRUE(bool(factor.linearize(values))) << "sigmoid linearization";
}
} // namespace
// Linear switch (the one OptimizerGTSAM uses) as a 1-D GTSAM manifold.
TEST(OptimizerGTSAM, SwitchVariableLinearManifold)
{
checkSwitch<vertigo::SwitchVariableLinear>(0.4, 0.7);
}
// Sigmoid switch as a 1-D GTSAM manifold (negative values are valid for it).
TEST(OptimizerGTSAM, SwitchVariableSigmoidManifold)
{
checkSwitch<vertigo::SwitchVariableSigmoid>(-0.4, 0.7);
}
// Switch bounds: retract() (applied at each optimization step) projects the
// linear switch to [0,1] and the sigmoid switch to [-10,10]. The constructor
// doesn't clamp the linear switch, but does clamp the sigmoid one. Moving the
// traits to internal::Manifold must not change this behavior.
TEST(OptimizerGTSAM, SwitchVariableClamping)
{
EXPECT_DOUBLE_EQ(vertigo::SwitchVariableLinear(0.4).retract(Vector1(2)).value(), 1.0);
EXPECT_DOUBLE_EQ(vertigo::SwitchVariableLinear(0.4).retract(Vector1(-2)).value(), 0.0);
EXPECT_DOUBLE_EQ(vertigo::SwitchVariableLinear(2).value(), 2.0);
EXPECT_DOUBLE_EQ(vertigo::SwitchVariableSigmoid(0).retract(Vector1(20)).value(), 10.0);
EXPECT_DOUBLE_EQ(vertigo::SwitchVariableSigmoid(0).retract(Vector1(-20)).value(), -10.0);
EXPECT_DOUBLE_EQ(vertigo::SwitchVariableSigmoid(20).value(), 10.0);
EXPECT_DOUBLE_EQ(vertigo::SwitchVariableSigmoid(-20).value(), -10.0);
}
// Robust loop closure factor with a linear switch, for 2D (Optimizer/Slam2D)
// and 3D graphs, with a partially-on switch (0.4).
TEST(OptimizerGTSAM, BetweenFactorSwitchableLinear)
{
checkBetweenLinear(Pose2(), Pose2(1, 2, 0.2), 0.4);
checkBetweenLinear(Pose3(), Pose3(Rot3::RzRyRx(0.1, 0.2, 0.3), Point3(1, 2, 3)), 0.4);
}
// Robust loop closure factor with a sigmoid switch, for 2D and 3D graphs.
// Switch values -2, 0 and 2 give weights ~0.12, 0.5 and ~0.88, staying away
// from the [-10,10] clamps.
TEST(OptimizerGTSAM, BetweenFactorSwitchableSigmoid)
{
for(double s : {-2.0, 0.0, 2.0})
{
SCOPED_TRACE(s);
checkBetweenSigmoid(Pose2(1, 2, 0.2), s);
checkBetweenSigmoid(Pose3(Rot3::RzRyRx(0.1, 0.2, 0.3), Point3(1, 2, 3)), s);
}
}
// Gravity constraints (Link::kGravity, Optimizer/GravitySigma > 0) are added
// as a Pose3 attitude factor. Its class changed name in GTSAM 4.3
// (Pose3AttitudeFactor -> AttitudeFactor<Pose3>), and some ROS 4.3 snapshots
// report the same version number with either API, so CMake detects which one
// compiles. This checks the detected API is usable as OptimizerGTSAM uses it:
// - Jacobian matches a numerical derivative on a tilted pose.
// - Residual is zero when the pose is aligned with gravity.
// - Optimizing a tilted pose (with a loose prior to fix the yaw and the
// translation, which gravity doesn't observe) removes the tilt.
TEST(OptimizerGTSAM, GravityFactor)
{
#ifdef RTABMAP_GTSAM_HAS_ATTITUDE_FACTOR_TEMPLATE
using GravityFactor = AttitudeFactor<Pose3>;
#else
using GravityFactor = Pose3AttitudeFactor;
#endif
// Reference direction: world z axis; measured in body frame: also z (pose
// should be level).
const GravityFactor gravity(1, Unit3(0,0,1), noiseModel::Isotropic::Sigma(2, 0.1));
const Pose3 initial(Rot3::RzRyRx(0.1, -0.2, 0.3), Point3(1, 2, 3));
Matrix h;
const Vector error = gravity.evaluateError(initial, h);
EXPECT_TRUE(near(h, numericalDerivative11<Vector, Pose3>(
[&](const Pose3& p) { return gravity.evaluateError(p); }, initial)));
EXPECT_TRUE(near(gravity.evaluateError(Pose3()), Vector2::Zero()));
NonlinearFactorGraph graph;
graph.add(gravity);
graph.add(PriorFactor<Pose3>(1, Pose3(), noiseModel::Isotropic::Sigma(6, 1)));
Values values;
values.insert(1, initial);
const Values result = LevenbergMarquardtOptimizer(graph, values).optimize();
EXPECT_LT(gravity.evaluateError(result.at<Pose3>(1)).norm(), error.norm()*0.01)
<< "gravity optimization reduces tilt";
}