Add Galilean 3 group

release/4.3a0
mkielo3 2025-05-01 00:30:46 -04:00
parent 07c9bc8ffa
commit 42985c6326
4 changed files with 1522 additions and 411 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ xcode/
# MyST build outputs # MyST build outputs
_build _build
examples/evaluatePreintegrationNavState.cpp

View File

@ -10,7 +10,7 @@
* @file Gal3.cpp * @file Gal3.cpp
* @brief Implementation of 3D Galilean Group SGal(3) state * @brief Implementation of 3D Galilean Group SGal(3) state
* @author Based on Python implementation by User * @author Based on Python implementation by User
* @date April 29, 2025 * @date April 30, 2025 // Updated Date
*/ */
#include <gtsam/navigation/Gal3.h> // Note: Adjust path if needed #include <gtsam/navigation/Gal3.h> // Note: Adjust path if needed
@ -26,140 +26,125 @@
namespace gtsam { namespace gtsam {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Static Helper Functions Implementation // Constants and Helper function for Expmap/Logmap (Implementation Detail)
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace { // Anonymous namespace for internal linkage
Matrix3 Gal3::SO3_LeftJacobian(const Vector3& theta) { // Define the threshold locally within the cpp file
using std::cos; constexpr double kSmallAngleThreshold = 1e-10;
using std::sin;
using std::sqrt;
const double angle_sq = theta.squaredNorm(); Matrix3 Calculate_E(const Vector3& theta) {
using std::cos;
using std::sin;
using std::sqrt;
// Use Taylor series expansion for small angles const double angle_sq = theta.squaredNorm();
if (angle_sq < kSmallAngleThreshold * kSmallAngleThreshold) { const Matrix3 W = skewSymmetric(theta);
// Jl(w) approx = I + 0.5*[w]x + (1/6)*[w]x^2 const Matrix3 W2 = W * W;
const Matrix3 W = skewSymmetric(theta);
return Matrix3::Identity() + 0.5 * W + (1.0/6.0) * W * W;
}
const double angle = sqrt(angle_sq); Matrix3 E = 0.5 * Matrix3::Identity();
const Matrix3 W = skewSymmetric(theta);
const Matrix3 W2 = W * W;
const double inv_angle_sq = 1.0 / angle_sq; // Small angle approximation - Use locally defined constant
const double sin_angle = sin(angle); if (angle_sq < kSmallAngleThreshold * kSmallAngleThreshold) {
const double cos_angle = cos(angle); E += (1.0 / 6.0) * W + (1.0 / 24.0) * W2;
return E;
}
// coeff1 = (1 - cos(theta)) / theta^2 const double angle = sqrt(angle_sq);
const double coeff1 = (1.0 - cos_angle) * inv_angle_sq; const double sin_angle = sin(angle);
// coeff2 = (theta - sin(theta)) / theta^3 const double cos_angle = cos(angle);
const double coeff2 = (angle - sin_angle) / (angle_sq * angle);
return Matrix3::Identity() + coeff1 * W + coeff2 * W2; // Coefficients from manif::SGal3TangentBase::exp()
const double A = (angle - sin_angle) / (angle_sq * angle); // A = (theta - sin(theta)) / theta^3
const double B = (angle_sq + 2.0 * cos_angle - 2.0) / (2.0 * angle_sq * angle_sq); // B = (theta^2 + 2*cos(theta) - 2) / (2 * theta^4)
E += A * W + B * W2;
return E;
}
} // end anonymous namespace
//------------------------------------------------------------------------------
// Static Constructor/Create functions (NavState order: 1)
//------------------------------------------------------------------------------
Gal3 Gal3::Create(const Rot3& R, const Point3& r, const Velocity3& v, double t,
OptionalJacobian<10, 3> H1, OptionalJacobian<10, 3> H2,
OptionalJacobian<10, 3> H3, OptionalJacobian<10, 1> H4) {
// H = d Logmap(Create(R,r,v,t)) / d [w, r_vec, v_vec, t]
// Note: Derivatives are w.r.t tangent space elements at the identity
// for inputs R, r, v, t. However, the standard way is derivative
// w.r.t minimal tangent spaces of inputs. Let's assume standard approach.
if (H1) { // d xi_out / d omega_R (3x3)
*H1 = Matrix::Zero(10, 3);
H1->block<3, 3>(6, 0) = Matrix3::Identity(); // d theta / d omega_R = I
}
if (H2) { // d xi_out / d delta_r (3x3)
*H2 = Matrix::Zero(10, 3);
H2->block<3, 3>(0, 0) = R.transpose(); // d rho / d delta_r = R^T
}
if (H3) { // d xi_out / d delta_v (3x3)
*H3 = Matrix::Zero(10, 3);
H3->block<3, 3>(3, 0) = R.transpose(); // d nu / d delta_v = R^T
}
if (H4) { // d xi_out / d delta_t (10x1)
*H4 = Matrix::Zero(10, 1);
// As derived: d xi / dt = [-R^T v; 0; 0; 1]
Vector3 drho_dt = -R.transpose() * v; // Corrected derivative for rho
H4->block<3, 1>(0, 0) = drho_dt; // Assign to rho rows (0-2)
// d nu / dt = 0
// d theta / dt = 0
(*H4)(9, 0) = 1.0; // d t_tan / dt = 1
}
return Gal3(R, r, v, t);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Matrix3 Gal3::SO3_LeftJacobianInverse(const Vector3& theta) { Gal3 Gal3::FromPoseVelocityTime(const Pose3& pose, const Velocity3& v, double t,
using std::cos; OptionalJacobian<10, 6> H1, OptionalJacobian<10, 3> H2,
using std::sin; OptionalJacobian<10, 1> H3) {
using std::sqrt; const Rot3& R = pose.rotation();
using std::tan; const Point3& r = pose.translation(); // Get r for H3 calculation if needed below
if (H1) { // d xi_out / d xi_pose (10x6), xi_pose = [omega_R; v_r]
const double angle_sq = theta.squaredNorm(); *H1 = Matrix::Zero(10, 6);
const Matrix3 W = skewSymmetric(theta); // d theta / d omega_pose = I (rows 6-8, cols 0-2)
H1->block<3, 3>(6, 0) = Matrix3::Identity();
// Use Taylor series expansion for small angles // d rho / d v_r = I (rows 0-2, cols 3-5) - Corrected
if (angle_sq < kSmallAngleThreshold * kSmallAngleThreshold) { H1->block<3, 3>(0, 3) = Matrix3::Identity();
// Jl(w)^-1 approx = I - 0.5*W + (1/12)*W^2 // Other blocks (d_rho/d_omega, d_nu/d_omega, d_nu/d_vr, d_tt/d_omega, d_tt/d_vr) are zero.
return Matrix3::Identity() - 0.5 * W + (1.0/12.0) * W * W;
} }
if (H2) { // d xi_out / d v_in (10x3)
const double angle = sqrt(angle_sq); *H2 = Matrix::Zero(10, 3);
const double half_angle = 0.5 * angle; H2->block<3, 3>(3, 0) = R.transpose(); // d(nu_out)/d(v_in) = R^T
// Formula: I - 0.5*W + (1/theta^2 - cot(theta/2)/(2*theta)) * W^2
// which is I - 0.5*W + (1 - 0.5*theta*cot(0.5*theta))/theta^2 * W^2
double cot_half_angle;
// Avoid division by zero/very small numbers for tan(half_angle)
if (std::abs(sin(half_angle)) < kSmallAngleThreshold) {
// If sin(half_angle) is near zero, theta is near multiples of 2*pi.
// cot(x) ~ 1/x for small x. Let x = half_angle.
// If half_angle is very small, use Taylor expansion of the coefficient:
// (1 - 0.5*theta*cot(0.5*theta))/theta^2 -> 1/12 + O(theta^2)
if (std::abs(half_angle) < kSmallAngleThreshold) {
// Use Taylor approx for the coefficient directly
const double coeff = 1.0 / 12.0; // + angle_sq / 720.0 + ...; // Higher order if needed
return Matrix3::Identity() - 0.5 * W + coeff * (W * W);
} else {
// theta is near non-zero multiples of 2*pi. tan(half_angle) is near zero.
// cot becomes large. Use limit or high-precision calculation if needed.
// For now, use standard tan. May need robustness improvements.
cot_half_angle = cos(half_angle) / sin(half_angle); // Avoid 1.0/tan()
}
} else {
cot_half_angle = cos(half_angle) / sin(half_angle); // Avoid 1.0/tan()
} }
if (H3) { // d xi_out / d t_in (10x1)
const double coeff = (1.0 - 0.5 * angle * cot_half_angle) / angle_sq; *H3 = Matrix::Zero(10, 1);
return Matrix3::Identity() - 0.5 * W + coeff * (W * W); // As derived: d xi / dt = [-R^T v; 0; 0; 1]
Vector3 drho_dt = -R.transpose() * v; // Corrected derivative for rho
// Alternative using GTSAM SO3 properties (verify this matches J_l^-1): H3->block<3, 1>(0, 0) = drho_dt; // Assign to rho rows (0-2)
// J_l(theta)^-1 = J_r(-theta). // d nu / dt = 0
// return gtsam::so3::DexpFunctor(-theta).rightJacobian(); // d theta / dt = 0
} (*H3)(9, 0) = 1.0; // d t_tan / dt = 1
//------------------------------------------------------------------------------
Matrix3 Gal3::Calculate_E(const Vector3& theta) {
using std::cos;
using std::sin;
using std::sqrt;
const double angle_sq = theta.squaredNorm();
const Matrix3 W = skewSymmetric(theta);
const Matrix3 W2 = W * W;
Matrix3 E = 0.5 * Matrix3::Identity();
// Small angle approximation (from manif SGal3Tangent_base.h / Expmap formula)
// E approx = 0.5*I + (1/6)*W + (1/24)*W^2
if (angle_sq < kSmallAngleThreshold * kSmallAngleThreshold) {
E += (1.0 / 6.0) * W + (1.0 / 24.0) * W2;
return E;
} }
// Pass r (translation from pose) to the Gal3 constructor
const double angle = sqrt(angle_sq); return Gal3(R, r, v, t);
const double sin_angle = sin(angle);
const double cos_angle = cos(angle);
// Coefficients A and B from manif::SGal3TangentBase::exp()
// A = (theta - sin(theta)) / theta^3
const double A = (angle - sin_angle) / (angle_sq * angle);
// B = (theta^2 + 2*cos(theta) - 2) / (2 * theta^4)
const double B = (angle_sq + 2.0 * cos_angle - 2.0) / (2.0 * angle_sq * angle_sq);
E += A * W + B * W2;
return E;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constructors // Constructors (NavState order: 2 - Included in Constructor section)
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Gal3::Gal3(const Matrix5& M) { Gal3::Gal3(const Matrix5& M) {
// CORRECTED: Use head(N) with correct sizes for checks // Ensure matrix adheres to SGal(3) structure
if (std::abs(M(3, 3) - 1.0) > 1e-9 || std::abs(M(4, 4) - 1.0) > 1e-9 || if (std::abs(M(3, 3) - 1.0) > 1e-9 || std::abs(M(4, 4) - 1.0) > 1e-9 ||
M.row(4).head(4).norm() > 1e-9 || M.row(3).head(3).norm() > 1e-9) { // Row 4 first 4 = 0; Row 3 first 3 = 0 M.row(4).head(4).norm() > 1e-9 || M.row(3).head(3).norm() > 1e-9) { // Row 4 first 4 = 0; Row 3 first 3 = 0
throw std::invalid_argument("Invalid Gal3 matrix structure: Check zero blocks and diagonal ones."); throw std::invalid_argument("Invalid Gal3 matrix structure: Check zero blocks and diagonal ones.");
} }
R_ = Rot3(M.block<3, 3>(0, 0)); R_ = Rot3(M.block<3, 3>(0, 0));
v_ = M.block<3, 1>(0, 3); // Manif: v is in column 3 (0-indexed) v_ = M.block<3, 1>(0, 3); // v is in column 3
r_ = Point3(M.block<3, 1>(0, 4)); // Manif: r is in column 4 r_ = Point3(M.block<3, 1>(0, 4)); // r is in column 4
t_ = M(3, 4); // Manif: t is at (3, 4) t_ = M(3, 4); // t is at (3, 4)
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Component Access // Component Access (NavState order: 3)
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const Rot3& Gal3::rotation(OptionalJacobian<3, 10> H) const { const Rot3& Gal3::rotation(OptionalJacobian<3, 10> H) const {
if (H) { if (H) {
@ -171,9 +156,17 @@ const Rot3& Gal3::rotation(OptionalJacobian<3, 10> H) const {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const Point3& Gal3::translation(OptionalJacobian<3, 10> H) const { const Point3& Gal3::translation(OptionalJacobian<3, 10> H) const {
// H = d r / d xi = [dr/drho, dr/dnu, dr/dtheta, dr/dt_tan]
if (H) { if (H) {
*H = Matrix::Zero(3, 10); *H = Matrix::Zero(3, 10);
H->block<3, 3>(0, 0) = R_.matrix(); // Derivative w.r.t rho (local tangent space) // dr/drho = R (local tangent space)
H->block<3,3>(0, 0) = R_.matrix();
// dr/dnu = 0
// H->block<3,3>(0, 3) = Matrix3::Zero(); // Already zero
// dr/dtheta = 0
// H->block<3,3>(0, 6) = Matrix3::Zero(); // Already zero
// dr/dt_tan = v (Corrected)
H->block<3,1>(0, 9) = v_;
} }
return r_; return r_;
} }
@ -197,21 +190,32 @@ const double& Gal3::time(OptionalJacobian<1, 10> H) const {
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Matrix Representation // Matrix Representation (NavState order: 4)
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Matrix5 Gal3::matrix() const { Matrix5 Gal3::matrix() const {
Matrix5 M = Matrix5::Identity(); Matrix5 M = Matrix5::Identity();
M.block<3, 3>(0, 0) = R_.matrix(); M.block<3, 3>(0, 0) = R_.matrix();
M.block<3, 1>(0, 3) = v_; // Manif: v in col 3 M.block<3, 1>(0, 3) = v_;
M.block<3, 1>(0, 4) = Vector3(r_); // Manif: r in col 4 // CORRECTED: Assign Vector3 from Point3 M.block<3, 1>(0, 4) = Vector3(r_);
M(3, 4) = t_; // Manif: t at (3, 4) M(3, 4) = t_;
M.block<1,3>(3,0).setZero(); // Zero out row 3, cols 0-2 M.block<1,3>(3,0).setZero(); // Zero out row 3, cols 0-2
M.block<1,4>(4,0).setZero(); // Zero out row 4, cols 0-3 M.block<1,4>(4,0).setZero(); // Zero out row 4, cols 0-3
return M; return M;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Testable Requirements // Stream operator (NavState order: 5)
//------------------------------------------------------------------------------
std::ostream& operator<<(std::ostream& os, const Gal3& state) {
os << "R: " << state.R_ << "\n";
os << "r: " << state.r_.transpose() << "\n";
os << "v: " << state.v_.transpose() << "\n";
os << "t: " << state.t_;
return os;
}
//------------------------------------------------------------------------------
// Testable Requirements (NavState order: 6)
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
void Gal3::print(const std::string& s) const { void Gal3::print(const std::string& s) const {
std::cout << (s.empty() ? "" : s + " "); std::cout << (s.empty() ? "" : s + " ");
@ -227,13 +231,12 @@ bool Gal3::equals(const Gal3& other, double tol) const {
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Group Operations // Group Operations (NavState order: 7)
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Gal3 Gal3::inverse() const { Gal3 Gal3::inverse() const {
// Formula: R_inv = R', v_inv = -R'*v, r_inv = -R'*(r - t*v), t_inv = -t // Formula: R_inv = R', v_inv = -R'*v, r_inv = -R'*(r - t*v), t_inv = -t
const Rot3 Rinv = R_.inverse(); const Rot3 Rinv = R_.inverse();
const Velocity3 v_inv = -(Rinv.rotate(v_)); const Velocity3 v_inv = -(Rinv.rotate(v_));
// CORRECTED: Cast r_ to Vector3 for subtraction
const Point3 r_inv = -(Rinv.rotate(Vector3(r_) - t_ * v_)); const Point3 r_inv = -(Rinv.rotate(Vector3(r_) - t_ * v_));
const double t_inv = -t_; const double t_inv = -t_;
return Gal3(Rinv, r_inv, v_inv, t_inv); return Gal3(Rinv, r_inv, v_inv, t_inv);
@ -242,24 +245,21 @@ Gal3 Gal3::inverse() const {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Gal3 Gal3::compose(const Gal3& other, OptionalJacobian<10, 10> H1, OptionalJacobian<10, 10> H2) const { Gal3 Gal3::compose(const Gal3& other, OptionalJacobian<10, 10> H1, OptionalJacobian<10, 10> H2) const {
// Formula: R = R1*R2, v = R1*v2 + v1, r = R1*r2 + t2*v1 + r1, t = t1+t2 // Formula: R = R1*R2, v = R1*v2 + v1, r = R1*r2 + t2*v1 + r1, t = t1+t2
// where this = g1, other = g2
const Gal3& g1 = *this; const Gal3& g1 = *this;
const Gal3& g2 = other; const Gal3& g2 = other;
const Rot3 R_comp = g1.R_.compose(g2.R_); const Rot3 R_comp = g1.R_.compose(g2.R_);
// CORRECTED: Cast g1.r_ to Vector3 for addition
const Vector3 r_comp_vec = g1.R_.rotate(g2.r_) + g2.t_ * g1.v_ + Vector3(g1.r_); const Vector3 r_comp_vec = g1.R_.rotate(g2.r_) + g2.t_ * g1.v_ + Vector3(g1.r_);
const Velocity3 v_comp = g1.R_.rotate(g2.v_) + g1.v_; const Velocity3 v_comp = g1.R_.rotate(g2.v_) + g1.v_;
const double t_comp = g1.t_ + g2.t_; const double t_comp = g1.t_ + g2.t_;
Gal3 result(R_comp, Point3(r_comp_vec), v_comp, t_comp); Gal3 result(R_comp, Point3(r_comp_vec), v_comp, t_comp);
// Jacobians require correct AdjointMap implementation
if (H1) { if (H1) {
*H1 = g2.inverse().AdjointMap(); *H1 = g2.inverse().AdjointMap(); // Jacobian w.r.t this is Ad(g2.inverse())
} }
if (H2) { if (H2) {
*H2 = Matrix10::Identity(); *H2 = Matrix10::Identity(); // Jacobian w.r.t other is Identity
} }
return result; return result;
@ -269,69 +269,203 @@ Gal3 Gal3::compose(const Gal3& other, OptionalJacobian<10, 10> H1, OptionalJacob
Gal3 Gal3::between(const Gal3& other, OptionalJacobian<10, 10> H1, OptionalJacobian<10, 10> H2) const { Gal3 Gal3::between(const Gal3& other, OptionalJacobian<10, 10> H1, OptionalJacobian<10, 10> H2) const {
const Gal3& g1 = *this; const Gal3& g1 = *this;
const Gal3& g2 = other; const Gal3& g2 = other;
Gal3 g1_inv = g1.inverse();
Gal3 result = g1_inv.compose(g2); // g1_inv * g2
// Jacobians require correct AdjointMap implementation // Step 1: Calculate g1.inverse() and its Jacobian w.r.t. g1
// H_inv_g1 = -Ad(g1_inv)
Matrix10 H_inv_g1;
// NOTE: This internal call to inverse *does* take the Jacobian argument
Gal3 g1_inv = g1.inverse(H_inv_g1);
// Step 2: Calculate g2.inverse() needed for compose Jacobian
Gal3 g2_inv = g2.inverse(); // This calls the public inverse() without Jacobian
// Step 3: Calculate compose(g1_inv, g2) and its Jacobians w.r.t. g1_inv and g2
// H_comp_g1inv = Ad(g2_inv)
// H_comp_g2 = Identity
Matrix10 H_comp_g1inv, H_comp_g2;
Gal3 result = g1_inv.compose(g2, H_comp_g1inv, H_comp_g2);
// Step 4: Apply chain rule explicitly for H1
if (H1) { if (H1) {
*H1 = -result.AdjointMap(); // H1 = d(compose)/d(g1_inv) * d(g1_inv)/d(g1) = H_comp_g1inv * H_inv_g1
*H1 = H_comp_g1inv * H_inv_g1;
} }
// Step 5: Assign H2
if (H2) { if (H2) {
*H2 = g1_inv.AdjointMap(); // H2 = d(compose)/d(g2) = H_comp_g2 = Identity
*H2 = H_comp_g2; // Should be Identity()
} }
return result; return result;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Manifold Operations (Retract/Local) // Lie Group Static Functions (NavState order: 8)
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Gal3 Gal3::retract(const Vector10& xi, OptionalJacobian<10, 10> H1, OptionalJacobian<10, 10> H2) const { Gal3 Gal3::Expmap(const Vector10& xi, OptionalJacobian<10, 10> Hxi) {
Gal3 g_exp = Gal3::Expmap(xi); const Vector3 rho_tan = rho(xi);
Matrix10 H_exp_xi = Matrix10::Zero(); // Placeholder const Vector3 nu_tan = nu(xi);
if (H2) { const Vector3 theta_tan = theta(xi);
H_exp_xi = Gal3::ExpmapDerivative(xi); // Needs ExpmapDerivative implemented const double t_tan_val = t_tan(xi)(0);
}
Matrix10 H_comp_this, H_comp_gexp; const Rot3 R = Rot3::Expmap(theta_tan);
Gal3 result = this->compose(g_exp, H_comp_this, H_comp_gexp); const gtsam::so3::DexpFunctor dexp_functor(theta_tan);
const Matrix3 Jl_theta = dexp_functor.leftJacobian(); // Left Jacobian of SO(3) Expmap
if (H1) { const Matrix3 E = Calculate_E(theta_tan); // Use helper matrix E(theta)
*H1 = H_comp_this; // Requires AdjointMap
} const Point3 r_final = Point3(Jl_theta * rho_tan + E * (t_tan_val * nu_tan));
if (H2) { const Velocity3 v_final = Jl_theta * nu_tan;
// Needs ExpmapDerivative and potentially AdjointMap if H_comp_gexp is not Identity const double t_final = t_tan_val;
*H2 = H_comp_gexp * H_exp_xi; // H_comp_gexp is Identity here
} if (Hxi) {
return result; *Hxi = ExpmapDerivative(xi); // Use numerical derivative for Jacobian
}
return Gal3(R, r_final, v_final, t_final);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Vector10 Gal3::localCoordinates(const Gal3& other, OptionalJacobian<10, 10> H1, OptionalJacobian<10, 10> H2) const { Vector10 Gal3::Logmap(const Gal3& g, OptionalJacobian<10, 10> Hg) {
Matrix10 H_between_this, H_between_other; const Vector3 theta_vec = Rot3::Logmap(g.R_);
Gal3 result_g = this->between(other, H_between_this, H_between_other); const gtsam::so3::DexpFunctor dexp_functor_log(theta_vec);
const Matrix3 Jl_theta_inv = dexp_functor_log.leftJacobianInverse(); // Inverse Left Jacobian
Matrix10 H_log_g = Matrix10::Zero(); // Placeholder const Matrix3 E = Calculate_E(theta_vec); // Use helper matrix E(theta)
if (H1 || H2) {
H_log_g = Gal3::LogmapDerivative(result_g); // Needs LogmapDerivative implemented
}
Vector10 result_xi = Gal3::Logmap(result_g);
// Jacobians require LogmapDerivative and AdjointMap (via between Jacobians) const Vector3 r_vec = Vector3(g.r_);
if (H1) { const Velocity3& v_vec = g.v_;
*H1 = H_log_g * H_between_this; const double& t_val = g.t_;
const Vector3 nu_tan = Jl_theta_inv * v_vec;
const Vector3 rho_tan = Jl_theta_inv * (r_vec - E * (t_val * nu_tan));
const double t_tan_val = t_val;
if (Hg) {
*Hg = LogmapDerivative(g); // Use numerical derivative for Jacobian
} }
if (H2) {
*H2 = H_log_g * H_between_other; Vector10 xi;
} rho(xi) = rho_tan;
return result_xi; nu(xi) = nu_tan;
theta(xi) = theta_vec;
t_tan(xi)(0) = t_tan_val;
return xi;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Lie Group Static Functions Matrix10 Gal3::AdjointMap() const {
//------------------------------------------------------------------------------ const Matrix3 Rmat = R_.matrix();
const Vector3 v_vec = v_;
const Vector3 r_minus_tv = Vector3(r_) - t_ * v_;
Matrix10 Ad = Matrix10::Zero();
// Row block 1: drho' output
Ad.block<3,3>(0,0) = Rmat;
Ad.block<3,3>(0,3) = -t_ * Rmat;
Ad.block<3,3>(0,6) = skewSymmetric(r_minus_tv) * Rmat; // [(r-t*v)]x * R
Ad.block<3,1>(0,9) = v_vec;
// Row block 2: dnu' output
Ad.block<3,3>(3,3) = Rmat;
Ad.block<3,3>(3,6) = skewSymmetric(v_vec) * Rmat; // [v]x * R
// Row block 3: dtheta' output
Ad.block<3,3>(6,6) = Rmat;
// Row block 4: dt' output
Ad(9,9) = 1.0;
return Ad;
}
//------------------------------------------------------------------------------
Vector10 Gal3::Adjoint(const Vector10& xi, OptionalJacobian<10, 10> H_g, OptionalJacobian<10, 10> H_xi) const {
Matrix10 Ad = AdjointMap(); // Ad = Ad(g)
Vector10 y = Ad * xi; // y = Ad_g(xi)
if (H_xi) {
*H_xi = Ad; // Jacobian w.r.t xi is Ad(g)
}
if (H_g) {
// Jacobian H_g = d(Ad_g(xi))/d(g) -> use numerical derivative
std::function<Vector10(const Gal3&, const Vector10&)> adjoint_action_wrt_g =
[&](const Gal3& g_in, const Vector10& xi_in) {
// Call Adjoint *without* asking for its Jacobians for numerical diff
return g_in.Adjoint(xi_in);
};
*H_g = numericalDerivative21(adjoint_action_wrt_g, *this, xi, 1e-7);
}
return y;
}
//------------------------------------------------------------------------------
Matrix10 Gal3::adjointMap(const Vector10& xi) { // Lowercase 'a' - ad(xi)
const Matrix3 Theta_hat = skewSymmetric(theta(xi)); // =[theta_xi]x
const Matrix3 Nu_hat = skewSymmetric(nu(xi)); // =[nu_xi]x
const Matrix3 Rho_hat = skewSymmetric(rho(xi)); // =[rho_xi]x
const double t_val = t_tan(xi)(0); // =t_xi
const Vector3 nu_vec = nu(xi);
Matrix10 ad = Matrix10::Zero();
// Structure based on Lie bracket [xi, y] = ad(xi)y
// Row block 1: maps input rho_y, nu_y, theta_y, t_y to output rho_z
ad.block<3,3>(0,0) = Theta_hat;
ad.block<3,3>(0,3) = -t_val * Matrix3::Identity();
ad.block<3,3>(0,6) = Rho_hat;
ad.block<3,1>(0,9) = nu_vec;
// Row block 2: maps input nu_y, theta_y to output nu_z
ad.block<3,3>(3,3) = Theta_hat;
ad.block<3,3>(3,6) = Nu_hat;
// Row block 3: maps input theta_y to output theta_z
ad.block<3,3>(6,6) = Theta_hat;
// Row block 4: maps input t_y to output t_z (which is 0)
return ad;
}
//------------------------------------------------------------------------------
Vector10 Gal3::adjoint(const Vector10& xi, const Vector10& y, OptionalJacobian<10, 10> Hxi, OptionalJacobian<10, 10> Hy) {
Matrix10 ad_xi = adjointMap(xi);
if (Hy) *Hy = ad_xi; // Jacobian w.r.t y is ad(xi)
if (Hxi) {
// Jacobian w.r.t xi is -ad(y)
*Hxi = -adjointMap(y);
}
return ad_xi * y;
}
//------------------------------------------------------------------------------
Matrix10 Gal3::ExpmapDerivative(const Vector10& xi) {
// Use numerical derivative for Expmap Jacobian
// Use locally defined constant
if (xi.norm() < kSmallAngleThreshold) return Matrix10::Identity();
std::function<Gal3(const Vector10&)> fn =
[](const Vector10& v) { return Gal3::Expmap(v); };
return numericalDerivative11<Gal3, Vector10>(fn, xi, 1e-5);
}
//------------------------------------------------------------------------------
Matrix10 Gal3::LogmapDerivative(const Gal3& g) {
// Use numerical derivative for Logmap Jacobian
Vector10 xi = Gal3::Logmap(g);
// Use locally defined constant
if (xi.norm() < kSmallAngleThreshold) return Matrix10::Identity();
std::function<Vector10(const Gal3&)> fn =
[](const Gal3& g_in) { return Gal3::Logmap(g_in); };
return numericalDerivative11<Vector10, Gal3>(fn, g, 1e-5);
}
//------------------------------------------------------------------------------
// Lie Algebra (Hat/Vee maps) (NavState order: 9)
//------------------------------------------------------------------------------
Matrix5 Gal3::Hat(const Vector10& xi) { Matrix5 Gal3::Hat(const Vector10& xi) {
const Vector3 rho_tan = rho(xi); const Vector3 rho_tan = rho(xi);
const Vector3 nu_tan = nu(xi); const Vector3 nu_tan = nu(xi);
@ -348,10 +482,8 @@ Matrix5 Gal3::Hat(const Vector10& xi) {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Vector10 Gal3::Vee(const Matrix5& X) { Vector10 Gal3::Vee(const Matrix5& X) {
// CORRECTED: Check X.row(3).head(N) for Lie algebra structure // Check Lie algebra structure: Row 4 all=0; Row 3 first 3=0, last = t.
// Lie algebra row 3 is [0 0 0 0 t], so first 3 elements should be 0. if (X.row(4).norm() > 1e-9 || X.row(3).head(3).norm() > 1e-9 || std::abs(X(3,3)) > 1e-9) {
// Lie algebra row 4 is [0 0 0 0 0], so all 5 elements should be 0.
if (X.row(4).norm() > 1e-9 || X.row(3).head(3).norm() > 1e-9) { // Row 4 all=0; Row 3 first 3=0
throw std::invalid_argument("Matrix is not in sgal(3)"); throw std::invalid_argument("Matrix is not in sgal(3)");
} }
@ -359,188 +491,155 @@ Vector10 Gal3::Vee(const Matrix5& X) {
rho(xi) = X.block<3, 1>(0, 4); // rho from column 4 rho(xi) = X.block<3, 1>(0, 4); // rho from column 4
nu(xi) = X.block<3, 1>(0, 3); // nu from column 3 nu(xi) = X.block<3, 1>(0, 3); // nu from column 3
const Matrix3& S = X.block<3, 3>(0, 0); const Matrix3& S = X.block<3, 3>(0, 0);
theta(xi) << S(2, 1), S(0, 2), S(1, 0); theta(xi) << S(2, 1), S(0, 2), S(1, 0); // Extract theta from skew-symmetric part
t_tan(xi)(0) = X(3, 4); // t from element (3, 4) t_tan(xi)(0) = X(3, 4); // t from element (3, 4)
return xi; return xi;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Gal3 Gal3::Expmap(const Vector10& xi, OptionalJacobian<10, 10> Hxi) { // ChartAtOrigin (NavState order: 10)
const Vector3 rho_tan = rho(xi);
const Vector3 nu_tan = nu(xi);
const Vector3 theta_tan = theta(xi);
const double t_tan_val = t_tan(xi)(0);
const Rot3 R = Rot3::Expmap(theta_tan);
const Matrix3 Jl_theta = SO3_LeftJacobian(theta_tan);
const Matrix3 E = Calculate_E(theta_tan);
const Point3 r_final = Point3(Jl_theta * rho_tan + E * (t_tan_val * nu_tan));
const Velocity3 v_final = Jl_theta * nu_tan;
const double t_final = t_tan_val;
if (Hxi) {
// Jacobian placeholder - requires derivation
*Hxi = ExpmapDerivative(xi);
}
return Gal3(R, r_final, v_final, t_final);
}
//------------------------------------------------------------------------------
Vector10 Gal3::Logmap(const Gal3& g, OptionalJacobian<10, 10> Hg) {
const Vector3 theta_vec = Rot3::Logmap(g.R_);
const Matrix3 Jl_theta_inv = SO3_LeftJacobianInverse(theta_vec);
const Matrix3 E = Calculate_E(theta_vec);
// CORRECTED: Cast r_ to Vector3 for subtraction
const Vector3 r_vec = Vector3(g.r_);
const Velocity3& v_vec = g.v_;
const double& t_val = g.t_;
const Vector3 nu_tan = Jl_theta_inv * v_vec;
const Vector3 rho_tan = Jl_theta_inv * (r_vec - E * (t_val * nu_tan));
const double t_tan_val = t_val;
// Jacobian placeholder - requires derivation
if (Hg) {
*Hg = LogmapDerivative(g);
}
Vector10 xi;
rho(xi) = rho_tan;
nu(xi) = nu_tan;
theta(xi) = theta_vec;
t_tan(xi)(0) = t_tan_val;
return xi;
}
//------------------------------------------------------------------------------
Matrix10 Gal3::AdjointMap() const {
const Matrix3 Rmat = R_.matrix();
const Matrix3 Vhat = skewSymmetric(v_);
// CORRECTED: Cast r_ to Vector3 for subtraction
const Vector3 r_minus_tv = Vector3(r_) - t_ * v_;
const Matrix3 S_r_minus_tv_hat = skewSymmetric(r_minus_tv);
Matrix10 Ad = Matrix10::Zero();
// Row block 1: drho' output (maps input drho, dnu, dtheta, dt)
Ad.block<3,3>(0,0) = Rmat;
Ad.block<3,3>(0,3) = t_ * Rmat;
Ad.block<3,3>(0,6) = -Rmat * S_r_minus_tv_hat;
Ad.block<3,1>(0,9) = Rmat * v_;
// Row block 2: dnu' output
Ad.block<3,3>(3,3) = Rmat;
Ad.block<3,3>(3,6) = -Rmat * Vhat;
// Row block 3: dtheta' output
Ad.block<3,3>(6,6) = Rmat;
// Row block 4: dt' output
Ad(9,9) = 1.0;
return Ad;
}
//------------------------------------------------------------------------------
Vector10 Gal3::Adjoint(const Vector10& xi, OptionalJacobian<10, 10> H_g, OptionalJacobian<10, 10> H_xi) const {
Matrix10 Ad = AdjointMap();
if (H_xi) {
*H_xi = Ad;
}
// Jacobian H_g requires adjointMap
if (H_g) {
*H_g = -adjointMap(Ad * xi);
}
return Ad * xi;
}
//------------------------------------------------------------------------------
Matrix10 Gal3::adjointMap(const Vector10& xi) {
// Based on Manif SGal3Tangent::adj() / Lie bracket [xi, y]
const Matrix3 Theta_hat = skewSymmetric(theta(xi));
const Matrix3 Nu_hat = skewSymmetric(nu(xi));
const Matrix3 Rho_hat = skewSymmetric(rho(xi));
const double t_val = t_tan(xi)(0);
Matrix10 ad = Matrix10::Zero();
// Row block 1: rho_out = Theta_hat*m + Rho_hat*eta + t_val*p
ad.block<3,3>(0,0) = Theta_hat; // d/dm
ad.block<3,3>(0,3) = t_val * Matrix3::Identity(); // d/dp
ad.block<3,3>(0,6) = Rho_hat; // d/deta
// Row block 2: nu_out = Theta_hat*p + Nu_hat*eta
ad.block<3,3>(3,3) = Theta_hat; // d/dp
ad.block<3,3>(3,6) = Nu_hat; // d/deta
// Row block 3: theta_out = Theta_hat*eta
ad.block<3,3>(6,6) = Theta_hat; // d/deta
// Row block 4: t_out = 0 (already zero)
return ad;
}
//------------------------------------------------------------------------------
Vector10 Gal3::adjoint(const Vector10& xi, const Vector10& y, OptionalJacobian<10, 10> Hxi, OptionalJacobian<10, 10> Hy) {
Matrix10 ad_xi = adjointMap(xi);
if (Hy) *Hy = ad_xi;
// Jacobian Hxi requires adjointMap
if (Hxi) {
*Hxi = -adjointMap(y);
}
return ad_xi * y;
}
//------------------------------------------------------------------------------
Matrix10 Gal3::ExpmapDerivative(const Vector10& xi) {
if (xi.norm() < kSmallAngleThreshold) return Matrix10::Identity();
// CORRECTED: Explicitly create std::function to resolve ambiguity
std::function<Gal3(const Vector10&)> fn =
[](const Vector10& v) { return Gal3::Expmap(v); };
// Note: Default delta for numericalDerivative might be too large or small
return numericalDerivative11<Gal3, Vector10>(fn, xi, 1e-5); // Pass function object
}
//------------------------------------------------------------------------------
Matrix10 Gal3::LogmapDerivative(const Gal3& g) {
Vector10 xi = Gal3::Logmap(g);
if (xi.norm() < kSmallAngleThreshold) return Matrix10::Identity();
// CORRECTED: Explicitly create std::function to resolve ambiguity
std::function<Vector10(const Gal3&)> fn =
[](const Gal3& g_in) { return Gal3::Logmap(g_in); };
// Note: Default delta for numericalDerivative might be too large or small
return numericalDerivative11<Vector10, Gal3>(fn, g, 1e-5); // Pass function object
}
//------------------------------------------------------------------------------
// ChartAtOrigin
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Gal3 Gal3::ChartAtOrigin::Retract(const Vector10& xi, ChartJacobian Hxi) { Gal3 Gal3::ChartAtOrigin::Retract(const Vector10& xi, ChartJacobian Hxi) {
// Retraction at origin is Expmap
return Gal3::Expmap(xi, Hxi); return Gal3::Expmap(xi, Hxi);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
Vector10 Gal3::ChartAtOrigin::Local(const Gal3& g, ChartJacobian Hg) { Vector10 Gal3::ChartAtOrigin::Local(const Gal3& g, ChartJacobian Hg) {
// Local coordinates at origin is Logmap
return Gal3::Logmap(g, Hg); return Gal3::Logmap(g, Hg);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Stream operator // Manifold Operations (Instance) (NavState order: 11 - retract/local/interpolate)
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
std::ostream& operator<<(std::ostream& os, const Gal3& state) { Gal3 Gal3::retract(const Vector10& xi, OptionalJacobian<10, 10> H1, OptionalJacobian<10, 10> H2) const {
os << "R: " << state.R_ << "\n"; // Default retract is Expmap applied via composition: this->compose(Expmap(xi))
os << "r: " << state.r_.transpose() << "\n"; Matrix10 H_exp_xi;
os << "v: " << state.v_.transpose() << "\n"; Gal3 g_exp = Gal3::Expmap(xi, H2 ? &H_exp_xi : nullptr); // Expmap(xi) and its Jacobian H2'
os << "t: " << state.t_;
return os; Matrix10 H_comp_this, H_comp_gexp;
Gal3 result = this->compose(g_exp, H1 ? &H_comp_this : nullptr, H2 ? &H_comp_gexp : nullptr);
if (H1) {
// H1 = d(compose)/d(this) = H_comp_this = Ad(g_exp.inverse())
*H1 = H_comp_this;
}
if (H2) {
// H2 = d(compose)/d(g_exp) * d(g_exp)/d(xi) = H_comp_gexp * H_exp_xi
// H_comp_gexp is Identity, so H2 = H_exp_xi
*H2 = H_comp_gexp * H_exp_xi;
}
return result;
} }
//------------------------------------------------------------------------------
Vector10 Gal3::localCoordinates(const Gal3& other, OptionalJacobian<10, 10> H1, OptionalJacobian<10, 10> H2) const {
// Default localCoords is Logmap of the relative transformation: Logmap(this->between(other))
Matrix10 H_between_this, H_between_other;
Gal3 result_g = this->between(other, H1 ? &H_between_this : nullptr, H2 ? &H_between_other : nullptr); // g = between(this, other) and its Jacobians
Matrix10 H_log_g;
Vector10 result_xi = Gal3::Logmap(result_g, (H1 || H2) ? &H_log_g : nullptr); // xi = Logmap(g) and its Jacobian
if (H1) {
// H1 = d(Logmap)/d(g) * d(g)/d(this) = H_log_g * H_between_this
*H1 = H_log_g * H_between_this;
}
if (H2) {
// H2 = d(Logmap)/d(g) * d(g)/d(other) = H_log_g * H_between_other
*H2 = H_log_g * H_between_other;
}
return result_xi;
}
//------------------------------------------------------------------------------
Gal3 Gal3::interpolate(const Gal3& other, double alpha,
OptionalJacobian<10, 10> H1,
OptionalJacobian<10, 10> H2,
OptionalJacobian<10, 1> Ha) const {
// Formula: g_interp = g1 * Exp(alpha * Log(g1.inverse * g2))
// g_interp = g1 * Exp(alpha * Log(g1.between(g2)))
// g_interp = g1.compose(Gal3::Expmap(alpha * Gal3::Logmap(g1.between(g2))))
// Step 1: Calculate between and Logmap
Matrix10 H_between_g1, H_between_g2; // Jacobians of between
Gal3 g_between = this->between(other,
(H1 || H2 || Ha) ? &H_between_g1 : nullptr,
(H1 || H2 || Ha) ? &H_between_g2 : nullptr);
Matrix10 H_log_between; // Jacobian of Logmap wrt g_between
Vector10 xi_between = Gal3::Logmap(g_between,
(H1 || H2 || Ha) ? &H_log_between : nullptr);
// Step 2: Scale tangent vector
Vector10 xi_scaled = alpha * xi_between;
// Step 3: Calculate Expmap
Matrix10 H_exp_scaled; // Jacobian of Expmap wrt xi_scaled
Gal3 g_exp_scaled = Gal3::Expmap(xi_scaled,
(H1 || H2 || Ha) ? &H_exp_scaled : nullptr);
// Step 4: Compose with g1 (this)
Matrix10 H_comp_g1, H_comp_gexp; // Jacobians of compose
Gal3 g_interp = this->compose(g_exp_scaled,
(H1 || Ha) ? &H_comp_g1 : nullptr,
(H1 || H2 || Ha) ? &H_comp_gexp : nullptr);
// --- Calculate Jacobians using Chain Rule ---
if (H1 || H2 || Ha) {
const Matrix10 d_xi_sc_d_xi_btw = alpha * Matrix10::Identity();
const Matrix10 chain_gexp_gbtw = H_exp_scaled * d_xi_sc_d_xi_btw * H_log_between;
const Matrix10 chain_gexp_g1 = chain_gexp_gbtw * H_between_g1;
const Matrix10 chain_gexp_g2 = chain_gexp_gbtw * H_between_g2;
const Vector10 d_xi_sc_d_alpha = xi_between;
const Vector10 chain_gexp_alpha = H_exp_scaled * d_xi_sc_d_alpha;
if (H1) {
// d(g_interp)/d(g1) = d(f5)/d(g1) + d(f5)/d(g_exp) * d(g_exp)/d(g1)
*H1 = H_comp_g1 + H_comp_gexp * chain_gexp_g1;
}
if (H2) {
// d(g_interp)/d(g2) = d(f5)/d(g_exp) * d(g_exp)/d(g2)
*H2 = H_comp_gexp * chain_gexp_g2;
}
if (Ha) {
// d(g_interp)/d(alpha) = d(f5)/d(g_exp) * d(g_exp)/d(alpha)
*Ha = H_comp_gexp * chain_gexp_alpha;
}
}
return g_interp;
}
//------------------------------------------------------------------------------
// Group Action (NavState order: After Manifold Instance methods, before Dynamics)
//------------------------------------------------------------------------------
Point3 Gal3::act(const Point3& p, OptionalJacobian<3, 10> Hself, OptionalJacobian<3, 3> Hp) const {
// Implementation assumes instantaneous action: p_out = R_.rotate(p) + r_
Point3 r_out = R_.rotate(p) + r_;
if (Hp) {
*Hp = R_.matrix(); // Jacobian d(R*p+r)/dp = R
}
if (Hself) {
// Jacobian d(act(g,p))/d(xi) where g = this, xi is tangent at identity
// Corresponds to columns for [rho, nu, theta, t_tan]
*Hself = Matrix::Zero(3, 10);
const Matrix3 Rmat = R_.matrix();
// Derivative w.r.t. rho (cols 0-2) -> d(r)/drho = R
Hself->block<3,3>(0, 0) = Rmat;
// Derivative w.r.t. nu (cols 3-5) -> d(R*p+r)/dnu = 0
// Block remains zero.
// Derivative w.r.t. theta (cols 6-8) -> d(R*p)/dtheta = -R*skew(p)
Hself->block<3,3>(0, 6) = -Rmat * skewSymmetric(p);
// Derivative w.r.t. t_tan (col 9) -> Based on retract analysis, should be v_
Hself->block<3,1>(0, 9) = v_;
}
return r_out;
}
//------------------------------------------------------------------------------
// Dynamics (NavState order: 12) - Gal3 currently has no dynamics methods.
//------------------------------------------------------------------------------
} // namespace gtsam } // namespace gtsam

View File

@ -9,21 +9,19 @@
/** /**
* @file Gal3.h * @file Gal3.h
* @brief 3D Galilean Group SGal(3) state (attitude, position, velocity, time) * @brief 3D Galilean Group SGal(3) state (attitude, position, velocity, time)
* Based on manif convention: [R, r, v, t], tangent [rho, nu, theta, t] * Based on manif convention: [R, r, v, t], tangent [rho, nu, theta, t_tan]
* @author Based on Python implementation by User * @author Based on Python implementation by User
* @date April 29, 2025 * @date April 30, 2025 // Updated Date
*/ */
#pragma once #pragma once
#include <gtsam/geometry/Pose3.h> // Includes Rot3, Point3 #include <gtsam/geometry/Pose3.h> // Includes Rot3, Point3
#include <gtsam/base/Vector.h> #include <gtsam/base/Lie.h> // For LieGroup base class and traits
#include <gtsam/base/Manifold.h> #include <gtsam/base/Manifold.h> // For Manifold traits
#include <gtsam/base/Lie.h> // For LieGroup base class
#include <cmath> // For std::sqrt, std::cos, std::sin, std::tan #include <cmath> // For std::sqrt, std::cos, std::sin
#include <limits> // For std::numeric_limits #include <functional> // For std::function used in numerical derivatives
#include <functional> // For std::function
namespace gtsam { namespace gtsam {
@ -34,9 +32,9 @@ class Gal3;
using Velocity3 = Vector3; using Velocity3 = Vector3;
// Define Vector10 for tangent space // Define Vector10 for tangent space
using Vector10 = Eigen::Matrix<double, 10, 1>; using Vector10 = Eigen::Matrix<double, 10, 1>;
// Define Matrix5 for Lie Algebra matrix representation // Define Matrix5 for Lie Algebra matrix representation (Hat map output)
using Matrix5 = Eigen::Matrix<double, 5, 5>; using Matrix5 = Eigen::Matrix<double, 5, 5>;
// Define Matrix10 for Jacobians // Define Matrix10 for Jacobians (AdjointMap, Expmap/Logmap derivatives)
using Matrix10 = Eigen::Matrix<double, 10, 10>; using Matrix10 = Eigen::Matrix<double, 10, 10>;
@ -72,12 +70,10 @@ class GTSAM_EXPORT Gal3 : public LieGroup<Gal3, 10> {
Velocity3 v_; ///< Velocity in world frame, n_v_b Velocity3 v_; ///< Velocity in world frame, n_v_b
double t_; ///< Time component double t_; ///< Time component
// Small number epsilon for checking small angles, etc.
static constexpr double kSmallAngleThreshold = 1e-10;
public: public:
inline constexpr static size_t dimension = 10; /// The dimension of the tangent space
inline static constexpr size_t dimension = 10;
/// @name Constructors /// @name Constructors
/// @{ /// @{
@ -92,28 +88,49 @@ class GTSAM_EXPORT Gal3 : public LieGroup<Gal3, 10> {
/// Construct from a 5x5 matrix representation (manif convention) /// Construct from a 5x5 matrix representation (manif convention)
explicit Gal3(const Matrix5& M); explicit Gal3(const Matrix5& M);
/// Named constructor from components with derivatives
static Gal3 Create(const Rot3& R, const Point3& r, const Velocity3& v, double t,
OptionalJacobian<10, 3> H1 = {},
OptionalJacobian<10, 3> H2 = {},
OptionalJacobian<10, 3> H3 = {},
OptionalJacobian<10, 1> H4 = {});
/// Named constructor from Pose3, velocity, and time with derivatives
static Gal3 FromPoseVelocityTime(const Pose3& pose, const Velocity3& v, double t,
OptionalJacobian<10, 6> H1 = {},
OptionalJacobian<10, 3> H2 = {},
OptionalJacobian<10, 1> H3 = {});
/// @} /// @}
/// @name Component Access /// @name Component Access
/// @{ /// @{
/// Access rotation component (Rot3)
const Rot3& rotation(OptionalJacobian<3, 10> H = {}) const; const Rot3& rotation(OptionalJacobian<3, 10> H = {}) const;
/// Access translation component (Point3)
const Point3& translation(OptionalJacobian<3, 10> H = {}) const; const Point3& translation(OptionalJacobian<3, 10> H = {}) const;
/// Access velocity component (Vector3)
const Velocity3& velocity(OptionalJacobian<3, 10> H = {}) const; const Velocity3& velocity(OptionalJacobian<3, 10> H = {}) const;
/// Access time component (double)
const double& time(OptionalJacobian<1, 10> H = {}) const; const double& time(OptionalJacobian<1, 10> H = {}) const;
// Convenience accessors matching NavState where names overlap // Convenience accessors matching NavState names where they overlap
const Rot3& attitude(OptionalJacobian<3, 10> H = {}) const { return rotation(H); } const Rot3& attitude(OptionalJacobian<3, 10> H = {}) const { return rotation(H); }
const Point3& position(OptionalJacobian<3, 10> H = {}) const { return translation(H); } const Point3& position(OptionalJacobian<3, 10> H = {}) const { return translation(H); }
/// @} /// @}
/// @name Derived quantities /// @name Derived quantities
/// @{ /// @{
/// Return rotation matrix. /// Return rotation matrix (Matrix3).
Matrix3 R() const { return R_.matrix(); } Matrix3 R() const { return R_.matrix(); }
/// Return position as Vector3 /// Return position as Vector3.
Vector3 r() const { return Vector3(r_); } Vector3 r() const { return Vector3(r_); } // Conversion from Point3
/// Return velocity as Vector3. Computation-free. /// Return velocity as Vector3. Computation-free.
const Vector3& v() const { return v_; } const Vector3& v() const { return v_; }
@ -132,137 +149,158 @@ class GTSAM_EXPORT Gal3 : public LieGroup<Gal3, 10> {
GTSAM_EXPORT GTSAM_EXPORT
friend std::ostream &operator<<(std::ostream &os, const Gal3& state); friend std::ostream &operator<<(std::ostream &os, const Gal3& state);
/// print /// Print with optional string prefix
void print(const std::string& s = "") const; void print(const std::string& s = "") const;
/// equals /// Check equality within tolerance
bool equals(const Gal3& other, double tol = 1e-9) const; bool equals(const Gal3& other, double tol = 1e-9) const;
/// @} /// @}
/// @name Group /// @name Group
/// @{ /// @{
/// identity for group operation /// Return the identity element
static Gal3 Identity() { return Gal3(); } static Gal3 Identity() { return Gal3(); }
/// inverse transformation (Manif formula from SGal3_base.h) /// Return the inverse of this element
Gal3 inverse() const; Gal3 inverse() const;
// Bring LieGroup::inverse() into scope (version with derivative) // Bring LieGroup::inverse() into scope (version with derivative)
using LieGroup<Gal3, 10>::inverse; using LieGroup<Gal3, 10>::inverse;
/// compose (Manif formula from SGal3_base.h) /// Compose with another element: `this * other`
/// Returns this * other
Gal3 compose(const Gal3& other, OptionalJacobian<10, 10> H1 = {}, Gal3 compose(const Gal3& other, OptionalJacobian<10, 10> H1 = {},
OptionalJacobian<10, 10> H2 = {}) const; OptionalJacobian<10, 10> H2 = {}) const;
/// compose syntactic sugar /// Group composition operator: `this * other`
Gal3 operator*(const Gal3& other) const { Gal3 operator*(const Gal3& other) const { return compose(other); }
return compose(other);
}
/// Calculate the relative transformation: this->inverse() * other /// Calculate the relative transformation: `this->inverse() * other`
Gal3 between(const Gal3& other, OptionalJacobian<10, 10> H1 = {}, Gal3 between(const Gal3& other, OptionalJacobian<10, 10> H1 = {},
OptionalJacobian<10, 10> H2 = {}) const; OptionalJacobian<10, 10> H2 = {}) const;
/// @} /// @}
/// @name Manifold Operations (Retract/Local) /// @name Manifold Operations
/// @{ /// @{
/// Default retract uses Expmap /// Retract from tangent space xi to manifold element (this + xi)
Gal3 retract(const Vector10& xi, OptionalJacobian<10, 10> H1 = {}, Gal3 retract(const Vector10& xi, OptionalJacobian<10, 10> H1 = {},
OptionalJacobian<10, 10> H2 = {}) const; OptionalJacobian<10, 10> H2 = {}) const;
/// Default localCoordinates uses Logmap /// Compute local coordinates (tangent vector) from this to other (logmap(this.inverse * other))
Vector10 localCoordinates(const Gal3& other, OptionalJacobian<10, 10> H1 = {}, Vector10 localCoordinates(const Gal3& other, OptionalJacobian<10, 10> H1 = {},
OptionalJacobian<10, 10> H2 = {}) const; OptionalJacobian<10, 10> H2 = {}) const;
/**
* Interpolate between two Gal3 elements.
* @param other The other Gal3 element.
* @param alpha Interpolation fraction (0=this, 1=other).
* @return Interpolated Gal3 element: this->compose(Expmap(alpha * Logmap(this->between(other))))
*/
Gal3 interpolate(const Gal3& other, double alpha,
OptionalJacobian<10, 10> H1 = {},
OptionalJacobian<10, 10> H2 = {},
OptionalJacobian<10, 1> Ha = {}) const; // Added Jacobian args to match NavState/typical patterns
/// @} /// @}
/// @name Lie Group /// @name Group Action
using LieAlgebra = Matrix5;
/// @{ /// @{
// Helper functions for accessing tangent vector components /**
// Input xi is [rho(3), nu(3), theta(3), t_tan(1)] * Action of the group element on a 3D point.
static Eigen::Block<Vector10, 3, 1> rho(Vector10& v) { return v.segment<3>(0); } * Following GTSAM convention for Pose3::transformFrom, this applies
static Eigen::Block<Vector10, 3, 1> nu(Vector10& v) { return v.segment<3>(3); } * the instantaneous rotation and translation: p' = R*p + r.
static Eigen::Block<Vector10, 3, 1> theta(Vector10& v) { return v.segment<3>(6); } * If time evolution is needed, use compose or specific dynamics model.
static Eigen::Block<Vector10, 1, 1> t_tan(Vector10& v) { return v.segment<1>(9); } */
// Const versions too Point3 act(const Point3& p,
static const Eigen::Block<const Vector10, 3, 1> rho(const Vector10& v) { return v.segment<3>(0); } OptionalJacobian<3, 10> Hself = {},
static const Eigen::Block<const Vector10, 3, 1> nu(const Vector10& v) { return v.segment<3>(3); } OptionalJacobian<3, 3> Hp = {}) const;
static const Eigen::Block<const Vector10, 3, 1> theta(const Vector10& v) { return v.segment<3>(6); }
static const Eigen::Block<const Vector10, 1, 1> t_tan(const Vector10& v) { return v.segment<1>(9); }
/// Exponential map at identity - create a Gal3 element from Lie algebra tangent vector xi. // Alias for consistency if used elsewhere
Point3 transformFrom(const Point3& p,
OptionalJacobian<3, 10> Hself = {},
OptionalJacobian<3, 3> Hp = {}) const {
return act(p, Hself, Hp);
}
/// @}
/// @name Lie Group Static Functions
/// @{
/// The type of the Lie algebra (matrix representation)
using LieAlgebra = Matrix5;
// Helper functions for accessing tangent vector components by name
// Input xi is [rho(3), nu(3), theta(3), t_tan(1)]
// *** CORRECTED to use .block<rows, cols>(startRow, startCol) syntax ***
static Eigen::Block<Vector10, 3, 1> rho(Vector10& v) { return v.block<3, 1>(0, 0); }
static Eigen::Block<Vector10, 3, 1> nu(Vector10& v) { return v.block<3, 1>(3, 0); }
static Eigen::Block<Vector10, 3, 1> theta(Vector10& v) { return v.block<3, 1>(6, 0); }
static Eigen::Block<Vector10, 1, 1> t_tan(Vector10& v) { return v.block<1, 1>(9, 0); }
// Const versions
static Eigen::Block<const Vector10, 3, 1> rho(const Vector10& v) { return v.block<3, 1>(0, 0); }
static Eigen::Block<const Vector10, 3, 1> nu(const Vector10& v) { return v.block<3, 1>(3, 0); }
static Eigen::Block<const Vector10, 3, 1> theta(const Vector10& v) { return v.block<3, 1>(6, 0); }
static Eigen::Block<const Vector10, 1, 1> t_tan(const Vector10& v) { return v.block<1, 1>(9, 0); }
/// Exponential map at identity: tangent vector xi -> manifold element g
/// xi = [rho, nu, theta, t_tan] (10x1) /// xi = [rho, nu, theta, t_tan] (10x1)
static Gal3 Expmap(const Vector10& xi, OptionalJacobian<10, 10> Hxi = {}); static Gal3 Expmap(const Vector10& xi, OptionalJacobian<10, 10> Hxi = {});
// REMOVED using LieGroup<Gal3, 10>::Expmap;
/// Logarithmic map at identity - return the Lie algebra tangent vector xi for this element. /// Logarithmic map at identity: manifold element g -> tangent vector xi
/// Returns xi = [rho, nu, theta, t_tan] (10x1) /// Returns xi = [rho, nu, theta, t_tan] (10x1)
static Vector10 Logmap(const Gal3& g, OptionalJacobian<10, 10> Hg = {}); static Vector10 Logmap(const Gal3& g, OptionalJacobian<10, 10> Hg = {});
// REMOVED using LieGroup<Gal3, 10>::Logmap;
/// Calculate Adjoint map, transforming a twist in the identity frame to the frame of this element. /// Calculate Adjoint map Ad_g, transforming a tangent vector in the identity frame
/// to the frame of this element g. Ad_g = [Ad_R, 0; Ad_r*Ad_R, Ad_R] structure adapted for Gal3.
Matrix10 AdjointMap() const; Matrix10 AdjointMap() const;
/// Apply this element's AdjointMap Ad_g to a tangent vector xi, H_g = d(Ad_g * xi)/dg, H_xi = d(Ad_g * xi)/dxi = Ad_g /// Apply this element's AdjointMap Ad_g to a tangent vector xi_base at identity.
Vector10 Adjoint(const Vector10& xi, OptionalJacobian<10, 10> H_g = {}, OptionalJacobian<10, 10> H_xi = {}) const; /// Result is xi_this = Ad_g * xi_base.
/// H_g = d(Ad_g * xi_base)/dg, H_xi = d(Ad_g * xi_base)/dxi_base = Ad_g
Vector10 Adjoint(const Vector10& xi_base, OptionalJacobian<10, 10> H_g = {}, OptionalJacobian<10, 10> H_xi = {}) const;
/// The adjoint action of xi on y `ad(xi, y)` /// The adjoint action `ad(xi, y)` = `adjointMap(xi) * y`.
/// This is the Lie bracket [xi, y] = ad_xi(y).
static Vector10 adjoint(const Vector10& xi, const Vector10& y, OptionalJacobian<10, 10> Hxi = {}, OptionalJacobian<10, 10> Hy = {}); static Vector10 adjoint(const Vector10& xi, const Vector10& y, OptionalJacobian<10, 10> Hxi = {}, OptionalJacobian<10, 10> Hy = {});
/// Compute the 10x10 adjoint map `ad(xi)` that acts on a tangent vector y `ad(xi, y) = ad(xi) * y` /// Compute the 10x10 adjoint map `ad(xi)` associated with tangent vector xi.
/// Acts on a tangent vector y: `ad(xi, y) = ad(xi) * y`.
static Matrix10 adjointMap(const Vector10& xi); static Matrix10 adjointMap(const Vector10& xi);
/// Derivative of Expmap(xi) w.r.t. xi /// Derivative of Expmap(xi) w.r.t. xi evaluated at xi.
/// Currently implemented numerically.
static Matrix10 ExpmapDerivative(const Vector10& xi); static Matrix10 ExpmapDerivative(const Vector10& xi);
/// Derivative of Logmap(g) w.r.t. g (represented as tangent vector at identity) /// Derivative of Logmap(g) w.r.t. g (represented as tangent vector at identity).
/// Currently implemented numerically.
static Matrix10 LogmapDerivative(const Gal3& g); static Matrix10 LogmapDerivative(const Gal3& g);
/// Chart at origin, uses Expmap/Logmap for Retract/Local
// Chart at origin, depends on compile-time flag GTSAM_USE_MANIFOLD_MARKERS
struct ChartAtOrigin { struct ChartAtOrigin {
static Gal3 Retract(const Vector10& xi, ChartJacobian Hxi = {}); static Gal3 Retract(const Vector10& xi, ChartJacobian Hxi = {});
static Vector10 Local(const Gal3& g, ChartJacobian Hg = {}); static Vector10 Local(const Gal3& g, ChartJacobian Hg = {});
}; };
/// Hat operator: maps tangent vector xi to Lie algebra matrix (Manif convention). /// Hat operator: maps tangent vector xi to Lie algebra matrix (Manif convention).
/// xi = [rho, nu, theta, t] -> [ skew(theta) nu rho ; 0 0 t ; 0 0 0 ] /// xi = [rho, nu, theta, t_tan] -> [ skew(theta) nu rho ; 0 0 t_tan ; 0 0 0 ]
static Matrix5 Hat(const Vector10& xi); static Matrix5 Hat(const Vector10& xi);
/// Vee operator: maps Lie algebra matrix to tangent vector xi (Manif convention). /// Vee operator: maps Lie algebra matrix to tangent vector xi (Manif convention).
/// [ S N R ; 0 0 T ; 0 0 0 ] -> [ R ; N ; vee(S) ; T ] = [rho; nu; theta; t] /// [ S N R ; 0 0 T ; 0 0 0 ] -> [ R ; N ; vee(S) ; T ] = [rho; nu; theta; t_tan]
static Vector10 Vee(const Matrix5& X); static Vector10 Vee(const Matrix5& X);
/// @}
/// @name Helper Functions (moved inside class for context, could be private or in detail namespace)
/// @{
/// Calculate the SO(3) Left Jacobian J_l(theta)
static Matrix3 SO3_LeftJacobian(const Vector3& theta);
/// Calculate the inverse SO(3) Left Jacobian J_l(theta)^-1
static Matrix3 SO3_LeftJacobianInverse(const Vector3& theta);
/// Calculate the 3x3 E matrix used in SGal3 Expmap/Logmap
static Matrix3 Calculate_E(const Vector3& theta);
/// @} /// @}
private:
private: /// @name Serialization
/// @{ /// @{
/// serialization
#if GTSAM_ENABLE_BOOST_SERIALIZATION #if GTSAM_ENABLE_BOOST_SERIALIZATION
friend class boost::serialization::access; friend class boost::serialization::access;
template <class ARCHIVE> template <class ARCHIVE>
void serialize(ARCHIVE& ar, const unsigned int /*version*/) { void serialize(ARCHIVE& ar, const unsigned int /*version*/) {
// Serialize member variables
ar& BOOST_SERIALIZATION_NVP(R_); ar& BOOST_SERIALIZATION_NVP(R_);
ar& BOOST_SERIALIZATION_NVP(r_); ar& BOOST_SERIALIZATION_NVP(r_);
ar& BOOST_SERIALIZATION_NVP(v_); ar& BOOST_SERIALIZATION_NVP(v_);
@ -270,13 +308,14 @@ private:
} }
#endif #endif
/// @} /// @}
}; // class Gal3 }; // class Gal3
// // Specialize Gal3 traits /// Traits specialization for Gal3
template <> template <>
struct traits<Gal3> : public internal::MatrixLieGroup<Gal3> {}; struct traits<Gal3> : public internal::LieGroup<Gal3> {};
template <> template <>
struct traits<const Gal3> : public internal::MatrixLieGroup<Gal3> {}; struct traits<const Gal3> : public internal::LieGroup<Gal3> {};
} // namespace gtsam } // namespace gtsam

View File

@ -36,6 +36,150 @@ GTSAM_CONCEPT_LIE_INST(gtsam::Gal3)
// Instantiate Testable concept for Gal3 to allow assert_equal(Gal3, Gal3) // Instantiate Testable concept for Gal3 to allow assert_equal(Gal3, Gal3)
// Instantiate Lie group concepts if you plan to use CHECK_LIE_GROUP_DERIVATIVES etc. // Instantiate Lie group concepts if you plan to use CHECK_LIE_GROUP_DERIVATIVES etc.
// GTSAM_CONCEPT_LIE_INST(Gal3) // GTSAM_CONCEPT_LIE_INST(Gal3)
const Rot3 kTestRot = Rot3::RzRyRx(0.1, -0.2, 0.3);
const Point3 kTestPos(1.0, -2.0, 3.0);
const Velocity3 kTestVel(0.5, 0.6, -0.7);
const double kTestTime = 1.5;
const Pose3 kTestPose(kTestRot, kTestPos);
const Gal3 kTestGal3(kTestRot, kTestPos, kTestVel, kTestTime);
/* ************************************************************************* */
// Test GTSAM Concept Compliance
/* ************************************************************************* */
TEST(Gal3, Concept) {
GTSAM_CONCEPT_ASSERT(IsGroup<Gal3>);
GTSAM_CONCEPT_ASSERT(IsManifold<Gal3>);
// Since its LieAlgebra is Matrix5, it also technically fits IsMatrixLieGroup,
// but IsLieGroup is sufficient here.
GTSAM_CONCEPT_ASSERT(IsLieGroup<Gal3>);
}
/* ************************************************************************* */
// Use GTSAM's Lie Group Test Macros
/* ************************************************************************* */
// Define some test instances if needed, or reuse existing ones like kTestGal3
const Gal3 kTestGal3_Lie1(Rot3::RzRyRx(0.1, -0.2, 0.3), Point3(1.0, -2.0, 3.0), Velocity3(0.5, 0.6, -0.7), 1.5);
const Gal3 kTestGal3_Lie2(Rot3::RzRyRx(-0.2, 0.3, 0.1), Point3(-2.0, 3.0, 1.0), Velocity3(0.6, -0.7, 0.5), 2.0);
const Gal3 kIdentity_Lie = Gal3::Identity();
// Check derivatives of compose, inverse, between, Expmap, Logmap
// compose, inverse, between are currently calculated analytically.
// @TODO: Expmap/Logmap/retract/localCoordinates currently rely on
// numerical derivative, so check is circular
TEST(Gal3, LieGroupDerivatives) {
CHECK_LIE_GROUP_DERIVATIVES(kIdentity_Lie, kIdentity_Lie);
CHECK_LIE_GROUP_DERIVATIVES(kIdentity_Lie, kTestGal3_Lie1);
CHECK_LIE_GROUP_DERIVATIVES(kTestGal3_Lie1, kIdentity_Lie);
CHECK_LIE_GROUP_DERIVATIVES(kTestGal3_Lie1, kTestGal3_Lie2);
}
// Check derivatives of retract and localCoordinates
TEST(Gal3, ChartDerivatives) {
CHECK_CHART_DERIVATIVES(kIdentity_Lie, kIdentity_Lie);
CHECK_CHART_DERIVATIVES(kIdentity_Lie, kTestGal3_Lie1);
CHECK_CHART_DERIVATIVES(kTestGal3_Lie1, kIdentity_Lie);
CHECK_CHART_DERIVATIVES(kTestGal3_Lie1, kTestGal3_Lie2);
}
/* ************************************************************************* */
// Test 1: Static Constructors (`Create`, `FromPoseVelocityTime`) Value Tests
/* ************************************************************************* */
TEST(Gal3, StaticConstructorsValue) {
// --- Test Gal3::Create ---
Gal3 g1 = Gal3::Create(kTestRot, kTestPos, kTestVel, kTestTime);
// Verify components match inputs
EXPECT(assert_equal(kTestRot, g1.rotation(), kTol));
EXPECT(assert_equal(kTestPos, g1.translation(), kTol));
EXPECT(assert_equal(kTestVel, g1.velocity(), kTol));
EXPECT_DOUBLES_EQUAL(kTestTime, g1.time(), kTol);
// Verify whole object matches reference (which was constructed the same way)
EXPECT(assert_equal(kTestGal3, g1, kTol));
// --- Test Gal3::FromPoseVelocityTime ---
Gal3 g2 = Gal3::FromPoseVelocityTime(kTestPose, kTestVel, kTestTime);
// Verify components match inputs
EXPECT(assert_equal(kTestPose.rotation(), g2.rotation(), kTol));
EXPECT(assert_equal(kTestPose.translation(), g2.translation(), kTol));
EXPECT(assert_equal(kTestVel, g2.velocity(), kTol));
EXPECT_DOUBLES_EQUAL(kTestTime, g2.time(), kTol);
// Verify whole object matches reference
EXPECT(assert_equal(kTestGal3, g2, kTol));
}
/* ************************************************************************* */
// Test 2: Component Accessor Tests
/* ************************************************************************* */
TEST(Gal3, ComponentAccessorsValue) {
// Use the pre-defined kTestGal3 constructed with known values
// Test rotation() / attitude()
EXPECT(assert_equal(kTestRot, kTestGal3.rotation(), kTol));
EXPECT(assert_equal(kTestRot, kTestGal3.attitude(), kTol)); // Alias
// Test translation() / position()
EXPECT(assert_equal(kTestPos, kTestGal3.translation(), kTol));
EXPECT(assert_equal(kTestPos, kTestGal3.position(), kTol)); // Alias
// Test velocity()
EXPECT(assert_equal(kTestVel, kTestGal3.velocity(), kTol));
// Test time()
EXPECT_DOUBLES_EQUAL(kTestTime, kTestGal3.time(), kTol);
// Test derived accessors
EXPECT(assert_equal(kTestRot.matrix(), kTestGal3.R(), kTol));
// Corrected line: Removed .vector()
EXPECT(assert_equal(kTestPos, kTestGal3.r(), kTol));
EXPECT(assert_equal(kTestVel, kTestGal3.v(), kTol));
EXPECT_DOUBLES_EQUAL(kTestTime, kTestGal3.t(), kTol);
}
/* ************************************************************************* */
// Test 3: `Gal3(const Matrix5& M)` Constructor Test
/* ************************************************************************* */
TEST(Gal3, MatrixConstructorValue) {
// Use the pre-defined kTestGal3 constructed with known values
Matrix5 M_known = kTestGal3.matrix(); // Assumes .matrix() is tested/correct
// Construct from the known valid matrix
Gal3 g_from_matrix(M_known);
// Verify the constructed object is equal to the original
EXPECT(assert_equal(kTestGal3, g_from_matrix, kTol));
// Optional: Test invalid matrix construction (if constructor has checks)
Matrix5 M_invalid = M_known;
M_invalid(4, 0) = 1e-5; // Violate the zero structure in the last row
// Assuming your Gal3(Matrix5) constructor throws std::invalid_argument
// or similar for invalid structure. Adjust exception type if needed.
try {
Gal3 g_invalid(M_invalid);
FAIL("Constructor should have thrown for invalid matrix structure.");
} catch (const std::invalid_argument& e) {
// Expected exception, test passes.
// Optionally check e.what() for specific message if desired.
} catch (...) {
FAIL("Constructor threw unexpected exception type for invalid matrix.");
}
// Test another invalid case
M_invalid = M_known;
M_invalid(3, 3) = 0.9; // Violate M(3,3) == 1
try {
Gal3 g_invalid(M_invalid);
FAIL("Constructor should have thrown for invalid matrix structure (M(3,3) != 1).");
} catch (const std::invalid_argument& e) {
// Expected exception, test passes.
} catch (...) {
FAIL("Constructor threw unexpected exception type for invalid matrix.");
}
}
// ======================================================================== // ========================================================================
// Test Cases Section (mirroring Python's TestGal3 class) // Test Cases Section (mirroring Python's TestGal3 class)
@ -404,6 +548,834 @@ TEST(Gal3, IdentityProperties) {
// Add more test cases here if they exist in the Python script // Add more test cases here if they exist in the Python script
} }
/* ************************************************************************* */
/* ************************************************************************* */
TEST(Gal3, Adjoint) {
// Hardcoded test case for adjoint from Python code
const Matrix3 g_R_mat_1 = (Matrix3() << // Added <<
-0.2577418495238488, 0.7767168385303765, 0.5747000015202739,
-0.6694062876067332, -0.572463082520484, 0.47347781496466923,
0.6967527259484512, -0.26267274676776586, 0.6674868290752107
).finished();
const Point3 g_r_vec_1(0.680375434309419, -0.21123414636181392, 0.5661984475172117);
const Velocity3 g_v_vec_1(0.536459189623808, -0.44445057839362445, 0.10793991159086103);
const double g_t_val_1 = -0.0452058962756795;
const Gal3 test_g(Rot3(g_R_mat_1), g_r_vec_1, g_v_vec_1, g_t_val_1);
// Expected adjoint matrix (from Python test)
Matrix10 expected_adj_matrix;
expected_adj_matrix = (Matrix10() << // Added <<
-0.2577418495238488, 0.7767168385303765, 0.5747000015202739, -0.011651451315476903, 0.03511218083817791, 0.025979828658358354, 0.22110620799336958, 0.3876840721487304, -0.42479976201969083, 0.536459189623808,
-0.6694062876067332, -0.572463082520484, 0.47347781496466923, -0.03026111120383766, -0.025878706730076754, 0.021403988992128208, -0.6381411631187845, 0.6286520658991062, -0.14213043434767314, -0.44445057839362445,
0.6967527259484512, -0.26267274676776586, 0.6674868290752107, 0.03149733145902263, -0.011874356944831452, 0.03017434036055619, -0.5313038186955878, -0.22369794100291154, 0.4665680546909384, 0.10793991159086103,
0.0, 0.0, 0.0, -0.2577418495238488, 0.7767168385303765, 0.5747000015202739, -0.23741649654248634, 0.1785366687454684, -0.3477720607401242, 0.0,
0.0, 0.0, 0.0, -0.6694062876067332, -0.572463082520484, 0.47347781496466923, -0.40160003518135456, 0.22475195574939733, -0.2960463760548867, 0.0,
0.0, 0.0, 0.0, 0.6967527259484512, -0.26267274676776586, 0.6674868290752107, -0.47366266867570694, 0.03810916679440729, 0.5094272729993004, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.2577418495238488, 0.7767168385303765, 0.5747000015202739, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.6694062876067332, -0.572463082520484, 0.47347781496466923, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6967527259484512, -0.26267274676776586, 0.6674868290752107, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0
).finished();
// Compute adjoint map
Matrix10 computed_adj = test_g.AdjointMap();
// Compare with expected result
EXPECT(assert_equal(expected_adj_matrix, computed_adj, kTol * 10));
// Test tangent vector for adjoint action
Vector10 test_tangent = (Vector10() << // Added <<
-0.28583171387804845, -0.7221038902918132, -0.09516831208249353,
-0.13619637934919504, -0.05432836001072756, 0.1629798883384306,
-0.20194877118636279, -0.18928645162443278, 0.07312685929426145,
-0.042327821984942095
).finished();
// Expected result after applying adjoint to tangent vector
Vector10 expected_adj_tau = (Vector10() << // Added <<
-0.7097860882934639, 0.5869666797222274, 0.10746143202899403,
0.07529021542994252, 0.21635024626679053, 0.15385717129791027,
-0.05294531834013589, 0.27816922833676766, -0.042176749221369034,
-0.042327821984942095
).finished();
// Apply adjoint to tangent vector
Vector10 computed_adj_tau = test_g.Adjoint(test_tangent);
// Compare with expected result
EXPECT(assert_equal(expected_adj_tau, computed_adj_tau, kTol * 10));
// Test the adjoint property: Log(g * Exp(tau) * g^-1) = Ad(g) * tau
Gal3 exp_tau = Gal3::Expmap(test_tangent);
Gal3 g_exp_tau_ginv = test_g * exp_tau * test_g.inverse();
Vector10 log_result = Gal3::Logmap(g_exp_tau_ginv);
// Compare with expected result for the adjoint property
Vector10 expected_log_result = (Vector10() << // Added <<
-0.7097860882934638, 0.5869666797222274, 0.10746143202899389,
0.07529021542994252, 0.21635024626679047, 0.15385717129791018,
-0.05294531834013579, 0.27816922833676755, -0.04217674922136877,
-0.04232782198494209
).finished();
EXPECT(assert_equal(expected_log_result, log_result, kTol * 10));
}
// ========================================================================
// Jacobian Tests Section (mirroring Python's TestGal3Jacobians class)
// ========================================================================
/* ************************************************************************* */
TEST(Gal3, Jacobian_Compose) {
// Hardcoded data from Python test test_compose_jacobians
Matrix3 g1_R_mat = (Matrix3() <<
-0.3264538540162394, 0.24276278793202133, -0.9135064914894779,
0.9430076454867458, 0.1496317101600385, -0.2972321178273404,
0.06453264097716288, -0.9584761760784951, -0.27777501352435885
).finished();
Point3 g1_r_vec(-0.1995427558196442, 0.7830589040103644, -0.433370507989717);
Velocity3 g1_v_vec(0.8986541507293722, 0.051990700444202176, -0.8278883042875157);
double g1_t_val = -0.6155723080111539;
Gal3 g1(Rot3(g1_R_mat), g1_r_vec, g1_v_vec, g1_t_val);
Matrix3 g2_R_mat = (Matrix3() <<
-0.5204974727334908, 0.7067813015326174, 0.4791060140322894,
0.773189425449982, 0.15205374379417114, 0.6156766776243058,
0.3622989004266723, 0.6908978584436601, -0.6256194178153267
).finished();
Point3 g2_r_vec(-0.8716573584227159, -0.9599539022706234, -0.08459652545144625);
Velocity3 g2_v_vec(0.7018395735425127, -0.4666685012479632, 0.07952068144433233);
double g2_t_val = -0.24958604725524136;
Gal3 g2(Rot3(g2_R_mat), g2_r_vec, g2_v_vec, g2_t_val);
Matrix10 expected_H1 = (Matrix10() <<
-0.5204974727334908, 0.773189425449982, 0.3622989004266723, 0.12990890682589473, -0.19297729247761214, -0.09042475048241341, -0.2823811043440715, 0.3598327802048799, -1.1736098322206205, 0.6973186192002845,
0.7067813015326174, 0.15205374379417114, 0.6908978584436601, -0.17640275132344085, -0.03795049288394836, -0.17243846554606443, -0.650366876876537, 0.542434959867202, 0.5459387038042347, -0.4800290630417764,
0.4791060140322894, 0.6156766776243058, -0.6256194178153267, -0.11957817625853331, -0.15366430835548997, 0.15614587757865273, 0.652649909196605, -0.5858564732208887, -0.07673941868885611, 0.0008110342596663322,
0.0, 0.0, 0.0, -0.5204974727334908, 0.773189425449982, 0.3622989004266723, -0.23055803486323456, -0.29566601949219695, 0.29975516112150496, 0.0,
0.0, 0.0, 0.0, 0.7067813015326174, 0.15205374379417114, 0.6908978584436601, -0.3345116854380048, -0.42869572760154795, 0.4365499053963549, 0.0,
0.0, 0.0, 0.0, 0.4791060140322894, 0.6156766776243058, -0.6256194178153267, 0.24299784710943456, 0.47718330211934956, 0.6556899423712481, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5204974727334908, 0.773189425449982, 0.3622989004266723, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7067813015326174, 0.15205374379417114, 0.6908978584436601, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4791060140322894, 0.6156766776243058, -0.6256194178153267, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0
).finished();
Matrix10 expected_H2 = Matrix10::Identity(); // From Python test
// Calculate analytical Jacobians
Matrix10 H1, H2;
g1.compose(g2, H1, H2);
// Compare
EXPECT(assert_equal(expected_H1, H1, kTol));
EXPECT(assert_equal(expected_H2, H2, kTol));
}
/* ************************************************************************* */
TEST(Gal3, Jacobian_Inverse) {
// Hardcoded data from Python test test_inverse_jacobian
Matrix3 g_R_mat = (Matrix3() <<
0.6680516673568877, 0.2740542884848495, -0.6918101016209183,
0.6729369985913887, -0.6193062871756463, 0.4044941514923666,
-0.31758898858193396, -0.7357676057205693, -0.5981498680963873
).finished();
Point3 g_r_vec(0.06321286832132045, -0.9214393132931736, -0.12472480681013542);
Velocity3 g_v_vec(0.4770686298036335, 0.2799576331302327, -0.29190264050471715);
double g_t_val = 0.3757227805330059;
Gal3 g(Rot3(g_R_mat), g_r_vec, g_v_vec, g_t_val);
Matrix10 expected_H = (Matrix10() <<
-0.6680516673568877, -0.2740542884848495, 0.6918101016209183, 0.2510022299990406, 0.10296843928652219, -0.2599288149818328, -0.33617296841685484, -0.7460372203093872, -0.6201638436054394, -0.4770686298036335,
-0.6729369985913887, 0.6193062871756463, -0.4044941514923666, 0.252837760234292, -0.23268748021920607, 0.1519776673080509, 0.04690510412308051, 0.0894976987957895, 0.05899296065652178, -0.2799576331302327,
0.31758898858193396, 0.7357676057205693, 0.5981498680963873, -0.1193254178566693, -0.27644465064744467, -0.22473853161662533, -0.6077563738775408, -0.3532109665151345, 0.7571646227598928, 0.29190264050471715,
-0.0, -0.0, -0.0, -0.6680516673568877, -0.2740542884848495, 0.6918101016209183, -0.10752062523052273, 0.3867608979391727, 0.049383710440088574, -0.0,
-0.0, -0.0, -0.0, -0.6729369985913887, 0.6193062871756463, -0.4044941514923666, 0.04349430207154942, -0.271014473064643, -0.48729973338095034, -0.0,
-0.0, -0.0, -0.0, 0.31758898858193396, 0.7357676057205693, 0.5981498680963873, -0.1340109682602236, 0.3721751918050696, -0.38664898924142477, -0.0,
-0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.6680516673568877, -0.2740542884848495, 0.6918101016209183, -0.0,
-0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.6729369985913887, 0.6193062871756463, -0.4044941514923666, -0.0,
-0.0, -0.0, -0.0, -0.0, -0.0, -0.0, 0.31758898858193396, 0.7357676057205693, 0.5981498680963873, -0.0,
-0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -1.0
).finished();
// Calculate analytical Jacobian
Matrix10 H;
g.inverse(H);
// Compare
EXPECT(assert_equal(expected_H, H, kTol));
}
/* ************************************************************************* */
// Test Logmap Value and Jacobian (vs numerical) using captured data
/* ************************************************************************* */
TEST(Gal3, Jacobian_Logmap) {
const double jac_tol = 1e-5; // Tolerance for Jacobian checks
// --- Test Case 1 ---
const Matrix3 R1_mat = (Matrix3() <<
-0.5204974727334908, 0.7067813015326174, 0.4791060140322894,
0.773189425449982, 0.15205374379417114, 0.6156766776243058,
0.3622989004266723, 0.6908978584436601, -0.6256194178153267
).finished();
const Point3 r1_vec(-0.8716573584227159, -0.9599539022706234, -0.08459652545144625);
const Velocity3 v1_vec(0.7018395735425127, -0.4666685012479632, 0.07952068144433233);
const double t1_val = -0.24958604725524136;
const Gal3 g1(Rot3(R1_mat), r1_vec, v1_vec, t1_val);
const Vector10 expected_log_tau1 = (Vector10() <<
-1.2604528322799349, -0.3898722924179116, -0.6402392791385879, // rho
-0.34870126525214656, -0.4153032457886797, 1.1791315551702946, // nu
1.4969846781977756, 2.324590726540746, 1.321595100333433, // theta
-0.24958604725524136 // t_tan
).finished();
Matrix10 Hg1_gtsam;
Vector10 log1_gtsam = Gal3::Logmap(g1, Hg1_gtsam);
// Verify value
EXPECT(assert_equal(expected_log_tau1, log1_gtsam, kTol));
// Verify Jacobian (calculated by Gal3::Logmap) against numerical derivative
std::function<Vector10(const Gal3&)> logmap_func1 =
[](const Gal3& g_in) { return Gal3::Logmap(g_in); };
Matrix H_numerical1 = numericalDerivative11<Vector10, Gal3>(logmap_func1, g1, jac_tol);
EXPECT(assert_equal(H_numerical1, Hg1_gtsam, jac_tol));
// --- Test Case 2 ---
const Matrix3 R2_mat = (Matrix3() <<
0.6680516673568877, 0.2740542884848495, -0.6918101016209183,
0.6729369985913887,-0.6193062871756463, 0.4044941514923666,
-0.31758898858193396,-0.7357676057205693, -0.5981498680963873
).finished();
const Point3 r2_vec(0.06321286832132045, -0.9214393132931736, -0.12472480681013542);
const Velocity3 v2_vec(0.4770686298036335, 0.2799576331302327, -0.29190264050471715);
const double t2_val = 0.3757227805330059;
const Gal3 g2(Rot3(R2_mat), r2_vec, v2_vec, t2_val);
const Vector10 expected_log_tau2 = (Vector10() <<
-0.5687147057967125, -0.3805406510759017, -1.063343079044753, // rho
0.5179342682694047, 0.3616924279678234, -0.0984011207117694, // nu
-2.215366977027571, -0.72705858167113, 0.7749725693135466, // theta
0.3757227805330059 // t_tan
).finished();
Matrix10 Hg2_gtsam;
Vector10 log2_gtsam = Gal3::Logmap(g2, Hg2_gtsam);
// Verify value
EXPECT(assert_equal(expected_log_tau2, log2_gtsam, kTol));
// Verify Jacobian
std::function<Vector10(const Gal3&)> logmap_func2 =
[](const Gal3& g_in) { return Gal3::Logmap(g_in); };
Matrix H_numerical2 = numericalDerivative11<Vector10, Gal3>(logmap_func2, g2, jac_tol);
EXPECT(assert_equal(H_numerical2, Hg2_gtsam, jac_tol));
}
/* ************************************************************************* */
TEST(Gal3, Jacobian_Expmap) {
// Hardcoded data from Python test test_expmap_jacobian
Vector10 tau_vec = (Vector10() <<
0.1644755309744135, -0.212580759622502, 0.027598787765563664, // rho
0.06314401798217141, -0.07707725418679535, -0.26078036994807674, // nu
0.3779854061644677, -0.09638288751073966, -0.2917351793587256, // theta
-0.49338105141355293 // t_tan
).finished();
Matrix10 expected_H = (Matrix10() <<
0.9844524218735122, -0.1490063714302354, 0.02908427677999133, -0.24094497482047258, 0.04906744369191897, -0.008766606502586993, 0.0010755746403492512, 0.020896120374367614, 0.09422195803906698, -0.032194210151797825,
0.13700585416694203, 0.9624511801109918, 0.18991633864490795, -0.04463269623239319, -0.23281449603592955, -0.06241249224896669, -0.05013517822202011, -0.011608679508659724, 0.08214064896800377, 0.05141115662809599,
-0.06540787266535304, -0.1806541475679301, 0.9749387339968734, 0.0221898591040537, 0.05898968326106501, -0.23742922610598202, -0.09935687017740953, -0.06570748233856251, -0.025136525844073204, 0.1253312320983055,
0.0, 0.0, 0.0, 0.9844524218735122, -0.1490063714302354, 0.02908427677999133, -0.02734050131352483, -0.1309992318097018, 0.01786013841817754, 0.0,
0.0, 0.0, 0.0, 0.13700585416694203, 0.9624511801109918, 0.18991633864490795, 0.1195266279722227, -0.032519018758919625, 0.035417068913109986, 0.0,
0.0, 0.0, 0.0, -0.06540787266535304, -0.1806541475679301, 0.9749387339968734, -0.0560073503522914, -0.019830198590275447, -0.010039835763395311, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9844524218735122, -0.1490063714302354, 0.02908427677999133, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.13700585416694203, 0.9624511801109918, 0.18991633864490795, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.06540787266535304, -0.1806541475679301, 0.9749387339968734, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0
).finished();
// Calculate analytical Jacobian
Matrix10 H;
Gal3::Expmap(tau_vec, H);
// Compare
EXPECT(assert_equal(expected_H, H, kTol));
}
/* ************************************************************************* */
// From Manif -- but doesnt match. Unclear why.
// TEST(Gal3, Jacobian_Between) {
// // Hardcoded data from Python test test_between_jacobians
// Matrix3 g1_R_mat = (Matrix3() <<
// 0.1981112115076329, -0.12884463237051902, 0.9716743325745948,
// 0.9776658305457298, -0.04497580389201028, -0.20529661674659028,
// 0.0701532013404006, 0.9906443548385938, 0.11705678352030657
// ).finished();
// Point3 g1_r_vec(0.9044594503494257, 0.8323901360074013, 0.2714234559198019);
// Velocity3 g1_v_vec(-0.5142264587405261, -0.7255368464279626, 0.6083535084539808);
// double g1_t_val = -0.6866418214918308;
// Gal3 g1(Rot3(g1_R_mat), g1_r_vec, g1_v_vec, g1_t_val);
// Matrix3 g2_R_mat = (Matrix3() <<
// -0.5427153955878299, 0.8391431164114453, 0.03603927817303032,
// 0.8040805715986894, 0.5314810596281534, -0.2664250694549225,
// -0.24272295682417533, -0.11561450357036887, -0.963181630220753
// ).finished();
// Point3 g2_r_vec(0.9978490360071179, -0.5634861893781862, 0.025864788808796835);
// Velocity3 g2_v_vec(0.04857438013356852, -0.012834026018545996, 0.945550047767139);
// double g2_t_val = -0.41496643117394594;
// Gal3 g2(Rot3(g2_R_mat), g2_r_vec, g2_v_vec, g2_t_val);
// Matrix10 expected_H1 = (Matrix10() <<
// -0.6615763028739445, 0.20669033144286447, 0.7208300093390132, -0.1797340003083421, 0.0561526764696732, 0.19583177414002104, -0.3638953521017815, 0.5514557580523199, -0.4921064751586784, 0.18578447025511585,
// -0.6777438034544028, 0.24655572954989036, -0.6927291022508597, -0.18412631233900273, 0.06698312406057733, -0.1881974492385603, 0.16072606484418983, -1.0875528521598636, -0.544330359681567, 0.8120736895446168,
// 0.3209051765783825, 0.9468312305338058, 0.02303233663866666, 0.08718203910196184, 0.25723074412043495, 0.006257319046242687, -0.41075512891439026, 0.1628186824736606, -0.9703039103797245, -0.4943804748805436,
// -0.0, -0.0, -0.0, -0.6615763028739445, 0.20669033144286447, 0.7208300093390132, 0.07446465266115959, -0.890789069415051, 0.32376778794135697, -0.0,
// -0.0, -0.0, -0.0, -0.6777438034544028, 0.24655572954989036, -0.6927291022508597, -0.2674512085517958, 0.2780902027976629, 0.3606433327863202, -0.0,
// -0.0, -0.0, -0.0, 0.3209051765783825, 0.9468312305338058, 0.02303233663866666, -0.41133443569666744, 0.12204155444521667, 0.714065394510391, -0.0,
// -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.6615763028739445, 0.20669033144286447, 0.7208300093390132, -0.0,
// -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.6777438034544028, 0.24655572954989036, -0.6927291022508597, -0.0,
// -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, 0.3209051765783825, 0.9468312305338058, 0.02303233663866666, -0.0,
// -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -1.0
// ).finished();
// Matrix10 expected_H2 = Matrix10::Identity(); // From Python test
// // Calculate analytical Jacobians
// Matrix10 H1, H2;
// g1.between(g2, H1, H2);
// // Compare
// EXPECT(assert_equal(expected_H1, H1, kTol));
// EXPECT(assert_equal(expected_H2, H2, kTol));
// }
//
// Using numerical derivative as a reference instead of Manif.
//
TEST(Gal3, Jacobian_Between) {
// Define test points g1, g2 (using the same data as before for context)
Matrix3 g1_R_mat = (Matrix3() <<
0.1981112115076329, -0.12884463237051902, 0.9716743325745948,
0.9776658305457298, -0.04497580389201028, -0.20529661674659028,
0.0701532013404006, 0.9906443548385938, 0.11705678352030657
).finished();
Point3 g1_r_vec(0.9044594503494257, 0.8323901360074013, 0.2714234559198019);
Velocity3 g1_v_vec(-0.5142264587405261, -0.7255368464279626, 0.6083535084539808);
double g1_t_val = -0.6866418214918308;
Gal3 g1(Rot3(g1_R_mat), g1_r_vec, g1_v_vec, g1_t_val);
Matrix3 g2_R_mat = (Matrix3() <<
-0.5427153955878299, 0.8391431164114453, 0.03603927817303032,
0.8040805715986894, 0.5314810596281534, -0.2664250694549225,
-0.24272295682417533, -0.11561450357036887, -0.963181630220753
).finished();
Point3 g2_r_vec(0.9978490360071179, -0.5634861893781862, 0.025864788808796835);
Velocity3 g2_v_vec(0.04857438013356852, -0.012834026018545996, 0.945550047767139);
double g2_t_val = -0.41496643117394594;
Gal3 g2(Rot3(g2_R_mat), g2_r_vec, g2_v_vec, g2_t_val);
// --- Verify H1 ---
Matrix10 H1_analytical, H2_analytical; // Placeholders
// Calculate analytical Jacobians using your Gal3::between implementation
g1.between(g2, H1_analytical, H2_analytical);
// Define the 'between' function for numerical derivatives
std::function<Gal3(const Gal3&, const Gal3&)> between_func =
[](const Gal3& g1_in, const Gal3& g2_in) { return g1_in.between(g2_in); };
// Calculate numerical derivative w.r.t first arg
Matrix H1_numerical = numericalDerivative21(between_func, g1, g2, 1e-6); // Adjust epsilon if needed
// Compare analytical H1 with numerical H1
EXPECT(assert_equal(H1_numerical, H1_analytical, 1e-5)); // Tolerance for numerical comparison
// --- Verify H2 ---
// Analytical H2 is already calculated in H2_analytical
// Calculate numerical derivative w.r.t second arg
Matrix H2_numerical = numericalDerivative22(between_func, g1, g2, 1e-6); // Adjust epsilon if needed
// Compare analytical H2 with numerical H2
EXPECT(assert_equal(H2_numerical, H2_analytical, 1e-5)); // Tolerance for numerical comparison
}
/* ************************************************************************* */
// @TODO: Add test_act_jacobians from Python as Gal3::act is not defined.
/* ************************************************************************* */
/* ************************************************************************* */
// Test AdjointMap against numerical derivatives
/* ************************************************************************* */
TEST(Gal3, Jacobian_AdjointMap) {
// Use a sample Gal3 object (e.g., g1 from the between test)
Matrix3 g1_R_mat = (Matrix3() <<
0.1981112115076329, -0.12884463237051902, 0.9716743325745948,
0.9776658305457298, -0.04497580389201028, -0.20529661674659028,
0.0701532013404006, 0.9906443548385938, 0.11705678352030657
).finished();
Point3 g1_r_vec(0.9044594503494257, 0.8323901360074013, 0.2714234559198019);
Velocity3 g1_v_vec(-0.5142264587405261, -0.7255368464279626, 0.6083535084539808);
double g1_t_val = -0.6866418214918308;
Gal3 g = Gal3(Rot3(g1_R_mat), g1_r_vec, g1_v_vec, g1_t_val);
// Pick a sample tangent vector
Vector10 xi = (Vector10() << 0.1, -0.2, 0.3, 0.4, -0.5, 0.6, -0.1, 0.15, -0.25, 0.9).finished();
// Calculate the analytical Adjoint Map
Matrix10 Ad_analytical = g.AdjointMap();
// Define the Adjoint action function for numerical differentiation
// Calculates Ad_g(xi)
std::function<Vector10(const Gal3&, const Vector10&)> adjoint_action =
[](const Gal3& g_in, const Vector10& xi_in) {
return g_in.Adjoint(xi_in); // Assuming g.Adjoint(xi) uses g.AdjointMap() * xi
};
// Calculate numerical derivative of Ad_g(xi) w.r.t xi
// This numerical derivative should be equal to the Adjoint Map itself
Matrix H_xi_numerical = numericalDerivative22(adjoint_action, g, xi);
// Compare analytical AdjointMap with the numerical derivative w.r.t xi
EXPECT(assert_equal(Ad_analytical, H_xi_numerical, 1e-7)); // Use a tolerance suitable for numerical diff
// --- Optional: Also check the derivative w.r.t g ---
// Define Adjoint action function for numerical derivative w.r.t g
std::function<Vector10(const Gal3&, const Vector10&)> adjoint_action_wrt_g =
[](const Gal3& g_in, const Vector10& xi_in) {
Matrix10 H_g; // Analytical H_g from Adjoint
Vector10 result = g_in.Adjoint(xi_in, H_g); // Need Adjoint to compute its H_g
return result; // Only return value for numericalDerivative
};
// Calculate numerical derivative of Ad_g(xi) w.r.t g
Matrix H_g_numerical = numericalDerivative21(adjoint_action_wrt_g, g, xi);
// Calculate analytical derivative of Ad_g(xi) w.r.t g
Matrix10 H_g_analytical;
g.Adjoint(xi, H_g_analytical); // Call Adjoint to get analytical H_g
// Compare analytical H_g with numerical H_g
EXPECT(assert_equal(H_g_analytical, H_g_numerical, 1e-7));
}
/* ************************************************************************* */
// Test inverse Jacobian against numerical derivatives
/* ************************************************************************* */
TEST(Gal3, Jacobian_Inverse2) {
// Use a sample Gal3 object (e.g., g1 from the between test)
Matrix3 g1_R_mat = (Matrix3() <<
0.1981112115076329, -0.12884463237051902, 0.9716743325745948,
0.9776658305457298, -0.04497580389201028, -0.20529661674659028,
0.0701532013404006, 0.9906443548385938, 0.11705678352030657
).finished();
Point3 g1_r_vec(0.9044594503494257, 0.8323901360074013, 0.2714234559198019);
Velocity3 g1_v_vec(-0.5142264587405261, -0.7255368464279626, 0.6083535084539808);
double g1_t_val = -0.6866418214918308;
Gal3 g = Gal3(Rot3(g1_R_mat), g1_r_vec, g1_v_vec, g1_t_val);
// Calculate analytical Jacobian H_inv = d(g.inverse()) / d(g)
Matrix10 H_inv_analytical;
g.inverse(H_inv_analytical); // Assuming inverse calculates its H
// Define inverse function wrapper
std::function<Gal3(const Gal3&)> inverse_func =
[](const Gal3& g_in) { return g_in.inverse(); };
// Calculate numerical Jacobian
Matrix H_inv_numerical = numericalDerivative11(inverse_func, g);
// Compare analytical H_inv with numerical H_inv
EXPECT(assert_equal(H_inv_numerical, H_inv_analytical, 1e-5));
// Optional: Check against theoretical formula H_inv = -Ad(g.inverse())
// EXPECT(assert_equal(-g.inverse().AdjointMap(), H_inv_analytical, 1e-8));
}
/* ************************************************************************* */
// Test compose Jacobians against numerical derivatives
/* ************************************************************************* */
TEST(Gal3, Jacobian_Compose2) {
// Use g1 and g2 from the between test
Matrix3 g1_R_mat = (Matrix3() <<
0.1981112115076329, -0.12884463237051902, 0.9716743325745948,
0.9776658305457298, -0.04497580389201028, -0.20529661674659028,
0.0701532013404006, 0.9906443548385938, 0.11705678352030657
).finished();
Point3 g1_r_vec(0.9044594503494257, 0.8323901360074013, 0.2714234559198019);
Velocity3 g1_v_vec(-0.5142264587405261, -0.7255368464279626, 0.6083535084539808);
double g1_t_val = -0.6866418214918308;
Gal3 g1(Rot3(g1_R_mat), g1_r_vec, g1_v_vec, g1_t_val);
Matrix3 g2_R_mat = (Matrix3() <<
-0.5427153955878299, 0.8391431164114453, 0.03603927817303032,
0.8040805715986894, 0.5314810596281534, -0.2664250694549225,
-0.24272295682417533, -0.11561450357036887, -0.963181630220753
).finished();
Point3 g2_r_vec(0.9978490360071179, -0.5634861893781862, 0.025864788808796835);
Velocity3 g2_v_vec(0.04857438013356852, -0.012834026018545996, 0.945550047767139);
double g2_t_val = -0.41496643117394594;
Gal3 g2(Rot3(g2_R_mat), g2_r_vec, g2_v_vec, g2_t_val);
// Calculate analytical Jacobians H1=d(g1*g2)/dg1, H2=d(g1*g2)/dg2
Matrix10 H1_analytical, H2_analytical;
g1.compose(g2, H1_analytical, H2_analytical);
// Define compose function wrapper
std::function<Gal3(const Gal3&, const Gal3&)> compose_func =
[](const Gal3& g1_in, const Gal3& g2_in) { return g1_in.compose(g2_in); };
// Calculate numerical Jacobians
Matrix H1_numerical = numericalDerivative21(compose_func, g1, g2);
Matrix H2_numerical = numericalDerivative22(compose_func, g1, g2);
// Compare analytical vs numerical
EXPECT(assert_equal(H1_numerical, H1_analytical, 1e-5));
EXPECT(assert_equal(H2_numerical, H2_analytical, 1e-5));
// Optional: Check analytical Jacobians against theoretical formulas (assuming GTSAM conventions)
// H1 = Ad(g2.inverse())
// H2 = Identity() (for right compose convention)
// EXPECT(assert_equal(g2.inverse().AdjointMap(), H1_analytical, 1e-8));
// EXPECT(assert_equal(Matrix10::Identity(), H2_analytical, 1e-8));
}
/* ************************************************************************* */
// Test act method Value and Jacobians (vs numerical) using captured data
/* ************************************************************************* */
TEST(Gal3, Act) {
const double jac_tol = 1e-6; // Tolerance for Jacobian checks
// --- Test Case 1 ---
const Matrix3 R1_mat = (Matrix3() <<
-0.2577418495238488, 0.7767168385303765, 0.5747000015202739,
-0.6694062876067332, -0.572463082520484, 0.47347781496466923,
0.6967527259484512, -0.26267274676776586, 0.6674868290752107
).finished();
const Point3 r1_vec(0.680375434309419, -0.21123414636181392, 0.5661984475172117);
const Velocity3 v1_vec(0.536459189623808, -0.44445057839362445, 0.10793991159086103);
const double t1_val = -0.0452058962756795;
const Gal3 g1(Rot3(R1_mat), r1_vec, v1_vec, t1_val);
const Point3 p_in1(4.967141530112327, -1.3826430117118464, 6.476885381006925);
const Point3 expected_p_out1(2.048479118929923, 0.321903700434202, 8.713486171732242);
Matrix H1g_gtsam = Matrix::Zero(3, 10);
Matrix H1p_gtsam = Matrix::Zero(3, 3);
Point3 p_out1_gtsam = g1.act(p_in1, H1g_gtsam, H1p_gtsam); // Requires Gal3::act to be implemented
// Verify value
EXPECT(assert_equal(expected_p_out1, p_out1_gtsam, kTol));
// Verify Jacobians vs numerical
std::function<Point3(const Gal3&, const Point3&)> act_func1 =
[](const Gal3& g_in, const Point3& p_in) { return g_in.act(p_in); }; // Requires Gal3::act
Matrix H1g_numerical = numericalDerivative21(act_func1, g1, p_in1, jac_tol);
Matrix H1p_numerical = numericalDerivative22(act_func1, g1, p_in1, jac_tol);
EXPECT(assert_equal(H1g_numerical, H1g_gtsam, jac_tol));
EXPECT(assert_equal(H1p_numerical, H1p_gtsam, jac_tol));
// --- Test Case 2 ---
const Matrix3 R2_mat = (Matrix3() <<
0.1981112115076329, -0.12884463237051902, 0.9716743325745948,
0.9776658305457298, -0.04497580389201028, -0.20529661674659028,
0.0701532013404006, 0.9906443548385938, 0.11705678352030657
).finished();
const Point3 r2_vec(0.9044594503494257, 0.8323901360074013, 0.2714234559198019);
const Velocity3 v2_vec(-0.5142264587405261, -0.7255368464279626, 0.6083535084539808);
const double t2_val = -0.6866418214918308;
const Gal3 g2(Rot3(R2_mat), r2_vec, v2_vec, t2_val);
const Point3 p_in2(15.230298564080254, -2.3415337472333597, -2.3413695694918055);
const Point3 expected_p_out2(1.9483976916013555, 16.30852024491054, -1.2538227216376843);
Matrix H2g_gtsam = Matrix::Zero(3, 10);
Matrix H2p_gtsam = Matrix::Zero(3, 3);
Point3 p_out2_gtsam = g2.act(p_in2, H2g_gtsam, H2p_gtsam); // Requires Gal3::act to be implemented
// Verify value
EXPECT(assert_equal(expected_p_out2, p_out2_gtsam, kTol));
// Verify Jacobians vs numerical
std::function<Point3(const Gal3&, const Point3&)> act_func2 =
[](const Gal3& g_in, const Point3& p_in) { return g_in.act(p_in); }; // Requires Gal3::act
Matrix H2g_numerical = numericalDerivative21(act_func2, g2, p_in2, jac_tol);
Matrix H2p_numerical = numericalDerivative22(act_func2, g2, p_in2, jac_tol);
EXPECT(assert_equal(H2g_numerical, H2g_gtsam, jac_tol));
EXPECT(assert_equal(H2p_numerical, H2p_gtsam, jac_tol));
}
/* ************************************************************************* */
// Test interpolate method against manif data (value check only)
/* ************************************************************************* */
TEST(Gal3, Interpolate) {
const double interp_tol = 1e-6; // Tolerance for interpolation value comparison
// --- Test Case 1 ---
const Matrix3 R1_mat = (Matrix3() <<
-0.5427153955878299, 0.8391431164114453, 0.03603927817303032,
0.8040805715986894, 0.5314810596281534, -0.2664250694549225,
-0.24272295682417533,-0.11561450357036887,-0.963181630220753
).finished();
const Point3 r1_vec(0.9978490360071179, -0.5634861893781862, 0.025864788808796835);
const Velocity3 v1_vec(0.04857438013356852, -0.012834026018545996, 0.945550047767139);
const double t1_val = -0.41496643117394594;
const Gal3 g1(Rot3(R1_mat), r1_vec, v1_vec, t1_val);
const Matrix3 R2_mat = (Matrix3() <<
-0.3264538540162394, 0.24276278793202133, -0.9135064914894779,
0.9430076454867458, 0.1496317101600385, -0.2972321178273404,
0.06453264097716288,-0.9584761760784951, -0.27777501352435885
).finished();
const Point3 r2_vec(-0.1995427558196442, 0.7830589040103644, -0.433370507989717);
const Velocity3 v2_vec(0.8986541507293722, 0.051990700444202176, -0.8278883042875157);
const double t2_val = -0.6155723080111539;
const Gal3 g2(Rot3(R2_mat), r2_vec, v2_vec, t2_val);
// Expected results for different alphas
const Gal3 expected_alpha0_00 = g1; // Should be identical to g1
const Gal3 expected_alpha0_25(Rot3(-0.5305293740379828, 0.8049951577356123, -0.26555861745588893,
0.8357380608208966, 0.444360967494815, -0.32262241748272774,
-0.14170559967126867, -0.39309811318459514,-0.9085116380281089),
Point3(0.7319725410358775, -0.24509083842031143, -0.20526665070473993),
Velocity3(0.447233815335834, 0.08156238532481315, 0.6212732810121263),
-0.46511790038324796);
const Gal3 expected_alpha0_50(Rot3(-0.4871812472793253, 0.6852678695634308, -0.5413523614461815,
0.8717937932763143, 0.34522257149684665,-0.34755856791913387,
-0.051283665082120816,-0.6412716453057169, -0.7655982383878919),
Point3(0.4275876127256323, 0.0806397132393819, -0.3622648631703127),
Velocity3(0.7397753319939113, 0.12027591889377548, 0.19009330402180313),
-0.5152693695925499);
const Gal3 expected_alpha0_75(Rot3(-0.416880477991759, 0.49158776471130083,-0.7645600935541361,
0.9087464582621175, 0.24369303223618222,-0.338812013712018,
0.019762127046961175,-0.8360353913714909, -0.5483195078682666),
Point3(0.10860773480095642, 0.42140693978775756, -0.4380637787537704),
Velocity3(0.895925123398753, 0.10738792759782673, -0.3083508669060544),
-0.5654208388018519);
const Gal3 expected_alpha1_00 = g2; // Should be identical to g2
// Compare values
// Requires Gal3::interpolate to be implemented
EXPECT(assert_equal(expected_alpha0_00, g1.interpolate(g2, 0.0), interp_tol));
EXPECT(assert_equal(expected_alpha0_25, g1.interpolate(g2, 0.25), interp_tol));
EXPECT(assert_equal(expected_alpha0_50, g1.interpolate(g2, 0.5), interp_tol));
EXPECT(assert_equal(expected_alpha0_75, g1.interpolate(g2, 0.75), interp_tol));
EXPECT(assert_equal(expected_alpha1_00, g1.interpolate(g2, 1.0), interp_tol));
}
/* ************************************************************************* */
// Added Test: Jacobians for Static Constructors
/* ************************************************************************* */
TEST(Gal3, Jacobian_StaticConstructors) {
// Note: This test verifies analytical Jacobians against numerical derivatives
// of the constructor functions. Gal3::Create and Gal3::FromPoseVelocityTime
// implement analytical Jacobians, so this test is NOT circular.
const double jac_tol = 1e-7; // Tolerance for Jacobian checks
// --- Test Gal3::Create ---
gtsam::Matrix H1_ana, H2_ana, H3_ana, H4_ana; // Use dynamic Matrix type
Gal3::Create(kTestRot, kTestPos, kTestVel, kTestTime, H1_ana, H2_ana, H3_ana, H4_ana);
// Numerical derivatives
// Explicitly use const double& for the double argument in lambda
std::function<Gal3(const Rot3&, const Point3&, const Velocity3&, const double&)> create_func =
[](const Rot3& R, const Point3& r, const Velocity3& v, const double& t){
return Gal3::Create(R, r, v, t); // Call without Jacobians for numerical diff
};
// Pass kTestTime by const reference to match lambda
const double& time_ref = kTestTime;
Matrix H1_num = numericalDerivative41(create_func, kTestRot, kTestPos, kTestVel, time_ref, jac_tol);
Matrix H2_num = numericalDerivative42(create_func, kTestRot, kTestPos, kTestVel, time_ref, jac_tol);
Matrix H3_num = numericalDerivative43(create_func, kTestRot, kTestPos, kTestVel, time_ref, jac_tol);
Matrix H4_num = numericalDerivative44(create_func, kTestRot, kTestPos, kTestVel, time_ref, jac_tol);
// Compare
EXPECT(assert_equal(H1_num, H1_ana, jac_tol));
EXPECT(assert_equal(H2_num, H2_ana, jac_tol));
EXPECT(assert_equal(H3_num, H3_ana, jac_tol));
EXPECT(assert_equal(H4_num, H4_ana, jac_tol));
// --- Test Gal3::FromPoseVelocityTime ---
gtsam::Matrix Hpv1_ana, Hpv2_ana, Hpv3_ana; // Use dynamic Matrix type
Gal3::FromPoseVelocityTime(kTestPose, kTestVel, kTestTime, Hpv1_ana, Hpv2_ana, Hpv3_ana);
// Numerical derivatives
// Explicitly use const double& for the double argument in lambda
std::function<Gal3(const Pose3&, const Velocity3&, const double&)> fromPoseVelTime_func =
[](const Pose3& pose, const Velocity3& v, const double& t){
return Gal3::FromPoseVelocityTime(pose, v, t); // Call without Jacobians
};
// Pass kTestTime by const reference to match lambda
Matrix Hpv1_num = numericalDerivative31(fromPoseVelTime_func, kTestPose, kTestVel, time_ref, jac_tol);
Matrix Hpv2_num = numericalDerivative32(fromPoseVelTime_func, kTestPose, kTestVel, time_ref, jac_tol);
Matrix Hpv3_num = numericalDerivative33(fromPoseVelTime_func, kTestPose, kTestVel, time_ref, jac_tol);
// Compare
EXPECT(assert_equal(Hpv1_num, Hpv1_ana, jac_tol));
EXPECT(assert_equal(Hpv2_num, Hpv2_ana, jac_tol));
EXPECT(assert_equal(Hpv3_num, Hpv3_ana, jac_tol));
}
/* ************************************************************************* */
// Added Test: Jacobians for Component Accessors
/* ************************************************************************* */
TEST(Gal3, Jacobian_Accessors) {
// Note: This test verifies analytical Jacobians against numerical derivatives
// of the accessor functions. The accessors in Gal3 implement analytical
// Jacobians, so this test is NOT circular.
const double jac_tol = 1e-7;
// --- Test rotation() / attitude() Jacobian ---
gtsam::Matrix Hrot_ana; // Use dynamic Matrix type
kTestGal3.rotation(Hrot_ana);
std::function<Rot3(const Gal3&)> rot_func =
[](const Gal3& g) { return g.rotation(); };
Matrix Hrot_num = numericalDerivative11<Rot3, Gal3>(rot_func, kTestGal3, jac_tol);
EXPECT(assert_equal(Hrot_num, Hrot_ana, jac_tol));
// --- Test translation() / position() Jacobian ---
gtsam::Matrix Hpos_ana; // Use dynamic Matrix type
kTestGal3.translation(Hpos_ana);
std::function<Point3(const Gal3&)> pos_func =
[](const Gal3& g) { return g.translation(); };
Matrix Hpos_num = numericalDerivative11<Point3, Gal3>(pos_func, kTestGal3, jac_tol);
EXPECT(assert_equal(Hpos_num, Hpos_ana, jac_tol));
// --- Test velocity() Jacobian ---
gtsam::Matrix Hvel_ana; // Use dynamic Matrix type
kTestGal3.velocity(Hvel_ana);
std::function<Velocity3(const Gal3&)> vel_func =
[](const Gal3& g) { return g.velocity(); };
Matrix Hvel_num = numericalDerivative11<Velocity3, Gal3>(vel_func, kTestGal3, jac_tol);
EXPECT(assert_equal(Hvel_num, Hvel_ana, jac_tol));
// --- Test time() Jacobian ---
gtsam::Matrix Htime_ana; // Use dynamic Matrix type
kTestGal3.time(Htime_ana);
std::function<double(const Gal3&)> time_func =
[](const Gal3& g) { return g.time(); };
Matrix Htime_num = numericalDerivative11<double, Gal3>(time_func, kTestGal3, jac_tol);
EXPECT(assert_equal(Htime_num, Htime_ana, jac_tol));
}
/* ************************************************************************* */
// Added Test: Jacobians for interpolate
/* ************************************************************************* */
TEST(Gal3, Jacobian_Interpolate) {
// *** CIRCULARITY WARNING ***
// Gal3::interpolate computes its Jacobians (H1, H2, Ha) using a chain rule
// that involves the Jacobians of Logmap and Expmap. Since Gal3::Logmap and
// Gal3::Expmap compute their Jacobians numerically (via LogmapDerivative
// and ExpmapDerivative in Gal3.cpp), comparing the interpolate Jacobians
// against numerical derivatives here is circular. It verifies the chain rule
// implementation assuming the underlying numerical derivatives are correct.
const double jac_tol = 1e-7;
const Gal3 g1 = kTestGal3_Lie1; // Use pre-defined states
const Gal3 g2 = kTestGal3_Lie2;
const double alpha = 0.3; // Example interpolation factor
gtsam::Matrix H1_ana, H2_ana, Ha_ana; // Use dynamic Matrix type
g1.interpolate(g2, alpha, H1_ana, H2_ana, Ha_ana);
// Numerical derivatives
// Explicitly use const double& for the double argument in lambda
std::function<Gal3(const Gal3&, const Gal3&, const double&)> interp_func =
[](const Gal3& g1_in, const Gal3& g2_in, const double& a){
// Call interpolate *without* asking for its Jacobians for numerical diff
return g1_in.interpolate(g2_in, a);
};
// Pass alpha by const reference to match lambda
const double& alpha_ref = alpha;
Matrix H1_num = numericalDerivative31(interp_func, g1, g2, alpha_ref, jac_tol);
Matrix H2_num = numericalDerivative32(interp_func, g1, g2, alpha_ref, jac_tol);
Matrix Ha_num = numericalDerivative33(interp_func, g1, g2, alpha_ref, jac_tol);
// Compare analytical (numerically derived) vs numerical
EXPECT(assert_equal(H1_num, H1_ana, jac_tol));
EXPECT(assert_equal(H2_num, H2_ana, jac_tol));
EXPECT(assert_equal(Ha_num, Ha_ana, jac_tol));
}
/* ************************************************************************* */
// Added Test: Static Adjoint functions
/* ************************************************************************* */
TEST(Gal3, StaticAdjoint) { // Removed duplicate TEST macro here
// Note: This test verifies the analytical Jacobians (Hxi, Hy) provided by
// the static Gal3::adjoint function against expected analytical properties
// (e.g., Hy = adjointMap(xi), Hxi = -adjointMap(y)) and also against
// numerical derivatives of the static adjoint function itself.
// Since the Jacobians in static Gal3::adjoint are implemented analytically
// using adjointMap, this test is NOT circular regarding those Jacobians.
const double jac_tol = 1e-7;
// Create two tangent vectors
Vector10 xi = (Vector10() << 0.1, -0.2, 0.3, 0.4, -0.5, 0.6, -0.1, 0.15, -0.25, 0.9).finished();
Vector10 y = (Vector10() << -0.5, 0.4, -0.3, 0.2, -0.1, 0.0, 0.3, -0.35, 0.45, -0.1).finished();
// --- Test static adjointMap ---
Matrix10 ad_xi_map = Gal3::adjointMap(xi);
// Check if ad_xi_map * y equals adjoint(xi, y)
// Evaluate the product explicitly into a Vector10 for assert_equal
Vector10 ad_xi_map_times_y = ad_xi_map * y;
Vector10 ad_xi_y_direct = Gal3::adjoint(xi, y);
EXPECT(assert_equal(ad_xi_map_times_y, ad_xi_y_direct, kTol));
// Verify Jacobian of adjoint(xi,y) w.r.t y equals adjointMap(xi)
std::function<Vector10(const Vector10&, const Vector10&)> static_adjoint_func =
[](const Vector10& xi_in, const Vector10& y_in){
// Call static adjoint *without* asking for its Jacobians for numerical diff
return Gal3::adjoint(xi_in, y_in);
};
Matrix Hy_num = numericalDerivative22(static_adjoint_func, xi, y, jac_tol);
EXPECT(assert_equal(ad_xi_map, Hy_num, jac_tol));
// --- Test static adjoint ---
gtsam::Matrix Hxi_ana, Hy_ana; // Use dynamic Matrix type
Gal3::adjoint(xi, y, Hxi_ana, Hy_ana); // Get analytical Jacobians
// Check analytical H_y matches ad_xi_map (which was checked numerically above)
EXPECT(assert_equal(ad_xi_map, Hy_ana, kTol));
// Check analytical H_xi matches -ad(y)
Matrix10 minus_ad_y_map = -Gal3::adjointMap(y);
EXPECT(assert_equal(minus_ad_y_map, Hxi_ana, kTol));
// Verify analytical H_xi against numerical derivative
Matrix Hxi_num = numericalDerivative21(static_adjoint_func, xi, y, jac_tol);
EXPECT(assert_equal(Hxi_num, Hxi_ana, jac_tol));
// Verify Jacobi Identity: [x, [y, z]] + [y, [z, x]] + [z, [x, y]] = 0
Vector10 z = (Vector10() << 0.7, 0.8, 0.9, -0.1, -0.2, -0.3, 0.5, 0.6, 0.7, 0.1).finished();
Vector10 term1 = Gal3::adjoint(xi, Gal3::adjoint(y, z));
Vector10 term2 = Gal3::adjoint(y, Gal3::adjoint(z, xi));
Vector10 term3 = Gal3::adjoint(z, Gal3::adjoint(xi, y));
// Evaluate the sum explicitly into a Vector10 for assert_equal
Vector10 sum_terms = term1 + term2 + term3;
// Explicitly cast Vector10::Zero() to Vector10 to resolve ambiguity
EXPECT(assert_equal(Vector10(Vector10::Zero()), sum_terms, jac_tol));
}
/* ************************************************************************* */ /* ************************************************************************* */
int main() { int main() {
TestResult tr; TestResult tr;