@@ -338,4 +338,97 @@ TEST(anti_windup, test_anti_windup) {
338338 EXPECT_TRUE (expected.isApprox (result, 1e-10 ));
339339}
340340
341+ TEST (average_quaternions, empty_input_throws) {
342+ std::vector<Eigen::Quaterniond> quats;
343+ EXPECT_THROW (average_quaternions (quats), std::invalid_argument);
344+ }
345+
346+ TEST (average_quaternions, single_quaternion_returns_same) {
347+ Eigen::Quaterniond q =
348+ Eigen::Quaterniond (Eigen::AngleAxisd (0.7 , Eigen::Vector3d::UnitX ()));
349+
350+ std::vector<Eigen::Quaterniond> quats{q};
351+
352+ Eigen::Quaterniond avg = average_quaternions (quats);
353+
354+ EXPECT_NEAR (avg.angularDistance (q), 0.0 , 1e-12 );
355+ }
356+
357+ TEST (average_quaternions, identical_quaternions) {
358+ Eigen::Quaterniond q =
359+ Eigen::Quaterniond (Eigen::AngleAxisd (1.2 , Eigen::Vector3d::UnitZ ()));
360+
361+ std::vector<Eigen::Quaterniond> quats = {q, q, q};
362+
363+ Eigen::Quaterniond avg = average_quaternions (quats);
364+
365+ EXPECT_NEAR (avg.angularDistance (q), 0.0 , 1e-12 );
366+ }
367+
368+ TEST (average_quaternions, antipodal_quaternions) {
369+ Eigen::Quaterniond q (Eigen::AngleAxisd (0.8 , Eigen::Vector3d::UnitY ()));
370+
371+ Eigen::Quaterniond q_neg = q;
372+ q_neg.coeffs () *= -1.0 ;
373+
374+ std::vector<Eigen::Quaterniond> quats = {q, q_neg};
375+
376+ Eigen::Quaterniond avg = average_quaternions (quats);
377+
378+ EXPECT_NEAR (avg.angularDistance (q), 0.0 , 1e-12 );
379+ }
380+
381+ TEST (average_quaternions, average_of_small_rotations_near_identity) {
382+ std::vector<Eigen::Quaterniond> quats;
383+
384+ quats.emplace_back (Eigen::AngleAxisd (0.05 , Eigen::Vector3d::UnitX ()));
385+ quats.emplace_back (Eigen::AngleAxisd (-0.05 , Eigen::Vector3d::UnitX ()));
386+ quats.emplace_back (Eigen::AngleAxisd (0.02 , Eigen::Vector3d::UnitX ()));
387+
388+ Eigen::Quaterniond avg = average_quaternions (quats);
389+
390+ EXPECT_TRUE (avg.isApprox (Eigen::Quaterniond::Identity (), 1e-2 ));
391+ }
392+
393+ TEST (average_quaternions, noisy_measurements_about_known_rotation) {
394+ const Eigen::Quaterniond truth =
395+ Eigen::Quaterniond (Eigen::AngleAxisd (1.0 , Eigen::Vector3d::UnitZ ()));
396+
397+ std::vector<Eigen::Quaterniond> quats;
398+ quats.emplace_back (Eigen::AngleAxisd (1.02 , Eigen::Vector3d::UnitZ ()));
399+ quats.emplace_back (Eigen::AngleAxisd (0.98 , Eigen::Vector3d::UnitZ ()));
400+ quats.emplace_back (Eigen::AngleAxisd (1.01 , Eigen::Vector3d::UnitZ ()));
401+ quats.emplace_back (Eigen::AngleAxisd (0.99 , Eigen::Vector3d::UnitZ ()));
402+
403+ Eigen::Quaterniond avg = average_quaternions (quats);
404+
405+ EXPECT_NEAR (avg.angularDistance (truth), 0.0 , 1e-2 );
406+ }
407+
408+ TEST (average_quaternions, symmetric_opposing_axes) {
409+ Eigen::Quaterniond qx =
410+ Eigen::Quaterniond (Eigen::AngleAxisd (0.5 , Eigen::Vector3d::UnitX ()));
411+ Eigen::Quaterniond qy =
412+ Eigen::Quaterniond (Eigen::AngleAxisd (0.5 , Eigen::Vector3d::UnitY ()));
413+
414+ std::vector<Eigen::Quaterniond> quats{qx, qy};
415+
416+ Eigen::Quaterniond avg = average_quaternions (quats);
417+
418+ double dx = avg.angularDistance (qx);
419+ double dy = avg.angularDistance (qy);
420+
421+ EXPECT_NEAR (dx, dy, 1e-6 );
422+ }
423+
424+ TEST (average_quaternions, test_degeneracy) {
425+ Eigen::Quaterniond q0 =
426+ Eigen::Quaterniond (Eigen::AngleAxisd (0.0 , Eigen::Vector3d::UnitZ ()));
427+
428+ Eigen::Quaterniond q180 =
429+ Eigen::Quaterniond (Eigen::AngleAxisd (M_PI, Eigen::Vector3d::UnitZ ()));
430+
431+ EXPECT_THROW (average_quaternions ({q0, q180}), std::runtime_error);
432+ }
433+
341434} // namespace vortex::utils::math
0 commit comments