@@ -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
287374TEST (pseudo_inverse, pseudo_inverse_of_square_matrix_same_as_inverse) {
0 commit comments