Skip to content

Commit 4d3b35e

Browse files
authored
quat error vec function (#45)
1 parent 95830f1 commit 4d3b35e

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

vortex_utils/cpp_test/test_math.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,93 @@ TEST(euler_to_quat_vec3, roll_pitch_yaw_all_nonzero) {
282282
EXPECT_TRUE(result.isApprox(expected, 1e-3));
283283
}
284284

285+
TEST(quaternion_error, identity_quaternions_give_zero_error) {
286+
Eigen::Quaterniond q = Eigen::Quaterniond::Identity();
287+
Eigen::Vector3d err = quaternion_error(q, q);
288+
EXPECT_TRUE(err.isApprox(Eigen::Vector3d::Zero(), 1e-12));
289+
}
290+
291+
TEST(quaternion_error, small_yaw_rotation) {
292+
double angle = 0.1;
293+
Eigen::Quaterniond q_ref = Eigen::Quaterniond::Identity();
294+
Eigen::Quaterniond q_meas(
295+
Eigen::AngleAxisd(angle, Eigen::Vector3d::UnitZ()));
296+
297+
Eigen::Vector3d err = quaternion_error(q_ref, q_meas);
298+
299+
// For small angles, 2*vec(q_err) ≈ axis-angle vector
300+
EXPECT_NEAR(err(0), 0.0, 1e-6);
301+
EXPECT_NEAR(err(1), 0.0, 1e-6);
302+
EXPECT_NEAR(err(2), angle, 1e-3);
303+
}
304+
305+
TEST(quaternion_error, small_roll_rotation) {
306+
double angle = 0.15;
307+
Eigen::Quaterniond q_ref = Eigen::Quaterniond::Identity();
308+
Eigen::Quaterniond q_meas(
309+
Eigen::AngleAxisd(angle, Eigen::Vector3d::UnitX()));
310+
311+
Eigen::Vector3d err = quaternion_error(q_ref, q_meas);
312+
313+
EXPECT_NEAR(err(0), angle, 1e-3);
314+
EXPECT_NEAR(err(1), 0.0, 1e-6);
315+
EXPECT_NEAR(err(2), 0.0, 1e-6);
316+
}
317+
318+
TEST(quaternion_error, error_is_zero_for_same_orientation) {
319+
Eigen::Quaterniond q(Eigen::AngleAxisd(1.2, Eigen::Vector3d::UnitY()));
320+
Eigen::Vector3d err = quaternion_error(q, q);
321+
EXPECT_TRUE(err.isApprox(Eigen::Vector3d::Zero(), 1e-12));
322+
}
323+
324+
TEST(quaternion_error, antipodal_quaternions_give_zero_error) {
325+
Eigen::Quaterniond q(Eigen::AngleAxisd(0.8, Eigen::Vector3d::UnitZ()));
326+
Eigen::Quaterniond q_neg = q;
327+
q_neg.coeffs() = -q_neg.coeffs();
328+
329+
Eigen::Vector3d err = quaternion_error(q, q_neg);
330+
EXPECT_TRUE(err.isApprox(Eigen::Vector3d::Zero(), 1e-12));
331+
}
332+
333+
TEST(quaternion_error, ninety_degree_rotation) {
334+
Eigen::Quaterniond q_ref = Eigen::Quaterniond::Identity();
335+
Eigen::Quaterniond q_meas(
336+
Eigen::AngleAxisd(M_PI / 2, Eigen::Vector3d::UnitZ()));
337+
338+
Eigen::Vector3d err = quaternion_error(q_ref, q_meas);
339+
340+
// 2*sin(pi/4) ≈ 1.414, actual angle is pi/2 ≈ 1.571
341+
// The approximation diverges for larger angles but direction is correct
342+
EXPECT_NEAR(err(0), 0.0, 1e-12);
343+
EXPECT_NEAR(err(1), 0.0, 1e-12);
344+
EXPECT_GT(err(2), 0.0);
345+
}
346+
347+
TEST(quaternion_error, sign_convention_positive_for_positive_rotation) {
348+
Eigen::Quaterniond q_ref = Eigen::Quaterniond::Identity();
349+
350+
Eigen::Quaterniond q_pos(Eigen::AngleAxisd(0.3, Eigen::Vector3d::UnitX()));
351+
Eigen::Quaterniond q_neg(Eigen::AngleAxisd(-0.3, Eigen::Vector3d::UnitX()));
352+
353+
Eigen::Vector3d err_pos = quaternion_error(q_ref, q_pos);
354+
Eigen::Vector3d err_neg = quaternion_error(q_ref, q_neg);
355+
356+
EXPECT_GT(err_pos(0), 0.0);
357+
EXPECT_LT(err_neg(0), 0.0);
358+
}
359+
360+
TEST(quaternion_error, combined_rotation) {
361+
Eigen::Quaterniond q_ref(Eigen::AngleAxisd(0.5, Eigen::Vector3d::UnitZ()));
362+
Eigen::Quaterniond q_meas(Eigen::AngleAxisd(0.7, Eigen::Vector3d::UnitZ()));
363+
364+
Eigen::Vector3d err = quaternion_error(q_ref, q_meas);
365+
366+
// Error should be approximately 0.2 rad about Z
367+
EXPECT_NEAR(err(0), 0.0, 1e-12);
368+
EXPECT_NEAR(err(1), 0.0, 1e-12);
369+
EXPECT_NEAR(err(2), 0.2, 1e-3);
370+
}
371+
285372
// Test pseudo inverse, results from
286373
// https://www.emathhelp.net/calculators/linear-algebra/pseudoinverse-calculator
287374
TEST(pseudo_inverse, pseudo_inverse_of_square_matrix_same_as_inverse) {

vortex_utils/include/vortex/utils/math.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,21 @@ Eigen::Quaterniond euler_to_quat(const double roll,
112112
*/
113113
Eigen::Quaterniond euler_to_quat(const Eigen::Vector3d& euler);
114114

115+
/**
116+
* @brief Compute the 3D orientation error between two quaternions.
117+
*
118+
* Computes the rotation from @p q_from to @p q_to and returns the
119+
* corresponding axis-angle-like error vector: 2 * vec(q_from^{-1} * q_to).
120+
* Uses shortest-path convention (flips sign when q_err.w < 0).
121+
* Only an approximation. For large errors it underestimates the true angle.
122+
*
123+
* @param q_from Start orientation (e.g. reference).
124+
* @param q_to End orientation (e.g. measured).
125+
* @return 3D error vector in radians (exact for small errors).
126+
*/
127+
Eigen::Vector3d quaternion_error(const Eigen::Quaterniond& q_from,
128+
const Eigen::Quaterniond& q_to);
129+
115130
/**
116131
* @brief Computes the Moore-Penrose pseudo-inverse of a matrix.
117132
* @param matrix The input matrix to be inverted.

vortex_utils/src/math.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ Eigen::Quaterniond euler_to_quat(const Eigen::Vector3d& euler) {
103103
return q.normalized();
104104
}
105105

106+
Eigen::Vector3d quaternion_error(const Eigen::Quaterniond& q_from,
107+
const Eigen::Quaterniond& q_to) {
108+
Eigen::Quaterniond q_err = q_from.conjugate() * q_to;
109+
q_err.normalize();
110+
111+
if (q_err.w() < 0.0) {
112+
q_err.coeffs() = -q_err.coeffs();
113+
}
114+
115+
return 2.0 * q_err.vec();
116+
}
117+
106118
Eigen::MatrixXd pseudo_inverse(const Eigen::MatrixXd& matrix) {
107119
if (matrix.rows() >= matrix.cols()) {
108120
return (matrix.transpose() * matrix).ldlt().solve(matrix.transpose());

0 commit comments

Comments
 (0)