Skip to content

Commit 07a66d6

Browse files
authored
Merge pull request #2317 from borglab/fix/sl4
Fix SL4 to determinant issue
2 parents ef33d45 + b0826bb commit 07a66d6

File tree

3 files changed

+87
-11
lines changed

3 files changed

+87
-11
lines changed

.github/workflows/build-macos.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ jobs:
9595
- name: Install Boost
9696
if: contains(matrix.name, 'boost') && matrix.build_type == 'Release'
9797
run: |
98+
brew update
9899
brew upgrade boost --quiet || brew install boost --quiet
99100
echo "GTSAM_USE_BOOST_FEATURES=ON" >> $GITHUB_ENV
100101
echo "GTSAM_ENABLE_BOOST_SERIALIZATION=ON" >> $GITHUB_ENV

gtsam/geometry/SL4.cpp

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
#include <gtsam/geometry/SL4.h>
88

99
// To use exp(), log()
10+
#include <cmath>
11+
#include <limits>
1012
#include <unsupported/Eigen/MatrixFunctions>
13+
#include <Eigen/SVD>
1114

1215
using namespace std;
1316

@@ -65,19 +68,39 @@ Eigen::Matrix<double, 15, 16> setAlgtoVecMatrix() {
6568
// ALG_TO_VEC * VEC_TO_ALG is equals to I_15x15
6669
const Eigen::Matrix<double, 16, 15> VEC_TO_ALG = setVecToAlgMatrix();
6770
const Eigen::Matrix<double, 15, 16> ALG_TO_VEC = setAlgtoVecMatrix();
71+
6872
} // namespace
6973
namespace gtsam {
7074

7175
SL4::SL4(const Matrix44& pose) {
72-
double det = pose.determinant();
73-
if (det <= 0.0) {
76+
// Compute SVD: pose = U * S * V^T
77+
const Eigen::JacobiSVD<Matrix44> svd(pose, Eigen::ComputeFullU | Eigen::ComputeFullV);
78+
79+
Matrix44 U = svd.matrixU();
80+
const Matrix44 V = svd.matrixV();
81+
const Vector4 S = svd.singularValues();
82+
83+
// Handle Orientation (Negative Determinant / Reflection)
84+
const double detUV = (U * V.transpose()).determinant();
85+
86+
if (detUV < 0.0) {
87+
U.col(3) = -U.col(3);
88+
}
89+
90+
// Reconstruct the matrix with corrected orientation
91+
const Matrix44 M_corrected = U * S.asDiagonal() * V.transpose();
92+
const double current_det_mag = S.prod();
93+
94+
// Check for Singularity
95+
if (current_det_mag <= std::numeric_limits<double>::epsilon() || !std::isfinite(current_det_mag)) {
7496
throw std::runtime_error(
75-
"Matrix determinant must be positive for SL(4) normalization. Got det "
76-
"= " +
77-
std::to_string(det));
97+
"SL4 Constructor: Input matrix is singular or invalid. "
98+
"SVD singular values product = " + std::to_string(current_det_mag));
7899
}
79100

80-
T_ = pose / std::pow(det, 1.0 / 4.0);
101+
// Normalize: T = M / det^(1/4)
102+
const double scale = std::pow(current_det_mag, 0.25);
103+
T_ = M_corrected / scale;
81104
}
82105

83106
/* ************************************************************************* */
@@ -89,9 +112,18 @@ bool SL4::equals(const SL4& sl4, double tol) const {
89112
}
90113
/* ************************************************************************* */
91114
SL4 SL4::ChartAtOrigin::Retract(const Vector15& v, ChartJacobian H) {
92-
SL4 retracted(I_4x4 + Hat(v));
93115
if (H) throw std::runtime_error("SL4::Retract: Jacobian not implemented.");
94-
return retracted;
116+
117+
const Matrix44 candidate = I_4x4 + Hat(v);
118+
const double det = candidate.determinant();
119+
120+
// Use fast first-order retraction when it stays inside SL(4); fall back to
121+
// the true exponential map otherwise to avoid invalid determinants.
122+
if (det > 0.0 && std::isfinite(det)) {
123+
return SL4(candidate);
124+
}
125+
126+
return Expmap(v);
95127
}
96128

97129
/* ************************************************************************* */
@@ -114,11 +146,17 @@ SL4 SL4::Expmap(const Vector& xi, SL4Jacobian H) {
114146

115147
// NOTE(hlim):
116148
// The cost of the computation is approximately 20n^3 for matrices of size n.
117-
// The number 20 depends weakly on the norm of the matrix.
118-
// See
149+
// The number 20 depends weakly on the norm of the matrix. See
119150
// https://eigen.tuxfamily.org/dox/unsupported/group__MatrixFunctions__Module.html
120151

121-
return SL4(A.exp());
152+
// For SL(4), the Lie algebra consists of trace-zero 4x4 matrices.
153+
// The exponential of a trace-zero matrix should have determinant 1 by the property:
154+
// det(exp(A)) = exp(trace(A)) = exp(0) = 1.
155+
// However, for large tangent vectors, numerical errors in the matrix exponential
156+
// can cause the determinant to drift from 1. The constructor handles normalization.
157+
158+
Matrix44 expA = A.exp();
159+
return SL4(expA);
122160
}
123161

124162
/* ************************************************************************* */

gtsam/geometry/tests/testSL4.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ static const Vector15 xi2 =
3535
0.12, 0.11, 0.15, 0.13, 0.14)
3636
.finished();
3737

