From 76c953784704c084ffba34e4d3fdc233a67b2ba4 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 16 Dec 2024 12:48:34 -0500 Subject: [PATCH 1/8] inverse Jacobians --- gtsam/geometry/SO3.cpp | 32 +++++++++++++++++++++----------- gtsam/geometry/SO3.h | 25 ++++++++++++++++++++----- gtsam/geometry/tests/testSO3.cpp | 19 ++++++++++++++++++- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/gtsam/geometry/SO3.cpp b/gtsam/geometry/SO3.cpp index c54306ae5a..6499d41396 100644 --- a/gtsam/geometry/SO3.cpp +++ b/gtsam/geometry/SO3.cpp @@ -87,19 +87,29 @@ SO3 ExpmapFunctor::expmap() const { return SO3(I_3x3 + A * W + B * WW); } DexpFunctor::DexpFunctor(const Vector3& omega, bool nearZeroApprox) : ExpmapFunctor(omega, nearZeroApprox), omega(omega) { - C = nearZero ? one_sixth : (1 - A) / theta2; - D = nearZero ? _one_twelfth : (A - 2.0 * B) / theta2; - E = nearZero ? _one_sixtieth : (B - 3.0 * C) / theta2; + if (!nearZero) { + C = (1 - A) / theta2; + D = (1.0 - A / (2.0 * B)) / theta2; + E = (2.0 * B - A) / theta2; + F = (3.0 * C - B) / theta2; + } else { + // Limit as theta -> 0 + // TODO(Frank): flipping signs here does not trigger any tests: harden! + C = one_sixth; + D = one_twelfth; + E = one_twelfth; + F = one_sixtieth; + } } Vector3 DexpFunctor::crossB(const Vector3& v, OptionalJacobian<3, 3> H) const { // Wv = omega x v const Vector3 Wv = gtsam::cross(omega, v); if (H) { - // Apply product rule: - // D * omega.transpose() is 1x3 Jacobian of B with respect to omega - // - skewSymmetric(v) is 3x3 Jacobian of B gtsam::cross(omega, v) - *H = Wv * D * omega.transpose() - B * skewSymmetric(v); + // Apply product rule to (B Wv) + // - E * omega.transpose() is 1x3 Jacobian of B with respect to omega + // - skewSymmetric(v) is 3x3 Jacobian of Wv = gtsam::cross(omega, v) + *H = - Wv * E * omega.transpose() - B * skewSymmetric(v); } return B * Wv; } @@ -111,10 +121,10 @@ Vector3 DexpFunctor::doubleCrossC(const Vector3& v, const Vector3 WWv = gtsam::doubleCross(omega, v, H ? &doubleCrossJacobian : nullptr); if (H) { - // Apply product rule: - // E * omega.transpose() is 1x3 Jacobian of C with respect to omega - // doubleCrossJacobian is 3x3 Jacobian of C gtsam::doubleCross(omega, v) - *H = WWv * E * omega.transpose() + C * doubleCrossJacobian; + // Apply product rule to (C WWv) + // - F * omega.transpose() is 1x3 Jacobian of C with respect to omega + // doubleCrossJacobian is 3x3 Jacobian of WWv = gtsam::doubleCross(omega, v) + *H = - WWv * F * omega.transpose() + C * doubleCrossJacobian; } return C * WWv; } diff --git a/gtsam/geometry/SO3.h b/gtsam/geometry/SO3.h index f831aa4266..488dea12e5 100644 --- a/gtsam/geometry/SO3.h +++ b/gtsam/geometry/SO3.h @@ -157,10 +157,16 @@ struct GTSAM_EXPORT ExpmapFunctor { /// Functor that implements Exponential map *and* its derivatives struct GTSAM_EXPORT DexpFunctor : public ExpmapFunctor { const Vector3 omega; - double C; // Ethan's C constant: (1 - A) / theta^2 or 1/6 for theta->0 + + // Ethan's C constant used in Jacobians + double C; // (1 - A) / theta^2 or 1/6 for theta->0 + + // Constant used in inverse Jacobians + double D; // 1.0 - A / (2.0 * B)) / theta2 or 1/12 for theta->0 + // Constants used in cross and doubleCross - double D; // (A - 2.0 * B) / theta2 or -1/12 for theta->0 - double E; // (B - 3.0 * C) / theta2 or -1/60 for theta->0 + double E; // (A - 2.0 * B) / theta2 or -1/12 for theta->0 + double F; // (B - 3.0 * C) / theta2 or -1/60 for theta->0 /// Constructor with element of Lie algebra so(3) explicit DexpFunctor(const Vector3& omega, bool nearZeroApprox = false); @@ -179,6 +185,15 @@ struct GTSAM_EXPORT DexpFunctor : public ExpmapFunctor { /// Differential of expmap == right Jacobian inline Matrix3 dexp() const { return rightJacobian(); } + /// Inverse of right Jacobian + Matrix3 rightJacobianInverse() const { return I_3x3 + 0.5 * W + D * WW; } + + // Inverse of left Jacobian + Matrix3 leftJacobianInverse() const { return I_3x3 - 0.5 * W + D * WW; } + + /// Synonym for rightJacobianInverse + inline Matrix3 invDexp() const { return rightJacobianInverse(); } + /// Computes B * (omega x v). Vector3 crossB(const Vector3& v, OptionalJacobian<3, 3> H = {}) const; @@ -198,8 +213,8 @@ struct GTSAM_EXPORT DexpFunctor : public ExpmapFunctor { OptionalJacobian<3, 3> H2 = {}) const; static constexpr double one_sixth = 1.0 / 6.0; - static constexpr double _one_twelfth = -1.0 / 12.0; - static constexpr double _one_sixtieth = -1.0 / 60.0; + static constexpr double one_twelfth = 1.0 / 12.0; + static constexpr double one_sixtieth = 1.0 / 60.0; }; } // namespace so3 diff --git a/gtsam/geometry/tests/testSO3.cpp b/gtsam/geometry/tests/testSO3.cpp index 3ecabe84bd..96872eae99 100644 --- a/gtsam/geometry/tests/testSO3.cpp +++ b/gtsam/geometry/tests/testSO3.cpp @@ -19,6 +19,7 @@ #include #include #include +#include using namespace std::placeholders; using namespace std; @@ -304,13 +305,29 @@ TEST(SO3, JacobianLogmap) { } namespace test_cases { -std::vector small{{0, 0, 0}, {1e-5, 0, 0}, {0, 1e-5, 0}, {0, 0, 1e-5}}; +std::vector small{{0, 0, 0}, // + {1e-5, 0, 0}, {0, 1e-5, 0}, {0, 0, 1e-5}, //, + {1e-4, 0, 0}, {0, 1e-4, 0}, {0, 0, 1e-4}}; std::vector large{ {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0.1, 0.2, 0.3}}; auto omegas = [](bool nearZero) { return nearZero ? small : large; }; std::vector vs{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0.4, 0.3, 0.2}}; } // namespace test_cases +//****************************************************************************** +TEST(SO3, JacobianInverses) { + Matrix HR, HL; + for (bool nearZero : {true, false}) { + for (const Vector3& omega : test_cases::omegas(nearZero)) { + so3::DexpFunctor local(omega, nearZero); + EXPECT(assert_equal(local.rightJacobian().inverse(), + local.rightJacobianInverse())); + EXPECT(assert_equal(local.leftJacobian().inverse(), + local.leftJacobianInverse())); + } + } +} + //****************************************************************************** TEST(SO3, CrossB) { Matrix aH1; From 6c84a2b539c144ed06c9a3aec5fa2ba697a29b5d Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 16 Dec 2024 12:48:52 -0500 Subject: [PATCH 2/8] Use X to map left to right --- gtsam/geometry/Pose3.cpp | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/gtsam/geometry/Pose3.cpp b/gtsam/geometry/Pose3.cpp index 5f0e8e3ce3..cb8de111e3 100644 --- a/gtsam/geometry/Pose3.cpp +++ b/gtsam/geometry/Pose3.cpp @@ -22,6 +22,7 @@ #include #include #include +#include "gtsam/geometry/Rot3.h" namespace gtsam { @@ -241,25 +242,18 @@ Vector6 Pose3::ChartAtOrigin::Local(const Pose3& pose, ChartJacobian Hpose) { /* ************************************************************************* */ namespace pose3 { struct GTSAM_EXPORT ExpmapFunctor : public so3::DexpFunctor { - // Constant used in computeQ - double F; // (B - 0.5) / theta2 or -1/24 for theta->0 - ExpmapFunctor(const Vector3& omega, bool nearZeroApprox = false) : so3::DexpFunctor(omega, nearZeroApprox) { - F = nearZero ? _one_twenty_fourth : (B - 0.5) / theta2; } - // Compute the bottom-left 3x3 block of the SE(3) Expmap derivative - // TODO(Frank): t = applyLeftJacobian(v), it would be nice to understand - // how to compute mess below from applyLeftJacobian derivatives in w and v. + // Compute the bottom-left 3x3 block of the SE(3) Expmap derivative. Matrix3 computeQ(const Vector3& v) const { - const Matrix3 V = skewSymmetric(v); - const Matrix3 WVW = W * V * W; - return -0.5 * V + C * (W * V + V * W - WVW) + - F * (WW * V + V * WW - 3 * WVW) - 0.5 * E * (WVW * W + W * WVW); + // X translate from left to right for our right expmap convention: + Matrix X = rightJacobian() * leftJacobianInverse(); + Matrix3 H; + applyLeftJacobian(v, H); + return X * H; } - - static constexpr double _one_twenty_fourth = - 1.0 / 24.0; }; } // namespace pose3 From 98697251bd9d8558258493f18c266c032b2c1dc3 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 16 Dec 2024 12:57:27 -0500 Subject: [PATCH 3/8] applyLeftJacobianInverse --- gtsam/geometry/SO3.cpp | 26 ++++++++++++++++++------ gtsam/geometry/SO3.h | 7 ++++++- gtsam/geometry/tests/testSO3.cpp | 34 ++++++++++++++++++++++++++------ 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/gtsam/geometry/SO3.cpp b/gtsam/geometry/SO3.cpp index 6499d41396..93bf072c53 100644 --- a/gtsam/geometry/SO3.cpp +++ b/gtsam/geometry/SO3.cpp @@ -142,14 +142,14 @@ Vector3 DexpFunctor::applyDexp(const Vector3& v, OptionalJacobian<3, 3> H1, Vector3 DexpFunctor::applyInvDexp(const Vector3& v, OptionalJacobian<3, 3> H1, OptionalJacobian<3, 3> H2) const { - const Matrix3 invDexp = rightJacobian().inverse(); - const Vector3 c = invDexp * v; + const Matrix3 invJr = rightJacobianInverse(); + const Vector3 c = invJr * v; if (H1) { - Matrix3 D_dexpv_w; - applyDexp(c, D_dexpv_w); // get derivative H of forward mapping - *H1 = -invDexp * D_dexpv_w; + Matrix3 H; + applyDexp(c, H); // get derivative H of forward mapping + *H1 = -invJr * H; } - if (H2) *H2 = invDexp; + if (H2) *H2 = invJr; return c; } @@ -164,6 +164,20 @@ Vector3 DexpFunctor::applyLeftJacobian(const Vector3& v, return v + BWv + CWWv; } +Vector3 DexpFunctor::applyLeftJacobianInverse(const Vector3& v, + OptionalJacobian<3, 3> H1, + OptionalJacobian<3, 3> H2) const { + const Matrix3 invJl = leftJacobianInverse(); + const Vector3 c = invJl * v; + if (H1) { + Matrix3 H; + applyLeftJacobian(c, H); // get derivative H of forward mapping + *H1 = -invJl * H; + } + if (H2) *H2 = invJl; + return c; +} + } // namespace so3 //****************************************************************************** diff --git a/gtsam/geometry/SO3.h b/gtsam/geometry/SO3.h index 488dea12e5..2845fcce78 100644 --- a/gtsam/geometry/SO3.h +++ b/gtsam/geometry/SO3.h @@ -162,7 +162,7 @@ struct GTSAM_EXPORT DexpFunctor : public ExpmapFunctor { double C; // (1 - A) / theta^2 or 1/6 for theta->0 // Constant used in inverse Jacobians - double D; // 1.0 - A / (2.0 * B)) / theta2 or 1/12 for theta->0 + double D; // (1 - A/2B) / theta2 or 1/12 for theta->0 // Constants used in cross and doubleCross double E; // (A - 2.0 * B) / theta2 or -1/12 for theta->0 @@ -212,6 +212,11 @@ struct GTSAM_EXPORT DexpFunctor : public ExpmapFunctor { Vector3 applyLeftJacobian(const Vector3& v, OptionalJacobian<3, 3> H1 = {}, OptionalJacobian<3, 3> H2 = {}) const; + /// Multiplies with leftJacobianInverse(), with optional derivatives + Vector3 applyLeftJacobianInverse(const Vector3& v, + OptionalJacobian<3, 3> H1 = {}, + OptionalJacobian<3, 3> H2 = {}) const; + static constexpr double one_sixth = 1.0 / 6.0; static constexpr double one_twelfth = 1.0 / 12.0; static constexpr double one_sixtieth = 1.0 / 60.0; diff --git a/gtsam/geometry/tests/testSO3.cpp b/gtsam/geometry/tests/testSO3.cpp index 96872eae99..d3313b7f1d 100644 --- a/gtsam/geometry/tests/testSO3.cpp +++ b/gtsam/geometry/tests/testSO3.cpp @@ -385,6 +385,28 @@ TEST(SO3, ApplyDexp) { } } +//****************************************************************************** +TEST(SO3, ApplyInvDexp) { + Matrix aH1, aH2; + for (bool nearZero : {true, false}) { + std::function f = + [=](const Vector3& omega, const Vector3& v) { + return so3::DexpFunctor(omega, nearZero).applyInvDexp(v); + }; + for (const Vector3& omega : test_cases::omegas(nearZero)) { + so3::DexpFunctor local(omega, nearZero); + Matrix invJr = local.rightJacobianInverse(); + for (const Vector3& v : test_cases::vs) { + EXPECT( + assert_equal(Vector3(invJr * v), local.applyInvDexp(v, aH1, aH2))); + EXPECT(assert_equal(numericalDerivative21(f, omega, v), aH1)); + EXPECT(assert_equal(numericalDerivative22(f, omega, v), aH2)); + EXPECT(assert_equal(invJr, aH2)); + } + } + } +} + //****************************************************************************** TEST(SO3, ApplyLeftJacobian) { Matrix aH1, aH2; @@ -407,22 +429,22 @@ TEST(SO3, ApplyLeftJacobian) { } //****************************************************************************** -TEST(SO3, ApplyInvDexp) { +TEST(SO3, ApplyLeftJacobianInverse) { Matrix aH1, aH2; for (bool nearZero : {true, false}) { std::function f = [=](const Vector3& omega, const Vector3& v) { - return so3::DexpFunctor(omega, nearZero).applyInvDexp(v); + return so3::DexpFunctor(omega, nearZero).applyLeftJacobianInverse(v); }; for (const Vector3& omega : test_cases::omegas(nearZero)) { so3::DexpFunctor local(omega, nearZero); - Matrix invDexp = local.dexp().inverse(); + Matrix invJl = local.leftJacobianInverse(); for (const Vector3& v : test_cases::vs) { - EXPECT(assert_equal(Vector3(invDexp * v), - local.applyInvDexp(v, aH1, aH2))); + EXPECT(assert_equal(Vector3(invJl * v), + local.applyLeftJacobianInverse(v, aH1, aH2))); EXPECT(assert_equal(numericalDerivative21(f, omega, v), aH1)); EXPECT(assert_equal(numericalDerivative22(f, omega, v), aH2)); - EXPECT(assert_equal(invDexp, aH2)); + EXPECT(assert_equal(invJl, aH2)); } } } From b11387167d9d8988e6a35c920518863c9d14fe44 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 16 Dec 2024 13:28:19 -0500 Subject: [PATCH 4/8] No more functor for Q --- gtsam/geometry/Pose3.cpp | 59 +++++++++++++----------------- gtsam/geometry/tests/testPose3.cpp | 11 ------ 2 files changed, 26 insertions(+), 44 deletions(-) diff --git a/gtsam/geometry/Pose3.cpp b/gtsam/geometry/Pose3.cpp index cb8de111e3..9f81204d63 100644 --- a/gtsam/geometry/Pose3.cpp +++ b/gtsam/geometry/Pose3.cpp @@ -22,6 +22,7 @@ #include #include #include + #include "gtsam/geometry/Rot3.h" namespace gtsam { @@ -112,7 +113,7 @@ Matrix6 Pose3::adjointMap(const Vector6& xi) { /* ************************************************************************* */ Vector6 Pose3::adjoint(const Vector6& xi, const Vector6& y, - OptionalJacobian<6, 6> Hxi, OptionalJacobian<6, 6> H_y) { + OptionalJacobian<6, 6> Hxi, OptionalJacobian<6, 6> H_y) { if (Hxi) { Hxi->setZero(); for (int i = 0; i < 6; ++i) { @@ -159,7 +160,7 @@ bool Pose3::equals(const Pose3& pose, double tol) const { /* ************************************************************************* */ Pose3 Pose3::interpolateRt(const Pose3& T, double t) const { return Pose3(interpolate(R_, T.R_, t), - interpolate(t_, T.t_, t)); + interpolate(t_, T.t_, t)); } /* ************************************************************************* */ @@ -239,30 +240,14 @@ Vector6 Pose3::ChartAtOrigin::Local(const Pose3& pose, ChartJacobian Hpose) { #endif } -/* ************************************************************************* */ -namespace pose3 { -struct GTSAM_EXPORT ExpmapFunctor : public so3::DexpFunctor { - ExpmapFunctor(const Vector3& omega, bool nearZeroApprox = false) - : so3::DexpFunctor(omega, nearZeroApprox) { - } - - // Compute the bottom-left 3x3 block of the SE(3) Expmap derivative. - Matrix3 computeQ(const Vector3& v) const { - // X translate from left to right for our right expmap convention: - Matrix X = rightJacobian() * leftJacobianInverse(); - Matrix3 H; - applyLeftJacobian(v, H); - return X * H; - } -}; -} // namespace pose3 - /* ************************************************************************* */ Matrix3 Pose3::ComputeQforExpmapDerivative(const Vector6& xi, double nearZeroThreshold) { const auto w = xi.head<3>(); const auto v = xi.tail<3>(); - return pose3::ExpmapFunctor(w).computeQ(v); + Matrix3 Q; + ExpmapTranslation(w, v, Q, {}, nearZeroThreshold); + return Q; } /* ************************************************************************* */ @@ -273,13 +258,22 @@ Vector3 Pose3::ExpmapTranslation(const Vector3& w, const Vector3& v, const double theta2 = w.dot(w); bool nearZero = (theta2 <= nearZeroThreshold); - if (Q) *Q = pose3::ExpmapFunctor(w, nearZero).computeQ(v); - - if (nearZero) { - return v + 0.5 * w.cross(v); + if (Q) { + // Instantiate functor for Dexp-related operations: + so3::DexpFunctor local(w, nearZero); + // X translate from left to right for our right expmap convention: + Matrix3 X = local.rightJacobian() * local.leftJacobianInverse(); + Matrix3 H; + Vector t = local.applyLeftJacobian(v, H); + *Q = X * H; + return t; + } else if (nearZero) { + // (I_3x3 + B * W + C * WW) * v with B->0.5, C->1/6 + Vector3 Wv = w.cross(v); + return v + 0.5 * Wv + w.cross(Wv) * so3::DexpFunctor::one_sixth; } else { - // NOTE(Frank): t can also be computed by calling applyLeftJacobian(v), but - // formulas below convey geometric insight and creating functor is not free. + // NOTE(Frank): if Q is not asked we use formulas below instead of calling + // applyLeftJacobian(v), as they convey geometric insight and are faster. Vector3 t_parallel = w * w.dot(v); // translation parallel to axis Vector3 w_cross_v = w.cross(v); // translation orthogonal to axis Rot3 rotation = R.value_or(Rot3::Expmap(w)); @@ -314,7 +308,6 @@ const Point3& Pose3::translation(OptionalJacobian<3, 6> Hself) const { } /* ************************************************************************* */ - const Rot3& Pose3::rotation(OptionalJacobian<3, 6> Hself) const { if (Hself) { *Hself << I_3x3, Z_3x3; @@ -332,14 +325,14 @@ Matrix4 Pose3::matrix() const { /* ************************************************************************* */ Pose3 Pose3::transformPoseFrom(const Pose3& aTb, OptionalJacobian<6, 6> Hself, - OptionalJacobian<6, 6> HaTb) const { + OptionalJacobian<6, 6> HaTb) const { const Pose3& wTa = *this; return wTa.compose(aTb, Hself, HaTb); } /* ************************************************************************* */ Pose3 Pose3::transformPoseTo(const Pose3& wTb, OptionalJacobian<6, 6> Hself, - OptionalJacobian<6, 6> HwTb) const { + OptionalJacobian<6, 6> HwTb) const { if (Hself) *Hself = -wTb.inverse().AdjointMap() * AdjointMap(); if (HwTb) *HwTb = I_6x6; const Pose3& wTa = *this; @@ -348,7 +341,7 @@ Pose3 Pose3::transformPoseTo(const Pose3& wTb, OptionalJacobian<6, 6> Hself, /* ************************************************************************* */ Point3 Pose3::transformFrom(const Point3& point, OptionalJacobian<3, 6> Hself, - OptionalJacobian<3, 3> Hpoint) const { + OptionalJacobian<3, 3> Hpoint) const { // Only get matrix once, to avoid multiple allocations, // as well as multiple conversions in the Quaternion case const Matrix3 R = R_.matrix(); @@ -372,7 +365,7 @@ Matrix Pose3::transformFrom(const Matrix& points) const { /* ************************************************************************* */ Point3 Pose3::transformTo(const Point3& point, OptionalJacobian<3, 6> Hself, - OptionalJacobian<3, 3> Hpoint) const { + OptionalJacobian<3, 3> Hpoint) const { // Only get transpose once, to avoid multiple allocations, // as well as multiple conversions in the Quaternion case const Matrix3 Rt = R_.transpose(); @@ -478,7 +471,7 @@ std::optional Pose3::Align(const Point3Pairs &abPointPairs) { std::optional Pose3::Align(const Matrix& a, const Matrix& b) { if (a.rows() != 3 || b.rows() != 3 || a.cols() != b.cols()) { throw std::invalid_argument( - "Pose3:Align expects 3*N matrices of equal shape."); + "Pose3:Align expects 3*N matrices of equal shape."); } Point3Pairs abPointPairs; for (Eigen::Index j = 0; j < a.cols(); j++) { diff --git a/gtsam/geometry/tests/testPose3.cpp b/gtsam/geometry/tests/testPose3.cpp index 4ff4f9a854..3b3de67924 100644 --- a/gtsam/geometry/tests/testPose3.cpp +++ b/gtsam/geometry/tests/testPose3.cpp @@ -900,17 +900,6 @@ TEST(Pose3, ExpmapDerivative4) { } } -TEST( Pose3, ExpmapDerivativeQr) { - Vector6 w = Vector6::Random(); - w.head<3>().normalize(); - w.head<3>() = w.head<3>() * 0.9e-2; - Matrix3 actualQr = Pose3::ComputeQforExpmapDerivative(w, 0.01); - Matrix expectedH = numericalDerivative21 >(&Pose3::Expmap, w, {}); - Matrix3 expectedQr = expectedH.bottomLeftCorner<3, 3>(); - EXPECT(assert_equal(expectedQr, actualQr, 1e-6)); -} - /* ************************************************************************* */ TEST( Pose3, LogmapDerivative) { Matrix6 actualH; From 2aa36d4f7ab89e59138fbaa2c9914643069b77ef Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 16 Dec 2024 14:08:02 -0500 Subject: [PATCH 5/8] Modernize ExmapTranslation --- gtsam/geometry/Pose3.cpp | 56 +++++++++++++++++--------------- gtsam/geometry/Pose3.h | 10 +++--- gtsam/geometry/tests/testSO3.cpp | 1 + gtsam/navigation/NavState.cpp | 27 ++++++++------- 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/gtsam/geometry/Pose3.cpp b/gtsam/geometry/Pose3.cpp index 9f81204d63..6fbd2468fe 100644 --- a/gtsam/geometry/Pose3.cpp +++ b/gtsam/geometry/Pose3.cpp @@ -164,22 +164,22 @@ Pose3 Pose3::interpolateRt(const Pose3& T, double t) const { } /* ************************************************************************* */ -/** Modified from Murray94book version (which assumes w and v normalized?) */ Pose3 Pose3::Expmap(const Vector6& xi, OptionalJacobian<6, 6> Hxi) { // Get angular velocity omega and translational velocity v from twist xi const Vector3 w = xi.head<3>(), v = xi.tail<3>(); // Compute rotation using Expmap - Matrix3 Jw; - Rot3 R = Rot3::Expmap(w, Hxi ? &Jw : nullptr); + Matrix3 Jr; + Rot3 R = Rot3::Expmap(w, Hxi ? &Jr : nullptr); - // Compute translation and optionally its Jacobian in w + // Compute translation and optionally its Jacobian Q in w + // The Jacobian in v is the right Jacobian Jr of SO(3), which we already have. Matrix3 Q; - const Vector3 t = ExpmapTranslation(w, v, Hxi ? &Q : nullptr, R); + const Vector3 t = ExpmapTranslation(w, v, Hxi ? &Q : nullptr); if (Hxi) { - *Hxi << Jw, Z_3x3, - Q, Jw; + *Hxi << Jr, Z_3x3, // + Q, Jr; } return Pose3(R, t); @@ -251,35 +251,39 @@ Matrix3 Pose3::ComputeQforExpmapDerivative(const Vector6& xi, } /* ************************************************************************* */ +// NOTE(Frank): t = applyLeftJacobian(v) does the same as the intuitive formulas +// t_parallel = w * w.dot(v); // translation parallel to axis +// w_cross_v = w.cross(v); // translation orthogonal to axis +// t = (w_cross_v - Rot3::Expmap(w) * w_cross_v + t_parallel) / theta2; +// but functor does not need R, deals automatically with the case where theta2 +// is near zero, and also gives us the machinery for the Jacobians. Vector3 Pose3::ExpmapTranslation(const Vector3& w, const Vector3& v, OptionalJacobian<3, 3> Q, - const std::optional& R, + OptionalJacobian<3, 3> J, double nearZeroThreshold) { const double theta2 = w.dot(w); bool nearZero = (theta2 <= nearZeroThreshold); + // Instantiate functor for Dexp-related operations: + so3::DexpFunctor local(w, nearZero); + + // Call applyLeftJacobian which is faster than local.leftJacobian() * v if you + // don't need Jacobians, and returns Jacobian of t with respect to w if asked. + Matrix3 H; + Vector t = local.applyLeftJacobian(v, Q ? &H : nullptr); + + // We return Jacobians for use in Expmap, so we multiply with X, that + // translates from left to right for our right expmap convention: if (Q) { - // Instantiate functor for Dexp-related operations: - so3::DexpFunctor local(w, nearZero); - // X translate from left to right for our right expmap convention: Matrix3 X = local.rightJacobian() * local.leftJacobianInverse(); - Matrix3 H; - Vector t = local.applyLeftJacobian(v, H); *Q = X * H; - return t; - } else if (nearZero) { - // (I_3x3 + B * W + C * WW) * v with B->0.5, C->1/6 - Vector3 Wv = w.cross(v); - return v + 0.5 * Wv + w.cross(Wv) * so3::DexpFunctor::one_sixth; - } else { - // NOTE(Frank): if Q is not asked we use formulas below instead of calling - // applyLeftJacobian(v), as they convey geometric insight and are faster. - Vector3 t_parallel = w * w.dot(v); // translation parallel to axis - Vector3 w_cross_v = w.cross(v); // translation orthogonal to axis - Rot3 rotation = R.value_or(Rot3::Expmap(w)); - Vector3 t = (w_cross_v - rotation * w_cross_v + t_parallel) / theta2; - return t; } + + if (J) { + *J = local.rightJacobian(); // = X * local.leftJacobian(); + } + + return t; } /* ************************************************************************* */ diff --git a/gtsam/geometry/Pose3.h b/gtsam/geometry/Pose3.h index d7c280c803..04504de651 100644 --- a/gtsam/geometry/Pose3.h +++ b/gtsam/geometry/Pose3.h @@ -223,17 +223,19 @@ class GTSAM_EXPORT Pose3: public LieGroup { const Vector6& xi, double nearZeroThreshold = 1e-5); /** - * Compute the translation part of the exponential map, with derivative. + * Compute the translation part of the exponential map, with Jacobians. * @param w 3D angular velocity * @param v 3D velocity * @param Q Optionally, compute 3x3 Jacobian wrpt w - * @param R Optionally, precomputed as Rot3::Expmap(w) + * @param J Optionally, compute 3x3 Jacobian wrpt v = right Jacobian of SO(3) * @param nearZeroThreshold threshold for small values - * Note Q is 3x3 bottom-left block of SE3 Expmap right derivative matrix + * @note This function returns Jacobians Q and J corresponding to the bottom + * blocks of the SE(3) exponential, and translated from left to right from the + * applyLeftJacobian Jacobians. */ static Vector3 ExpmapTranslation(const Vector3& w, const Vector3& v, OptionalJacobian<3, 3> Q = {}, - const std::optional& R = {}, + OptionalJacobian<3, 3> J = {}, double nearZeroThreshold = 1e-5); using LieGroup::inverse; // version with derivative diff --git a/gtsam/geometry/tests/testSO3.cpp b/gtsam/geometry/tests/testSO3.cpp index d3313b7f1d..2006424563 100644 --- a/gtsam/geometry/tests/testSO3.cpp +++ b/gtsam/geometry/tests/testSO3.cpp @@ -304,6 +304,7 @@ TEST(SO3, JacobianLogmap) { EXPECT(assert_equal(Jexpected, Jactual)); } +//****************************************************************************** namespace test_cases { std::vector small{{0, 0, 0}, // {1e-5, 0, 0}, {0, 1e-5, 0}, {0, 0, 1e-5}, //, diff --git a/gtsam/navigation/NavState.cpp b/gtsam/navigation/NavState.cpp index b9d48a3cb8..c9cd244d38 100644 --- a/gtsam/navigation/NavState.cpp +++ b/gtsam/navigation/NavState.cpp @@ -114,24 +114,23 @@ NavState NavState::inverse() const { //------------------------------------------------------------------------------ NavState NavState::Expmap(const Vector9& xi, OptionalJacobian<9, 9> Hxi) { - // Get angular velocity w, translational velocity v, and acceleration a - Vector3 w = xi.head<3>(); - Vector3 rho = xi.segment<3>(3); - Vector3 nu = xi.tail<3>(); + // Get angular velocity w and components rho and nu from xi + Vector3 w = xi.head<3>(), rho = xi.segment<3>(3), nu = xi.tail<3>(); // Compute rotation using Expmap - Rot3 R = Rot3::Expmap(w); + Matrix3 Jr; + Rot3 R = Rot3::Expmap(w, Hxi ? &Jr : nullptr); - // Compute translations and optionally their Jacobians + // Compute translations and optionally their Jacobians Q in w + // The Jacobians with respect to rho and nu are equal to Jr Matrix3 Qt, Qv; - Vector3 t = Pose3::ExpmapTranslation(w, rho, Hxi ? &Qt : nullptr, R); - Vector3 v = Pose3::ExpmapTranslation(w, nu, Hxi ? &Qv : nullptr, R); + Vector3 t = Pose3::ExpmapTranslation(w, rho, Hxi ? &Qt : nullptr); + Vector3 v = Pose3::ExpmapTranslation(w, nu, Hxi ? &Qv : nullptr); if (Hxi) { - const Matrix3 Jw = Rot3::ExpmapDerivative(w); - *Hxi << Jw, Z_3x3, Z_3x3, - Qt, Jw, Z_3x3, - Qv, Z_3x3, Jw; + *Hxi << Jr, Z_3x3, Z_3x3, // + Qt, Jr, Z_3x3, // + Qv, Z_3x3, Jr; } return NavState(R, t, v); @@ -235,8 +234,8 @@ Matrix9 NavState::LogmapDerivative(const NavState& state) { Matrix3 Qt, Qv; const Rot3 R = Rot3::Expmap(w); - Pose3::ExpmapTranslation(w, rho, Qt, R); - Pose3::ExpmapTranslation(w, nu, Qv, R); + Pose3::ExpmapTranslation(w, rho, Qt); + Pose3::ExpmapTranslation(w, nu, Qv); const Matrix3 Jw = Rot3::LogmapDerivative(w); const Matrix3 Qt2 = -Jw * Qt * Jw; const Matrix3 Qv2 = -Jw * Qv * Jw; From db5b9dee65442245a8de1708fb1a21d72ee16e55 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 16 Dec 2024 18:35:17 -0500 Subject: [PATCH 6/8] Taylor expansion --- gtsam/geometry/SO3.cpp | 25 ++++--- gtsam/geometry/SO3.h | 16 ++--- gtsam/geometry/tests/testPose3.cpp | 101 +++++++++++++++++------------ 3 files changed, 83 insertions(+), 59 deletions(-) diff --git a/gtsam/geometry/SO3.cpp b/gtsam/geometry/SO3.cpp index 93bf072c53..41f7ce8107 100644 --- a/gtsam/geometry/SO3.cpp +++ b/gtsam/geometry/SO3.cpp @@ -33,6 +33,15 @@ namespace gtsam { //****************************************************************************** namespace so3 { +static constexpr double one_6th = 1.0 / 6.0; +static constexpr double one_12th = 1.0 / 12.0; +static constexpr double one_24th = 1.0 / 24.0; +static constexpr double one_60th = 1.0 / 60.0; +static constexpr double one_120th = 1.0 / 120.0; +static constexpr double one_180th = 1.0 / 180.0; +static constexpr double one_720th = 1.0 / 720.0; +static constexpr double one_1260th = 1.0 / 1260.0; + GTSAM_EXPORT Matrix99 Dcompose(const SO3& Q) { Matrix99 H; auto R = Q.matrix(); @@ -60,9 +69,9 @@ void ExpmapFunctor::init(bool nearZeroApprox) { 2.0 * s2 * s2; // numerically better than [1 - cos(theta)] B = one_minus_cos / theta2; } else { - // Limits as theta -> 0: - A = 1.0; - B = 0.5; + // Taylor expansion at 0 + A = 1.0 - theta2 * one_6th; + B = 0.5 - theta2 * one_24th; } } @@ -93,12 +102,12 @@ DexpFunctor::DexpFunctor(const Vector3& omega, bool nearZeroApprox) E = (2.0 * B - A) / theta2; F = (3.0 * C - B) / theta2; } else { - // Limit as theta -> 0 + // Taylor expansion at 0 // TODO(Frank): flipping signs here does not trigger any tests: harden! - C = one_sixth; - D = one_twelfth; - E = one_twelfth; - F = one_sixtieth; + C = one_6th - theta2 * one_120th; + D = one_12th + theta2 * one_720th; + E = one_12th - theta2 * one_180th; + F = one_60th - theta2 * one_1260th; } } diff --git a/gtsam/geometry/SO3.h b/gtsam/geometry/SO3.h index 2845fcce78..6e2e381011 100644 --- a/gtsam/geometry/SO3.h +++ b/gtsam/geometry/SO3.h @@ -138,8 +138,8 @@ struct GTSAM_EXPORT ExpmapFunctor { bool nearZero; // Ethan Eade's constants: - double A; // A = sin(theta) / theta or 1 for theta->0 - double B; // B = (1 - cos(theta)) / theta^2 or 0.5 for theta->0 + double A; // A = sin(theta) / theta + double B; // B = (1 - cos(theta)) /// Constructor with element of Lie algebra so(3) explicit ExpmapFunctor(const Vector3& omega, bool nearZeroApprox = false); @@ -159,14 +159,14 @@ struct GTSAM_EXPORT DexpFunctor : public ExpmapFunctor { const Vector3 omega; // Ethan's C constant used in Jacobians - double C; // (1 - A) / theta^2 or 1/6 for theta->0 + double C; // (1 - A) / theta^2 // Constant used in inverse Jacobians - double D; // (1 - A/2B) / theta2 or 1/12 for theta->0 + double D; // (1 - A/2B) / theta2 // Constants used in cross and doubleCross - double E; // (A - 2.0 * B) / theta2 or -1/12 for theta->0 - double F; // (B - 3.0 * C) / theta2 or -1/60 for theta->0 + double E; // (2B - A) / theta2 + double F; // (3C - B) / theta2 /// Constructor with element of Lie algebra so(3) explicit DexpFunctor(const Vector3& omega, bool nearZeroApprox = false); @@ -216,10 +216,6 @@ struct GTSAM_EXPORT DexpFunctor : public ExpmapFunctor { Vector3 applyLeftJacobianInverse(const Vector3& v, OptionalJacobian<3, 3> H1 = {}, OptionalJacobian<3, 3> H2 = {}) const; - - static constexpr double one_sixth = 1.0 / 6.0; - static constexpr double one_twelfth = 1.0 / 12.0; - static constexpr double one_sixtieth = 1.0 / 60.0; }; } // namespace so3 diff --git a/gtsam/geometry/tests/testPose3.cpp b/gtsam/geometry/tests/testPose3.cpp index 3b3de67924..ce74091f0a 100644 --- a/gtsam/geometry/tests/testPose3.cpp +++ b/gtsam/geometry/tests/testPose3.cpp @@ -835,38 +835,7 @@ TEST(Pose3, Align2) { } /* ************************************************************************* */ -TEST( Pose3, ExpmapDerivative1) { - Matrix6 actualH; - Vector6 w; w << 0.1, 0.2, 0.3, 4.0, 5.0, 6.0; - Pose3::Expmap(w,actualH); - Matrix expectedH = numericalDerivative21 >(&Pose3::Expmap, w, {}); - EXPECT(assert_equal(expectedH, actualH)); -} - -/* ************************************************************************* */ -TEST( Pose3, ExpmapDerivative2) { - Matrix6 actualH; - Vector6 w; w << 1.0, -2.0, 3.0, -10.0, -20.0, 30.0; - Pose3::Expmap(w,actualH); - Matrix expectedH = numericalDerivative21 >(&Pose3::Expmap, w, {}); - EXPECT(assert_equal(expectedH, actualH)); -} - -/* ************************************************************************* */ -TEST( Pose3, ExpmapDerivative3) { - Matrix6 actualH; - Vector6 w; w << 0.0, 0.0, 0.0, -10.0, -20.0, 30.0; - Pose3::Expmap(w,actualH); - Matrix expectedH = numericalDerivative21 >(&Pose3::Expmap, w, {}); - // Small angle approximation is not as precise as numerical derivative? - EXPECT(assert_equal(expectedH, actualH, 1e-5)); -} - -/* ************************************************************************* */ -TEST(Pose3, ExpmapDerivative4) { +TEST(Pose3, ExpmapDerivative) { // Iserles05an (Lie-group Methods) says: // scalar is easy: d exp(a(t)) / dt = exp(a(t)) a'(t) // matrix is hard: d exp(A(t)) / dt = exp(A(t)) dexp[-A(t)] A'(t) @@ -900,15 +869,65 @@ TEST(Pose3, ExpmapDerivative4) { } } -/* ************************************************************************* */ -TEST( Pose3, LogmapDerivative) { - Matrix6 actualH; - Vector6 w; w << 0.1, 0.2, 0.3, 4.0, 5.0, 6.0; - Pose3 p = Pose3::Expmap(w); - EXPECT(assert_equal(w, Pose3::Logmap(p,actualH), 1e-5)); - Matrix expectedH = numericalDerivative21 >(&Pose3::Logmap, p, {}); - EXPECT(assert_equal(expectedH, actualH)); +//****************************************************************************** +namespace test_cases { +std::vector small{{0, 0, 0}, // + {1e-5, 0, 0}, {0, 1e-5, 0}, {0, 0, 1e-5}, //, + {1e-4, 0, 0}, {0, 1e-4, 0}, {0, 0, 1e-4}}; +std::vector large{{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, + {0, 0, 1}, {.1, .2, .3}, {1, -2, 3}}; +auto omegas = [](bool nearZero) { return nearZero ? small : large; }; +std::vector vs{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, + {.4, .3, .2}, {4, 5, 6}, {-10, -20, 30}}; +} // namespace test_cases + +//****************************************************************************** +TEST(Pose3, ExpmapDerivatives) { + for (bool nearZero : {true, false}) { + for (const Vector3& w : test_cases::omegas(nearZero)) { + for (Vector3 v : test_cases::vs) { + const Vector6 xi = (Vector6() << w, v).finished(); + const Matrix6 expectedH = + numericalDerivative21 >( + &Pose3::Expmap, xi, {}); + Matrix actualH; + Pose3::Expmap(xi, actualH); + EXPECT(assert_equal(expectedH, actualH)); + } + } + } +} + +//****************************************************************************** +// Check logmap for all small values, as we don't want wrapping. +TEST(Pose3, Logmap) { + static constexpr bool nearZero = true; + for (const Vector3& w : test_cases::omegas(nearZero)) { + for (Vector3 v : test_cases::vs) { + const Vector6 xi = (Vector6() << w, v).finished(); + Pose3 pose = Pose3::Expmap(xi); + EXPECT(assert_equal(xi, Pose3::Logmap(pose), 1e-5)); + } + } +} + +//****************************************************************************** +// Check logmap derivatives for all values +TEST(Pose3, LogmapDerivatives) { + for (bool nearZero : {true, false}) { + for (const Vector3& w : test_cases::omegas(nearZero)) { + for (Vector3 v : test_cases::vs) { + const Vector6 xi = (Vector6() << w, v).finished(); + Pose3 pose = Pose3::Expmap(xi); + const Matrix6 expectedH = + numericalDerivative21 >( + &Pose3::Logmap, pose, {}); + Matrix actualH; + Pose3::Logmap(pose, actualH); + EXPECT(assert_equal(expectedH, actualH)); + } + } + } } /* ************************************************************************* */ From 49c2a040095a4d4d682a2c846f8a79bb8f87584c Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 16 Dec 2024 18:51:29 -0500 Subject: [PATCH 7/8] Address review comments --- gtsam/geometry/Pose3.cpp | 11 ++++++----- gtsam/geometry/Pose3.h | 2 +- gtsam/geometry/SO3.h | 4 ++++ gtsam/geometry/tests/testSO3.cpp | 1 - gtsam/navigation/NavState.cpp | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/gtsam/geometry/Pose3.cpp b/gtsam/geometry/Pose3.cpp index 6fbd2468fe..16dda3d1e0 100644 --- a/gtsam/geometry/Pose3.cpp +++ b/gtsam/geometry/Pose3.cpp @@ -11,20 +11,19 @@ /** * @file Pose3.cpp - * @brief 3D Pose + * @brief 3D Pose manifold SO(3) x R^3 and group SE(3) */ -#include +#include #include +#include +#include #include -#include #include #include #include -#include "gtsam/geometry/Rot3.h" - namespace gtsam { /** instantiate concept checks */ @@ -164,6 +163,8 @@ Pose3 Pose3::interpolateRt(const Pose3& T, double t) const { } /* ************************************************************************* */ +// Expmap is implemented in so3::ExpmapFunctor::expmap, based on Ethan Eade's +// elegant Lie group document, at https://www.ethaneade.org/lie.pdf. Pose3 Pose3::Expmap(const Vector6& xi, OptionalJacobian<6, 6> Hxi) { // Get angular velocity omega and translational velocity v from twist xi const Vector3 w = xi.head<3>(), v = xi.tail<3>(); diff --git a/gtsam/geometry/Pose3.h b/gtsam/geometry/Pose3.h index 04504de651..81a9848e2c 100644 --- a/gtsam/geometry/Pose3.h +++ b/gtsam/geometry/Pose3.h @@ -11,7 +11,7 @@ /** *@file Pose3.h - *@brief 3D Pose + * @brief 3D Pose manifold SO(3) x R^3 and group SE(3) */ // \callgraph diff --git a/gtsam/geometry/SO3.h b/gtsam/geometry/SO3.h index 6e2e381011..163ae66231 100644 --- a/gtsam/geometry/SO3.h +++ b/gtsam/geometry/SO3.h @@ -132,6 +132,8 @@ GTSAM_EXPORT Matrix99 Dcompose(const SO3& R); // functor also implements dedicated methods to apply dexp and/or inv(dexp). /// Functor implementing Exponential map +/// Math is based on Ethan Eade's elegant Lie group document, at +/// https://www.ethaneade.org/lie.pdf. struct GTSAM_EXPORT ExpmapFunctor { const double theta2, theta; const Matrix3 W, WW; @@ -155,6 +157,8 @@ struct GTSAM_EXPORT ExpmapFunctor { }; /// Functor that implements Exponential map *and* its derivatives +/// Math extends Ethan theme of elegant I + aW + bWW expressions. +/// See https://www.ethaneade.org/lie.pdf expmap (82) and left Jacobian (83). struct GTSAM_EXPORT DexpFunctor : public ExpmapFunctor { const Vector3 omega; diff --git a/gtsam/geometry/tests/testSO3.cpp b/gtsam/geometry/tests/testSO3.cpp index 2006424563..c6fd521613 100644 --- a/gtsam/geometry/tests/testSO3.cpp +++ b/gtsam/geometry/tests/testSO3.cpp @@ -19,7 +19,6 @@ #include #include #include -#include using namespace std::placeholders; using namespace std; diff --git a/gtsam/navigation/NavState.cpp b/gtsam/navigation/NavState.cpp index c9cd244d38..bd482132e4 100644 --- a/gtsam/navigation/NavState.cpp +++ b/gtsam/navigation/NavState.cpp @@ -114,7 +114,7 @@ NavState NavState::inverse() const { //------------------------------------------------------------------------------ NavState NavState::Expmap(const Vector9& xi, OptionalJacobian<9, 9> Hxi) { - // Get angular velocity w and components rho and nu from xi + // Get angular velocity w and components rho (for t) and nu (for v) from xi Vector3 w = xi.head<3>(), rho = xi.segment<3>(3), nu = xi.tail<3>(); // Compute rotation using Expmap From 8e473c04e82eacbef373cc8efdc8be59eb01e4e1 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 16 Dec 2024 22:46:16 -0500 Subject: [PATCH 8/8] Address Quaternion on Ubuntu accuracy --- gtsam/geometry/tests/testPose3.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gtsam/geometry/tests/testPose3.cpp b/gtsam/geometry/tests/testPose3.cpp index ce74091f0a..b04fb1982a 100644 --- a/gtsam/geometry/tests/testPose3.cpp +++ b/gtsam/geometry/tests/testPose3.cpp @@ -906,7 +906,7 @@ TEST(Pose3, Logmap) { for (Vector3 v : test_cases::vs) { const Vector6 xi = (Vector6() << w, v).finished(); Pose3 pose = Pose3::Expmap(xi); - EXPECT(assert_equal(xi, Pose3::Logmap(pose), 1e-5)); + EXPECT(assert_equal(xi, Pose3::Logmap(pose))); } } } @@ -924,7 +924,13 @@ TEST(Pose3, LogmapDerivatives) { &Pose3::Logmap, pose, {}); Matrix actualH; Pose3::Logmap(pose, actualH); +#ifdef GTSAM_USE_QUATERNIONS + // TODO(Frank): Figure out why quaternions are not as accurate. + // Hint: 6 cases fail on Ubuntu 22.04, but none on MacOS. + EXPECT(assert_equal(expectedH, actualH, 1e-7)); +#else EXPECT(assert_equal(expectedH, actualH)); +#endif } } }