38+
// define xi_large - moderately large values to test numerical stability
39+
// while remaining within the feasible range for matrix exponential
40+
static const Vector15 xi_large =
41+
(Vector15() << 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4,
42+
1.5, 1.6, 1.7, 1.8, 1.9)
43+
.finished();
3844
// Create a SL4
3945
const Matrix4 T_matrix =
4046
(Matrix4() << 1, 0, 0, 1, 0, 1, 0, 2, 0, 0, 1, 3, 0, 0, 0, 1).finished();
@@ -60,6 +66,21 @@ TEST(SL4, Identity) {
6066
TEST(SL4, Expmap) {
6167
SL4 expected(SL4::Expmap(xi0));
6268
EXPECT(assert_equal(expected, SL4::Expmap(xi0), 1e-8));
69+
70+
// Verify that the result has determinant 1
71+
Matrix4 T = expected.matrix();
72+
double det = T.determinant();
73+
EXPECT_DOUBLES_EQUAL(1.0, det, 1e-8);
74+
}
75+
76+
/* ************************************************************************* */
77+
TEST(SL4, ExpmapLargeTangent) {
78+
// Test with large tangent vector - the numerical stability fix should handle this
79+
SL4 result = SL4::Expmap(xi_large);
80+
// Verify determinant is 1
81+
Matrix4 T = result.matrix();
82+
double det = T.determinant();
83+
EXPECT_DOUBLES_EQUAL(1.0, det, 1e-6); // Looser tolerance for large inputs
6384
}
6485

6586
/* ************************************************************************* */
@@ -74,6 +95,22 @@ TEST(SL4, Retract) {
7495
SL4 actual = SL4::Retract(xi);
7596
SL4 expected(I_4x4 + SL4::Hat(xi));
7697
EXPECT(assert_equal(expected, actual, 1e-8));
98+
99+
// Verify that the result has determinant 1
100+
Matrix4 T = actual.matrix();
101+
double det = T.determinant();
102+
EXPECT_DOUBLES_EQUAL(1.0, det, 1e-8);
103+
}
104+
105+
/* ************************************************************************* */
106+
TEST(SL4, RetractLargeTangent) {
107+
// Test with large tangent vector - the retraction will fall back to Expmap
108+
// for large inputs that produce invalid first-order approximations.
109+
SL4 result = SL4::Retract(xi_large);
110+
// Verify determinant is 1
111+
Matrix4 T = result.matrix();
112+
double det = T.determinant();
113+
EXPECT_DOUBLES_EQUAL(1.0, det, 1e-6); // Looser tolerance for large inputs
77114
}
78115

79116
/* ************************************************************************* */

0 commit comments

Comments
 (0)