Reuse rotation functionality in (Eigen)QuaternionManifold

Allow to specify the memory layout of quaternion coefficients using a
template parameter which defaults to Ceres coefficients order.

The changes are, for the most part, backwards compatible unless the
floating-point type is explicitly specified, e.g., as
&ceres::QuaternionToAngleAxis<double> to obtain a pointer to the
corresponding function. In such rare use cases, the coefficients order
must be given explicitly first as
ceres::QuaternionToAngleAxis<ceres::CeresQuaternionOrder>. In normal
situations, however, this should not be needed.

Change-Id: I05dd80f0593672dec656cc785cf06fe5268aee74
diff --git a/include/ceres/manifold_test_utils.h b/include/ceres/manifold_test_utils.h
index 37841f9..4872e66 100644
--- a/include/ceres/manifold_test_utils.h
+++ b/include/ceres/manifold_test_utils.h
@@ -1,5 +1,5 @@
 // Ceres Solver - A fast non-linear least squares minimizer
-// Copyright 2023 Google Inc. All rights reserved.
+// Copyright 2025 Google Inc. All rights reserved.
 // http://ceres-solver.org/
 //
 // Redistribution and use in source and binary forms, with or without
@@ -31,11 +31,13 @@
 #include <cmath>
 #include <limits>
 #include <memory>
+#include <type_traits>
 
 #include "ceres/dynamic_numeric_diff_cost_function.h"
 #include "ceres/internal/eigen.h"
 #include "ceres/manifold.h"
 #include "ceres/numeric_diff_options.h"
+#include "ceres/rotation.h"
 #include "ceres/types.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -214,16 +216,53 @@
   Vector actual = Vector::Zero(ambient_size);
   EXPECT_TRUE(arg.Plus(x.data(), y_minus_x.data(), actual.data()));
 
-  const double n = (actual - Vector{y}).norm();
-  const double d = y.norm();
-  const double diffnorm = (d == 0.0) ? n : (n / d);
-  if (diffnorm > tolerance) {
-    *result_listener << "\nx: " << x.transpose()
-                     << "\nexpected: " << y.transpose()
-                     << "\nactual:" << actual.transpose()
-                     << "\ndiff:" << (y - actual).transpose()
-                     << "\ndiffnorm: " << diffnorm;
-    return false;
+  using ManifoldType = std::decay_t<decltype(arg)>;
+
+  constexpr bool kIsCeresQuaternion =
+      std::is_same_v<ManifoldType, ceres::QuaternionManifold>;
+  constexpr bool kIsEigenQuaternion =
+      std::is_same_v<ManifoldType, ceres::EigenQuaternionManifold>;
+
+  if constexpr (!(kIsEigenQuaternion || kIsCeresQuaternion)) {
+    const double n = (actual - Vector{y}).norm();
+    const double d = y.norm();
+    const double diffnorm = (d == 0.0) ? n : (n / d);
+    if (diffnorm > tolerance) {
+      *result_listener << "\nx: " << x.transpose()
+                       << "\nexpected: " << y.transpose()
+                       << "\nactual: " << actual.transpose()
+                       << "\ndiff: " << (y - actual).transpose()
+                       << "\ndiffnorm: " << diffnorm;
+      return false;
+    }
+  } else {
+    // Quaternions are equivalent up to a sign change. Compute the algebraic
+    // distance by taking the minimum of |x+y| and |x-y| where |∘| is the L²
+    // norm. For more details, refer to
+    //
+    //   Hartley, R., Trumpf, J., Dai, Y., & Li, H. (2013). Rotation Averaging.
+    //   International Journal of Computer Vision, 103(3), 267–305.
+    //   https://doi.org/10.1007/s11263-012-0601-0
+    //
+    // The computation here is closely related to the IsNearQuaternion matcher
+    // use in rotation_test.cc. The matcher, however, does not compute the
+    // distance but only compares the coefficients.
+    const Eigen::Vector4d plus = actual + Vector{y};
+    const Eigen::Vector4d minus = actual - Vector{y};
+
+    const double plus_norm = plus.norm();
+    const double minus_norm = minus.norm();
+    const double diffnorm = std::min(plus_norm, minus_norm);
+
+    if (diffnorm > tolerance) {
+      *result_listener << "\nx: " << x.transpose()
+                       << "\nexpected: " << y.transpose()
+                       << "\nactual: " << actual.transpose()
+                       << "\ndiff +: " << plus.transpose()
+                       << "\ndiff -: " << minus.transpose()
+                       << "\ndiffnorm: " << diffnorm;
+      return false;
+    }
   }
   return true;
 }
diff --git a/include/ceres/rotation.h b/include/ceres/rotation.h
index c04278e..addd598 100644
--- a/include/ceres/rotation.h
+++ b/include/ceres/rotation.h
@@ -79,13 +79,29 @@
 template <typename T>
 MatrixAdapter<T, 3, 1> RowMajorAdapter3x3(T* pointer);
 
+// (Default) Ceres quaternion coefficients order (w, x, y, z).
+struct CeresQuaternionOrder {
+  static constexpr int kW = 0;
+  static constexpr int kX = 1;
+  static constexpr int kY = 2;
+  static constexpr int kZ = 3;
+};
+
+// Eigen quaternion coefficients order (x, y, z, w).
+struct EigenQuaternionOrder {
+  static constexpr int kW = 3;
+  static constexpr int kX = 0;
+  static constexpr int kY = 1;
+  static constexpr int kZ = 2;
+};
+
 // Convert a value in combined axis-angle representation to a quaternion.
 // The value angle_axis is a triple whose norm is an angle in radians,
 // and whose direction is aligned with the axis of rotation,
 // and quaternion is a 4-tuple that will contain the resulting quaternion.
 // The implementation may be used with auto-differentiation up to the first
 // derivative, higher derivatives may have unexpected results near the origin.
-template <typename T>
+template <typename Order = CeresQuaternionOrder, typename T>
 void AngleAxisToQuaternion(const T* angle_axis, T* quaternion);
 
 // Convert a quaternion to the equivalent combined axis-angle representation.
@@ -94,16 +110,19 @@
 // rotation in radians, and whose direction is the axis of rotation.
 // The implementation may be used with auto-differentiation up to the first
 // derivative, higher derivatives may have unexpected results near the origin.
-template <typename T>
+template <typename Order = CeresQuaternionOrder, typename T>
 void QuaternionToAngleAxis(const T* quaternion, T* angle_axis);
 
 // Conversions between 3x3 rotation matrix (in column major order) and
 // quaternion rotation representations. Templated for use with
 // autodifferentiation.
-template <typename T>
+template <typename Order = CeresQuaternionOrder, typename T>
 void RotationMatrixToQuaternion(const T* R, T* quaternion);
 
-template <typename T, int row_stride, int col_stride>
+template <typename Order = CeresQuaternionOrder,
+          typename T,
+          int row_stride,
+          int col_stride>
 void RotationMatrixToQuaternion(
     const MatrixAdapter<const T, row_stride, col_stride>& R, T* quaternion);
 
@@ -221,10 +240,13 @@
 // such that det(Q) = 1 and Q*Q' = I
 //
 // WARNING: The rotation matrix is ROW MAJOR
-template <typename T>
+template <typename Order = CeresQuaternionOrder, typename T>
 inline void QuaternionToScaledRotation(const T q[4], T R[3 * 3]);
 
-template <typename T, int row_stride, int col_stride>
+template <typename Order = CeresQuaternionOrder,
+          typename T,
+          int row_stride,
+          int col_stride>
 inline void QuaternionToScaledRotation(
     const T q[4], const MatrixAdapter<T, row_stride, col_stride>& R);
 
@@ -232,10 +254,13 @@
 // Frobenius norm, so that R * R' = I (and det(R) = 1).
 //
 // WARNING: The rotation matrix is ROW MAJOR
-template <typename T>
+template <typename Order = CeresQuaternionOrder, typename T>
 inline void QuaternionToRotation(const T q[4], T R[3 * 3]);
 
-template <typename T, int row_stride, int col_stride>
+template <typename Order = CeresQuaternionOrder,
+          typename T,
+          int row_stride,
+          int col_stride>
 inline void QuaternionToRotation(
     const T q[4], const MatrixAdapter<T, row_stride, col_stride>& R);
 
@@ -250,7 +275,7 @@
 //
 // Inplace rotation is not supported. pt and result must point to different
 // memory locations, otherwise the result will be undefined.
-template <typename T>
+template <typename Order = CeresQuaternionOrder, typename T>
 inline void UnitQuaternionRotatePoint(const T q[4], const T pt[3], T result[3]);
 
 // With this function you do not need to assume that q has unit norm.
@@ -258,7 +283,7 @@
 //
 // Inplace rotation is not supported. pt and result must point to different
 // memory locations, otherwise the result will be undefined.
-template <typename T>
+template <typename Order = CeresQuaternionOrder, typename T>
 inline void QuaternionRotatePoint(const T q[4], const T pt[3], T result[3]);
 
 // zw = z * w, where * is the Quaternion product between 4 vectors.
@@ -266,9 +291,15 @@
 // Inplace quaternion product is not supported. The resulting quaternion zw must
 // not share the memory with the input quaternion z and w, otherwise the result
 // will be undefined.
-template <typename T>
+template <typename Order = CeresQuaternionOrder, typename T>
 inline void QuaternionProduct(const T z[4], const T w[4], T zw[4]);
 
+// Compute the conjugate of the quaternion q and store it in z.
+//
+// Inplace conjugation is supported.
+template <typename Order = CeresQuaternionOrder, typename T>
+inline void QuaternionConjugate(const T q[4], T z[4]);
+
 // xy = x cross y;
 //
 // Inplace cross product is not supported. The resulting vector x_cross_y must
@@ -310,7 +341,7 @@
   return MatrixAdapter<T, 3, 1>(pointer);
 }
 
-template <typename T>
+template <typename Order, typename T>
 inline void AngleAxisToQuaternion(const T* angle_axis, T* quaternion) {
   using std::fpclassify;
   using std::hypot;
@@ -325,28 +356,28 @@
   if (fpclassify(theta) != FP_ZERO) {
     const T half_theta = theta * T(0.5);
     k = sin(half_theta) / theta;
-    quaternion[0] = cos(half_theta);
+    quaternion[Order::kW] = cos(half_theta);
   } else {
     // At the origin, sqrt() will produce NaN in the derivative since
     // the argument is zero.  By approximating with a Taylor series,
     // and truncating at one term, the value and first derivatives will be
     // computed correctly when Jets are used.
     k = T(0.5);
-    quaternion[0] = T(1.0);
+    quaternion[Order::kW] = T(1.0);
   }
 
-  quaternion[1] = a0 * k;
-  quaternion[2] = a1 * k;
-  quaternion[3] = a2 * k;
+  quaternion[Order::kX] = a0 * k;
+  quaternion[Order::kY] = a1 * k;
+  quaternion[Order::kZ] = a2 * k;
 }
 
-template <typename T>
+template <typename Order, typename T>
 inline void QuaternionToAngleAxis(const T* quaternion, T* angle_axis) {
   using std::fpclassify;
   using std::hypot;
-  const T& q1 = quaternion[1];
-  const T& q2 = quaternion[2];
-  const T& q3 = quaternion[3];
+  const T& q1 = quaternion[Order::kX];
+  const T& q2 = quaternion[Order::kY];
+  const T& q3 = quaternion[Order::kZ];
 
   T k;
   const T sin_theta = hypot(q1, q2, q3);
@@ -354,7 +385,7 @@
   // For quaternions representing non-zero rotation, the conversion
   // is numerically stable.
   if (fpclassify(sin_theta) != FP_ZERO) {
-    const T& cos_theta = quaternion[0];
+    const T& cos_theta = quaternion[Order::kW];
 
     // If cos_theta is negative, theta is greater than pi/2, which
     // means that angle for the angle_axis vector which is 2 * theta
@@ -385,32 +416,32 @@
   angle_axis[2] = q3 * k;
 }
 
-template <typename T>
+template <typename Order, typename T>
 void RotationMatrixToQuaternion(const T* R, T* quaternion) {
-  RotationMatrixToQuaternion(ColumnMajorAdapter3x3(R), quaternion);
+  RotationMatrixToQuaternion<Order>(ColumnMajorAdapter3x3(R), quaternion);
 }
 
 // This algorithm comes from "Quaternion Calculus and Fast Animation",
 // Ken Shoemake, 1987 SIGGRAPH course notes
-template <typename T, int row_stride, int col_stride>
+template <typename Order, typename T, int row_stride, int col_stride>
 void RotationMatrixToQuaternion(
     const MatrixAdapter<const T, row_stride, col_stride>& R, T* quaternion) {
   const T trace = R(0, 0) + R(1, 1) + R(2, 2);
   if (trace >= 0.0) {
     T t = sqrt(trace + T(1.0));
-    quaternion[0] = T(0.5) * t;
+    quaternion[Order::kW] = T(0.5) * t;
     t = T(0.5) / t;
-    quaternion[1] = (R(2, 1) - R(1, 2)) * t;
-    quaternion[2] = (R(0, 2) - R(2, 0)) * t;
-    quaternion[3] = (R(1, 0) - R(0, 1)) * t;
+    quaternion[Order::kX] = (R(2, 1) - R(1, 2)) * t;
+    quaternion[Order::kY] = (R(0, 2) - R(2, 0)) * t;
+    quaternion[Order::kZ] = (R(1, 0) - R(0, 1)) * t;
   } else {
-    int i = 0;
+    int i = Order::kW;
     if (R(1, 1) > R(0, 0)) {
-      i = 1;
+      i = Order::kX;
     }
 
     if (R(2, 2) > R(i, i)) {
-      i = 2;
+      i = Order::kY;
     }
 
     const int j = (i + 1) % 3;
@@ -418,7 +449,7 @@
     T t = sqrt(R(i, i) - R(j, j) - R(k, k) + T(1.0));
     quaternion[i + 1] = T(0.5) * t;
     t = T(0.5) / t;
-    quaternion[0] = (R(k, j) - R(j, k)) * t;
+    quaternion[Order::kW] = (R(k, j) - R(j, k)) * t;
     quaternion[j + 1] = (R(j, i) + R(i, j)) * t;
     quaternion[k + 1] = (R(k, i) + R(i, k)) * t;
   }
@@ -663,19 +694,19 @@
   R(2, 2) = c2 * c3;
 }
 
-template <typename T>
+template <typename Order, typename T>
 inline void QuaternionToScaledRotation(const T q[4], T R[3 * 3]) {
-  QuaternionToScaledRotation(q, RowMajorAdapter3x3(R));
+  QuaternionToScaledRotation<Order>(q, RowMajorAdapter3x3(R));
 }
 
-template <typename T, int row_stride, int col_stride>
+template <typename Order, typename T, int row_stride, int col_stride>
 inline void QuaternionToScaledRotation(
     const T q[4], const MatrixAdapter<T, row_stride, col_stride>& R) {
   // Make convenient names for elements of q.
-  T a = q[0];
-  T b = q[1];
-  T c = q[2];
-  T d = q[3];
+  T a = q[Order::kW];
+  T b = q[Order::kX];
+  T c = q[Order::kY];
+  T d = q[Order::kZ];
   // This is not to eliminate common sub-expression, but to
   // make the lines shorter so that they fit in 80 columns!
   T aa = a * a;
@@ -696,17 +727,18 @@
   // clang-format on
 }
 
-template <typename T>
+template <typename Order, typename T>
 inline void QuaternionToRotation(const T q[4], T R[3 * 3]) {
-  QuaternionToRotation(q, RowMajorAdapter3x3(R));
+  QuaternionToRotation<Order>(q, RowMajorAdapter3x3(R));
 }
 
-template <typename T, int row_stride, int col_stride>
+template <typename Order, typename T, int row_stride, int col_stride>
 inline void QuaternionToRotation(
     const T q[4], const MatrixAdapter<T, row_stride, col_stride>& R) {
-  QuaternionToScaledRotation(q, R);
+  QuaternionToScaledRotation<Order>(q, R);
 
-  T normalizer = q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3];
+  T normalizer = q[Order::kW] * q[Order::kW] + q[Order::kX] * q[Order::kX] +
+                 q[Order::kY] * q[Order::kY] + q[Order::kZ] * q[Order::kZ];
   normalizer = T(1) / normalizer;
 
   for (int i = 0; i < 3; ++i) {
@@ -716,60 +748,69 @@
   }
 }
 
-template <typename T>
+template <typename Order, typename T>
 inline void UnitQuaternionRotatePoint(const T q[4],
                                       const T pt[3],
                                       T result[3]) {
   DCHECK_NE(pt, result) << "Inplace rotation is not supported.";
 
   // clang-format off
-  T uv0 = q[2] * pt[2] - q[3] * pt[1];
-  T uv1 = q[3] * pt[0] - q[1] * pt[2];
-  T uv2 = q[1] * pt[1] - q[2] * pt[0];
+  T uv0 = q[Order::kY] * pt[2] - q[Order::kZ] * pt[1];
+  T uv1 = q[Order::kZ] * pt[0] - q[Order::kX] * pt[2];
+  T uv2 = q[Order::kX] * pt[1] - q[Order::kY] * pt[0];
   uv0 += uv0;
   uv1 += uv1;
   uv2 += uv2;
-  result[0] = pt[0] + q[0] * uv0;
-  result[1] = pt[1] + q[0] * uv1;
-  result[2] = pt[2] + q[0] * uv2;
-  result[0] += q[2] * uv2 - q[3] * uv1;
-  result[1] += q[3] * uv0 - q[1] * uv2;
-  result[2] += q[1] * uv1 - q[2] * uv0;
+  result[0] = pt[0] + q[Order::kW] * uv0;
+  result[1] = pt[1] + q[Order::kW] * uv1;
+  result[2] = pt[2] + q[Order::kW] * uv2;
+  result[0] += q[Order::kY] * uv2 - q[Order::kZ] * uv1;
+  result[1] += q[Order::kZ] * uv0 - q[Order::kX] * uv2;
+  result[2] += q[Order::kX] * uv1 - q[Order::kY] * uv0;
   // clang-format on
 }
 
-template <typename T>
+template <typename Order, typename T>
 inline void QuaternionRotatePoint(const T q[4], const T pt[3], T result[3]) {
   DCHECK_NE(pt, result) << "Inplace rotation is not supported.";
 
   // 'scale' is 1 / norm(q).
   const T scale =
-      T(1) / sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
+      T(1) / sqrt(q[Order::kW] * q[Order::kW] + q[Order::kX] * q[Order::kX] +
+                  q[Order::kY] * q[Order::kY] + q[Order::kZ] * q[Order::kZ]);
 
   // Make unit-norm version of q.
   const T unit[4] = {
-      scale * q[0],
-      scale * q[1],
-      scale * q[2],
-      scale * q[3],
+      scale * q[Order::kW],
+      scale * q[Order::kX],
+      scale * q[Order::kY],
+      scale * q[Order::kZ],
   };
 
-  UnitQuaternionRotatePoint(unit, pt, result);
+  UnitQuaternionRotatePoint<Order>(unit, pt, result);
 }
 
-template <typename T>
+template <typename Order, typename T>
 inline void QuaternionProduct(const T z[4], const T w[4], T zw[4]) {
   DCHECK_NE(z, zw) << "Inplace quaternion product is not supported.";
   DCHECK_NE(w, zw) << "Inplace quaternion product is not supported.";
 
   // clang-format off
-  zw[0] = z[0] * w[0] - z[1] * w[1] - z[2] * w[2] - z[3] * w[3];
-  zw[1] = z[0] * w[1] + z[1] * w[0] + z[2] * w[3] - z[3] * w[2];
-  zw[2] = z[0] * w[2] - z[1] * w[3] + z[2] * w[0] + z[3] * w[1];
-  zw[3] = z[0] * w[3] + z[1] * w[2] - z[2] * w[1] + z[3] * w[0];
+  zw[Order::kW] = z[Order::kW] * w[Order::kW] - z[Order::kX] * w[Order::kX] - z[Order::kY] * w[Order::kY] - z[Order::kZ] * w[Order::kZ];
+  zw[Order::kX] = z[Order::kW] * w[Order::kX] + z[Order::kX] * w[Order::kW] + z[Order::kY] * w[Order::kZ] - z[Order::kZ] * w[Order::kY];
+  zw[Order::kY] = z[Order::kW] * w[Order::kY] - z[Order::kX] * w[Order::kZ] + z[Order::kY] * w[Order::kW] + z[Order::kZ] * w[Order::kX];
+  zw[Order::kZ] = z[Order::kW] * w[Order::kZ] + z[Order::kX] * w[Order::kY] - z[Order::kY] * w[Order::kX] + z[Order::kZ] * w[Order::kW];
   // clang-format on
 }
 
+template <typename Order, typename T>
+inline void QuaternionConjugate(const T q[4], T w[4]) {
+  w[Order::kW] = q[Order::kW];
+  w[Order::kX] = -q[Order::kX];
+  w[Order::kY] = -q[Order::kY];
+  w[Order::kZ] = -q[Order::kZ];
+}
+
 // xy = x cross y;
 template <typename T>
 inline void CrossProduct(const T x[3], const T y[3], T x_cross_y[3]) {
diff --git a/internal/ceres/manifold.cc b/internal/ceres/manifold.cc
index f2cd378..9830803 100644
--- a/internal/ceres/manifold.cc
+++ b/internal/ceres/manifold.cc
@@ -5,59 +5,20 @@
 
 #include "absl/log/check.h"
 #include "ceres/internal/eigen.h"
+#include "ceres/rotation.h"
 
 namespace ceres {
 namespace {
 
-struct CeresQuaternionOrder {
-  static constexpr int kW = 0;
-  static constexpr int kX = 1;
-  static constexpr int kY = 2;
-  static constexpr int kZ = 3;
-};
-
-struct EigenQuaternionOrder {
-  static constexpr int kW = 3;
-  static constexpr int kX = 0;
-  static constexpr int kY = 1;
-  static constexpr int kZ = 2;
-};
-
 template <typename Order>
 inline void QuaternionPlusImpl(const double* x,
                                const double* delta,
                                double* x_plus_delta) {
   // x_plus_delta = QuaternionProduct(q_delta, x), where q_delta is the
   // quaternion constructed from delta.
-  const double norm_delta = std::hypot(delta[0], delta[1], delta[2]);
-
-  if (std::fpclassify(norm_delta) == FP_ZERO) {
-    // No change in rotation: return the quaternion as is.
-    std::copy_n(x, 4, x_plus_delta);
-    return;
-  }
-
-  const double half_norm_delta = norm_delta / 2;
-  const double sin_half_delta_by_delta =
-      (std::sin(half_norm_delta) / norm_delta);
   double q_delta[4];
-  q_delta[Order::kW] = std::cos(half_norm_delta);
-  q_delta[Order::kX] = sin_half_delta_by_delta * delta[0];
-  q_delta[Order::kY] = sin_half_delta_by_delta * delta[1];
-  q_delta[Order::kZ] = sin_half_delta_by_delta * delta[2];
-
-  x_plus_delta[Order::kW] =
-      q_delta[Order::kW] * x[Order::kW] - q_delta[Order::kX] * x[Order::kX] -
-      q_delta[Order::kY] * x[Order::kY] - q_delta[Order::kZ] * x[Order::kZ];
-  x_plus_delta[Order::kX] =
-      q_delta[Order::kW] * x[Order::kX] + q_delta[Order::kX] * x[Order::kW] +
-      q_delta[Order::kY] * x[Order::kZ] - q_delta[Order::kZ] * x[Order::kY];
-  x_plus_delta[Order::kY] =
-      q_delta[Order::kW] * x[Order::kY] - q_delta[Order::kX] * x[Order::kZ] +
-      q_delta[Order::kY] * x[Order::kW] + q_delta[Order::kZ] * x[Order::kX];
-  x_plus_delta[Order::kZ] =
-      q_delta[Order::kW] * x[Order::kZ] + q_delta[Order::kX] * x[Order::kY] -
-      q_delta[Order::kY] * x[Order::kX] + q_delta[Order::kZ] * x[Order::kW];
+  AngleAxisToQuaternion<Order>(delta, q_delta);
+  QuaternionProduct<Order>(q_delta, x, x_plus_delta);
 }
 
 template <typename Order>
@@ -86,31 +47,12 @@
                                 double* y_minus_x) {
   // ambient_y_minus_x = QuaternionProduct(y, -x) where -x is the conjugate of
   // x.
-  double ambient_y_minus_x[4];
-  ambient_y_minus_x[Order::kW] =
-      y[Order::kW] * x[Order::kW] + y[Order::kX] * x[Order::kX] +
-      y[Order::kY] * x[Order::kY] + y[Order::kZ] * x[Order::kZ];
-  ambient_y_minus_x[Order::kX] =
-      -y[Order::kW] * x[Order::kX] + y[Order::kX] * x[Order::kW] -
-      y[Order::kY] * x[Order::kZ] + y[Order::kZ] * x[Order::kY];
-  ambient_y_minus_x[Order::kY] =
-      -y[Order::kW] * x[Order::kY] + y[Order::kX] * x[Order::kZ] +
-      y[Order::kY] * x[Order::kW] - y[Order::kZ] * x[Order::kX];
-  ambient_y_minus_x[Order::kZ] =
-      -y[Order::kW] * x[Order::kZ] - y[Order::kX] * x[Order::kY] +
-      y[Order::kY] * x[Order::kX] + y[Order::kZ] * x[Order::kW];
+  double x_conj[4];
+  QuaternionConjugate<Order>(x, x_conj);
 
-  const double u_norm = std::hypot(ambient_y_minus_x[Order::kX],
-                                   ambient_y_minus_x[Order::kY],
-                                   ambient_y_minus_x[Order::kZ]);
-  if (std::fpclassify(u_norm) != FP_ZERO) {
-    const double theta = 2 * std::atan2(u_norm, ambient_y_minus_x[Order::kW]);
-    y_minus_x[0] = theta * ambient_y_minus_x[Order::kX] / u_norm;
-    y_minus_x[1] = theta * ambient_y_minus_x[Order::kY] / u_norm;
-    y_minus_x[2] = theta * ambient_y_minus_x[Order::kZ] / u_norm;
-  } else {
-    std::fill_n(y_minus_x, 3, 0.0);
-  }
+  double ambient_y_minus_x[4];
+  QuaternionProduct<Order>(y, x_conj, ambient_y_minus_x);
+  QuaternionToAngleAxis<Order>(ambient_y_minus_x, y_minus_x);
 }
 
 template <typename Order>
diff --git a/internal/ceres/rotation_test.cc b/internal/ceres/rotation_test.cc
index 85c8b78..28b34ef 100644
--- a/internal/ceres/rotation_test.cc
+++ b/internal/ceres/rotation_test.cc
@@ -36,6 +36,7 @@
 #include <limits>
 #include <random>
 #include <string>
+#include <type_traits>
 #include <utility>
 
 #include "absl/log/log.h"
@@ -201,111 +202,149 @@
   return true;
 }
 
+template <typename Order, typename T>
+constexpr auto MakeQuaternion(T w, T x, T y, T z)
+    -> std::enable_if_t<std::is_same_v<Order, CeresQuaternionOrder>,
+                        std::array<T, 4>> {
+  return {w, x, y, z};
+}
+
+template <typename Order, typename T>
+constexpr auto MakeQuaternion(T w, T x, T y, T z)
+    -> std::enable_if_t<std::is_same_v<Order, EigenQuaternionOrder>,
+                        std::array<T, 4>> {
+  return {x, y, z, w};
+}
+
+template <typename T>
+class QuaternionTest : public testing::Test {};
+
+using QuaternionOrderTypes =
+    testing::Types<ceres::CeresQuaternionOrder, ceres::EigenQuaternionOrder>;
+
+TYPED_TEST_SUITE(QuaternionTest, QuaternionOrderTypes);
+
 // Transforms a zero axis/angle to a quaternion.
-TEST(Rotation, ZeroAngleAxisToQuaternion) {
+TYPED_TEST(QuaternionTest, ZeroAngleAxisToQuaternion) {
+  using Order = TypeParam;
   double axis_angle[3] = {0, 0, 0};
   double quaternion[4];
-  double expected[4] = {1, 0, 0, 0};
-  AngleAxisToQuaternion(axis_angle, quaternion);
+  const std::array<double, 4> expected =
+      MakeQuaternion<Order>(1.0, 0.0, 0.0, 0.0);
+  AngleAxisToQuaternion<Order>(axis_angle, quaternion);
   EXPECT_THAT(quaternion, IsNormalizedQuaternion());
   EXPECT_THAT(quaternion, IsNearQuaternion(expected));
 }
 
 // Test that exact conversion works for small angles.
-TEST(Rotation, SmallAngleAxisToQuaternion) {
+TYPED_TEST(QuaternionTest, SmallAngleAxisToQuaternion) {
+  using Order = TypeParam;
   // Small, finite value to test.
   double theta = 1.0e-2;
   double axis_angle[3] = {theta, 0, 0};
   double quaternion[4];
-  double expected[4] = {cos(theta / 2), sin(theta / 2.0), 0, 0};
-  AngleAxisToQuaternion(axis_angle, quaternion);
+  const std::array<double, 4> expected =
+      MakeQuaternion<Order>(cos(theta / 2.0), sin(theta / 2.0), 0.0, 0.0);
+  AngleAxisToQuaternion<Order>(axis_angle, quaternion);
   EXPECT_THAT(quaternion, IsNormalizedQuaternion());
   EXPECT_THAT(quaternion, IsNearQuaternion(expected));
 }
 
 // Test that approximate conversion works for very small angles.
-TEST(Rotation, TinyAngleAxisToQuaternion) {
+TYPED_TEST(QuaternionTest, TinyAngleAxisToQuaternion) {
+  using Order = TypeParam;
   // Very small value that could potentially cause underflow.
   double theta = pow(std::numeric_limits<double>::min(), 0.75);
   double axis_angle[3] = {theta, 0, 0};
   double quaternion[4];
-  double expected[4] = {cos(theta / 2), sin(theta / 2.0), 0, 0};
-  AngleAxisToQuaternion(axis_angle, quaternion);
+  const std::array<double, 4> expected =
+      MakeQuaternion<Order>(cos(theta / 2.0), sin(theta / 2.0), 0.0, 0.0);
+  AngleAxisToQuaternion<Order>(axis_angle, quaternion);
   EXPECT_THAT(quaternion, IsNormalizedQuaternion());
   EXPECT_THAT(quaternion, IsNearQuaternion(expected));
 }
 
 // Transforms a rotation by pi/2 around X to a quaternion.
-TEST(Rotation, XRotationToQuaternion) {
+TYPED_TEST(QuaternionTest, XRotationToQuaternion) {
+  using Order = TypeParam;
   double axis_angle[3] = {kPi / 2, 0, 0};
   double quaternion[4];
-  double expected[4] = {kHalfSqrt2, kHalfSqrt2, 0, 0};
-  AngleAxisToQuaternion(axis_angle, quaternion);
+  const std::array<double, 4> expected =
+      MakeQuaternion<Order>(kHalfSqrt2, kHalfSqrt2, 0.0, 0.0);
+  AngleAxisToQuaternion<Order>(axis_angle, quaternion);
   EXPECT_THAT(quaternion, IsNormalizedQuaternion());
   EXPECT_THAT(quaternion, IsNearQuaternion(expected));
 }
 
 // Transforms a unit quaternion to an axis angle.
-TEST(Rotation, UnitQuaternionToAngleAxis) {
-  double quaternion[4] = {1, 0, 0, 0};
+TYPED_TEST(QuaternionTest, UnitQuaternionToAngleAxis) {
+  using Order = TypeParam;
+  const std::array<double, 4> quaternion =
+      MakeQuaternion<Order>(1.0, 0.0, 0.0, 0.0);
   double axis_angle[3];
   double expected[3] = {0, 0, 0};
-  QuaternionToAngleAxis(quaternion, axis_angle);
+  QuaternionToAngleAxis<Order>(quaternion.data(), axis_angle);
   EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
 }
 
 // Transforms a quaternion that rotates by pi about the Y axis to an axis angle.
-TEST(Rotation, YRotationQuaternionToAngleAxis) {
-  double quaternion[4] = {0, 0, 1, 0};
+TYPED_TEST(QuaternionTest, YRotationQuaternionToAngleAxis) {
+  using Order = TypeParam;
+  const std::array<double, 4> quaternion =
+      MakeQuaternion<Order>(0.0, 0.0, 1.0, 0.0);
   double axis_angle[3];
   double expected[3] = {0, kPi, 0};
-  QuaternionToAngleAxis(quaternion, axis_angle);
+  QuaternionToAngleAxis<Order>(quaternion.data(), axis_angle);
   EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
 }
 
 // Transforms a quaternion that rotates by pi/3 about the Z axis to an axis
 // angle.
-TEST(Rotation, ZRotationQuaternionToAngleAxis) {
-  double quaternion[4] = {sqrt(3) / 2, 0, 0, 0.5};
+TYPED_TEST(QuaternionTest, ZRotationQuaternionToAngleAxis) {
+  using Order = TypeParam;
+  const std::array<double, 4> quaternion =
+      MakeQuaternion<Order>(sqrt(3) / 2, 0.0, 0.0, 0.5);
   double axis_angle[3];
   double expected[3] = {0, 0, kPi / 3};
-  QuaternionToAngleAxis(quaternion, axis_angle);
+  QuaternionToAngleAxis<Order>(quaternion.data(), axis_angle);
   EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
 }
 
 // Test that exact conversion works for small angles.
-TEST(Rotation, SmallQuaternionToAngleAxis) {
+TYPED_TEST(QuaternionTest, SmallQuaternionToAngleAxis) {
+  using Order = TypeParam;
   // Small, finite value to test.
   double theta = 1.0e-2;
-  double quaternion[4] = {cos(theta / 2), sin(theta / 2.0), 0, 0};
+  const std::array<double, 4> quaternion =
+      MakeQuaternion<Order>(cos(theta / 2.0), sin(theta / 2.0), 0.0, 0.0);
   double axis_angle[3];
   double expected[3] = {theta, 0, 0};
-  QuaternionToAngleAxis(quaternion, axis_angle);
+  QuaternionToAngleAxis<Order>(quaternion.data(), axis_angle);
   EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
 }
 
 // Test that approximate conversion works for very small angles.
-TEST(Rotation, TinyQuaternionToAngleAxis) {
+TYPED_TEST(QuaternionTest, TinyQuaternionToAngleAxis) {
+  using Order = TypeParam;
   // Very small value that could potentially cause underflow.
   double theta = pow(std::numeric_limits<double>::min(), 0.75);
-  double quaternion[4] = {cos(theta / 2), sin(theta / 2.0), 0, 0};
+  const std::array<double, 4> quaternion =
+      MakeQuaternion<Order>(cos(theta / 2.0), sin(theta / 2.0), 0.0, 0.0);
   double axis_angle[3];
   double expected[3] = {theta, 0, 0};
-  QuaternionToAngleAxis(quaternion, axis_angle);
+  QuaternionToAngleAxis<Order>(quaternion.data(), axis_angle);
   EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
 }
 
-TEST(Rotation, QuaternionToAngleAxisAngleIsLessThanPi) {
-  double quaternion[4];
+TYPED_TEST(QuaternionTest, QuaternionToAngleAxisAngleIsLessThanPi) {
+  using Order = TypeParam;
   double angle_axis[3];
 
   const double half_theta = 0.75 * kPi;
 
-  quaternion[0] = cos(half_theta);
-  quaternion[1] = 1.0 * sin(half_theta);
-  quaternion[2] = 0.0;
-  quaternion[3] = 0.0;
-  QuaternionToAngleAxis(quaternion, angle_axis);
+  const std::array<double, 4> quaternion =
+      MakeQuaternion<Order>(cos(half_theta), 1.0 * sin(half_theta), 0.0, 0.0);
+  QuaternionToAngleAxis<Order>(quaternion.data(), angle_axis);
   const double angle = std::hypot(angle_axis[0], angle_axis[1], angle_axis[2]);
   EXPECT_LE(angle, kPi);
 }
@@ -314,7 +353,8 @@
 
 // Takes a bunch of random axis/angle values, converts them to quaternions,
 // and back again.
-TEST(Rotation, AngleAxisToQuaterionAndBack) {
+TYPED_TEST(QuaternionTest, AngleAxisToQuaterionAndBack) {
+  using Order = TypeParam;
   std::mt19937 prng;
   std::uniform_real_distribution<double> uniform_distribution{-1.0, 1.0};
   for (int i = 0; i < kNumTrials; i++) {
@@ -340,16 +380,17 @@
     // We use ASSERTs here because if there's one failure, there are
     // probably many and spewing a million failures doesn't make anyone's
     // day.
-    AngleAxisToQuaternion(axis_angle, quaternion);
+    AngleAxisToQuaternion<Order>(axis_angle, quaternion);
     ASSERT_THAT(quaternion, IsNormalizedQuaternion());
-    QuaternionToAngleAxis(quaternion, round_trip);
+    QuaternionToAngleAxis<Order>(quaternion, round_trip);
     ASSERT_THAT(round_trip, IsNearAngleAxis(axis_angle));
   }
 }
 
 // Takes a bunch of random quaternions, converts them to axis/angle,
 // and back again.
-TEST(Rotation, QuaterionToAngleAxisAndBack) {
+TYPED_TEST(QuaternionTest, QuaterionToAngleAxisAndBack) {
+  using Order = TypeParam;
   std::mt19937 prng;
   std::uniform_real_distribution<double> uniform_distribution{-1.0, 1.0};
   for (int i = 0; i < kNumTrials; i++) {
@@ -368,8 +409,8 @@
 
     double axis_angle[3];
     double round_trip[4];
-    QuaternionToAngleAxis(quaternion, axis_angle);
-    AngleAxisToQuaternion(axis_angle, round_trip);
+    QuaternionToAngleAxis<Order>(quaternion, axis_angle);
+    AngleAxisToQuaternion<Order>(axis_angle, round_trip);
     ASSERT_THAT(round_trip, IsNormalizedQuaternion());
     ASSERT_THAT(round_trip, IsNearQuaternion(quaternion));
   }
@@ -1004,26 +1045,27 @@
     static_cast<int>(1 + log(std::numeric_limits<double>::min()) / log(10.0));
 
 // Test that exact conversion works for small angles when jets are used.
-TEST(Rotation, SmallAngleAxisToQuaternionForJets) {
+TYPED_TEST(QuaternionTest, SmallAngleAxisToQuaternionForJets) {
+  using Order = TypeParam;
   // Examine small x rotations that are still large enough
   // to be well within the range represented by doubles.
   for (int i = -2; i >= kSmallTinyCutoff; i--) {
     double theta = pow(10.0, i);
     J3 axis_angle[3] = {J3(theta, 0), J3(0, 1), J3(0, 2)};
     J3 quaternion[4];
-    J3 expected[4] = {
-        MakeJ3(cos(theta / 2), -sin(theta / 2) / 2, 0, 0),
-        MakeJ3(sin(theta / 2), cos(theta / 2) / 2, 0, 0),
-        MakeJ3(0, 0, sin(theta / 2) / theta, 0),
-        MakeJ3(0, 0, 0, sin(theta / 2) / theta),
-    };
-    AngleAxisToQuaternion(axis_angle, quaternion);
+    const std::array<J3, 4> expected =
+        MakeQuaternion<Order>(MakeJ3(cos(theta / 2), -sin(theta / 2) / 2, 0, 0),
+                              MakeJ3(sin(theta / 2), cos(theta / 2) / 2, 0, 0),
+                              MakeJ3(0, 0, sin(theta / 2) / theta, 0),
+                              MakeJ3(0, 0, 0, sin(theta / 2) / theta));
+    AngleAxisToQuaternion<Order>(axis_angle, quaternion);
     EXPECT_THAT(quaternion, testing::Pointwise(JetClose(kTolerance), expected));
   }
 }
 
 // Test that conversion works for very small angles when jets are used.
-TEST(Rotation, TinyAngleAxisToQuaternionForJets) {
+TYPED_TEST(QuaternionTest, TinyAngleAxisToQuaternionForJets) {
+  using Order = TypeParam;
   // Examine tiny x rotations that extend all the way to where
   // underflow occurs.
   for (int i = kSmallTinyCutoff; i >= kTinyZeroLimit; i--) {
@@ -1033,40 +1075,41 @@
     // To avoid loss of precision in the test itself,
     // a finite expansion is used here, which will
     // be exact up to machine precision for the test values used.
-    J3 expected[4] = {
-        MakeJ3(1.0, 0, 0, 0),
-        MakeJ3(0, 0.5, 0, 0),
-        MakeJ3(0, 0, 0.5, 0),
-        MakeJ3(0, 0, 0, 0.5),
-    };
-    AngleAxisToQuaternion(axis_angle, quaternion);
+    const std::array<J3, 4> expected =
+        MakeQuaternion<Order>(MakeJ3(1.0, 0, 0, 0),
+                              MakeJ3(0, 0.5, 0, 0),
+                              MakeJ3(0, 0, 0.5, 0),
+                              MakeJ3(0, 0, 0, 0.5));
+    AngleAxisToQuaternion<Order>(axis_angle, quaternion);
     EXPECT_THAT(quaternion, testing::Pointwise(JetClose(kTolerance), expected));
   }
 }
 
 // Test that derivatives are correct for zero rotation.
-TEST(Rotation, ZeroAngleAxisToQuaternionForJets) {
+TYPED_TEST(QuaternionTest, ZeroAngleAxisToQuaternionForJets) {
+  using Order = TypeParam;
   J3 axis_angle[3] = {J3(0, 0), J3(0, 1), J3(0, 2)};
   J3 quaternion[4];
-  J3 expected[4] = {
-      MakeJ3(1.0, 0, 0, 0),
-      MakeJ3(0, 0.5, 0, 0),
-      MakeJ3(0, 0, 0.5, 0),
-      MakeJ3(0, 0, 0, 0.5),
-  };
-  AngleAxisToQuaternion(axis_angle, quaternion);
+  const std::array<J3, 4> expected =
+      MakeQuaternion<Order>(MakeJ3(1.0, 0, 0, 0),
+                            MakeJ3(0, 0.5, 0, 0),
+                            MakeJ3(0, 0, 0.5, 0),
+                            MakeJ3(0, 0, 0, 0.5));
+  AngleAxisToQuaternion<Order>(axis_angle, quaternion);
   EXPECT_THAT(quaternion, testing::Pointwise(JetClose(kTolerance), expected));
 }
 
 // Test that exact conversion works for small angles.
-TEST(Rotation, SmallQuaternionToAngleAxisForJets) {
+TYPED_TEST(QuaternionTest, SmallQuaternionToAngleAxisForJets) {
+  using Order = TypeParam;
   // Examine small x rotations that are still large enough
   // to be well within the range represented by doubles.
   for (int i = -2; i >= kSmallTinyCutoff; i--) {
     double theta = pow(10.0, i);
     double s = sin(theta);
     double c = cos(theta);
-    J4 quaternion[4] = {J4(c, 0), J4(s, 1), J4(0, 2), J4(0, 3)};
+    const std::array<J4, 4> quaternion =
+        MakeQuaternion<Order>(J4(c, 0), J4(s, 1), J4(0, 2), J4(0, 3));
     J4 axis_angle[3];
     // clang-format off
     J4 expected[3] = {
@@ -1075,20 +1118,22 @@
         MakeJ4(0,        0,   0,    0,         2*theta/s),
     };
     // clang-format on
-    QuaternionToAngleAxis(quaternion, axis_angle);
+    QuaternionToAngleAxis<Order>(quaternion.data(), axis_angle);
     EXPECT_THAT(axis_angle, testing::Pointwise(JetClose(kTolerance), expected));
   }
 }
 
 // Test that conversion works for very small angles.
-TEST(Rotation, TinyQuaternionToAngleAxisForJets) {
+TYPED_TEST(QuaternionTest, TinyQuaternionToAngleAxisForJets) {
+  using Order = TypeParam;
   // Examine tiny x rotations that extend all the way to where
   // underflow occurs.
   for (int i = kSmallTinyCutoff; i >= kTinyZeroLimit; i--) {
     double theta = pow(10.0, i);
     double s = sin(theta);
     double c = cos(theta);
-    J4 quaternion[4] = {J4(c, 0), J4(s, 1), J4(0, 2), J4(0, 3)};
+    const std::array<J4, 4> quaternion =
+        MakeQuaternion<Order>(J4(c, 0), J4(s, 1), J4(0, 2), J4(0, 3));
     J4 axis_angle[3];
     // To avoid loss of precision in the test itself,
     // a finite expansion is used here, which will
@@ -1100,21 +1145,23 @@
         MakeJ4(0,        0,   0,   0,   2.0),
     };
     // clang-format on
-    QuaternionToAngleAxis(quaternion, axis_angle);
+    QuaternionToAngleAxis<Order>(quaternion.data(), axis_angle);
     EXPECT_THAT(axis_angle, testing::Pointwise(JetClose(kTolerance), expected));
   }
 }
 
 // Test that conversion works for no rotation.
-TEST(Rotation, ZeroQuaternionToAngleAxisForJets) {
-  J4 quaternion[4] = {J4(1, 0), J4(0, 1), J4(0, 2), J4(0, 3)};
+TYPED_TEST(QuaternionTest, ZeroQuaternionToAngleAxisForJets) {
+  using Order = TypeParam;
+  const std::array<J4, 4> quaternion =
+      MakeQuaternion<Order>(J4(1, 0), J4(0, 1), J4(0, 2), J4(0, 3));
   J4 axis_angle[3];
   J4 expected[3] = {
       MakeJ4(0, 0, 2.0, 0, 0),
       MakeJ4(0, 0, 0, 2.0, 0),
       MakeJ4(0, 0, 0, 0, 2.0),
   };
-  QuaternionToAngleAxis(quaternion, axis_angle);
+  QuaternionToAngleAxis<Order>(quaternion.data(), axis_angle);
   EXPECT_THAT(axis_angle, testing::Pointwise(JetClose(kTolerance), expected));
 }
 
@@ -1619,14 +1666,13 @@
   }
 }
 
-TEST(Quaternion, RotatePointGivesSameAnswerAsRotationByMatrixCanned) {
+TYPED_TEST(QuaternionTest, RotatePointGivesSameAnswerAsRotationByMatrixCanned) {
+  using Order = TypeParam;
   // Canned data generated in octave.
-  double const q[4] = {
-      +0.1956830471754074,
-      -0.0150618562474847,
-      +0.7634572982788086,
-      -0.3019454777240753,
-  };
+  const std::array<double, 4> q = MakeQuaternion<Order>(+0.1956830471754074,
+                                                        -0.0150618562474847,
+                                                        +0.7634572982788086,
+                                                        -0.3019454777240753);
   double const Q[3][3] = {
       // Scaled rotation matrix.
       {-0.6355194033477252, +0.0951730541682254, +0.3078870197911186},
@@ -1642,22 +1688,21 @@
 
   // Compute R from q and compare to known answer.
   double Rq[3][3];
-  QuaternionToScaledRotation<double>(q, Rq[0]);
+  QuaternionToScaledRotation<Order>(q.data(), Rq[0]);
   ExpectArraysClose(9, Q[0], Rq[0], kTolerance);
 
   // Now do the same but compute R with normalization.
-  QuaternionToRotation<double>(q, Rq[0]);
+  QuaternionToRotation<Order>(q.data(), Rq[0]);
   ExpectArraysClose(9, R[0], Rq[0], kTolerance);
 }
 
-TEST(Quaternion, RotatePointGivesSameAnswerAsRotationByMatrix) {
+TYPED_TEST(QuaternionTest, RotatePointGivesSameAnswerAsRotationByMatrix) {
+  using Order = TypeParam;
   // Rotation defined by a unit quaternion.
-  double const q[4] = {
-      +0.2318160216097109,
-      -0.0178430356832060,
-      +0.9044300776717159,
-      -0.3576998641394597,
-  };
+  const std::array<double, 4> q = MakeQuaternion<Order>(+0.2318160216097109,
+                                                        -0.0178430356832060,
+                                                        +0.9044300776717159,
+                                                        -0.3576998641394597);
   double const p[3] = {
       +0.11,
       -13.15,
@@ -1665,10 +1710,10 @@
   };
 
   double R[3 * 3];
-  QuaternionToRotation(q, R);
+  QuaternionToRotation<Order>(q.data(), R);
 
   double result1[3];
-  UnitQuaternionRotatePoint(q, p, result1);
+  UnitQuaternionRotatePoint<Order>(q.data(), p, result1);
 
   double result2[3];
   VectorRef(result2, 3) = ConstMatrixRef(R, 3, 3) * ConstVectorRef(p, 3);
@@ -1676,7 +1721,8 @@
 }
 
 // Verify that (a * b) * c == a * (b * c).
-TEST(Quaternion, MultiplicationIsAssociative) {
+TYPED_TEST(QuaternionTest, MultiplicationIsAssociative) {
+  using Order = TypeParam;
   std::mt19937 prng;
   std::uniform_real_distribution<double> uniform_distribution{-1.0, 1.0};
   double a[4];
@@ -1690,18 +1736,38 @@
 
   double ab[4];
   double ab_c[4];
-  QuaternionProduct(a, b, ab);
-  QuaternionProduct(ab, c, ab_c);
+  QuaternionProduct<Order>(a, b, ab);
+  QuaternionProduct<Order>(ab, c, ab_c);
 
   double bc[4];
   double a_bc[4];
-  QuaternionProduct(b, c, bc);
-  QuaternionProduct(a, bc, a_bc);
+  QuaternionProduct<Order>(b, c, bc);
+  QuaternionProduct<Order>(a, bc, a_bc);
 
-  ASSERT_NEAR(ab_c[0], a_bc[0], kTolerance);
-  ASSERT_NEAR(ab_c[1], a_bc[1], kTolerance);
-  ASSERT_NEAR(ab_c[2], a_bc[2], kTolerance);
-  ASSERT_NEAR(ab_c[3], a_bc[3], kTolerance);
+  ASSERT_NEAR(ab_c[Order::kW], a_bc[Order::kW], kTolerance);
+  ASSERT_NEAR(ab_c[Order::kX], a_bc[Order::kX], kTolerance);
+  ASSERT_NEAR(ab_c[Order::kY], a_bc[Order::kY], kTolerance);
+  ASSERT_NEAR(ab_c[Order::kZ], a_bc[Order::kZ], kTolerance);
+}
+
+TYPED_TEST(QuaternionTest, UnitConjugationIdentity) {
+  using Order = TypeParam;
+  std::mt19937 prng;
+  std::uniform_real_distribution<double> uniform_distribution{-1.0, 1.0};
+  double a[4];
+  for (int i = 0; i < 4; ++i) {
+    a[i] = uniform_distribution(prng);
+  }
+  Eigen::Map<Eigen::Vector4d>{a}.normalize();
+  double b[4];
+  QuaternionConjugate<Order>(a, b);
+  double c[4];
+  QuaternionProduct<Order>(a, b, c);
+
+  EXPECT_NEAR(c[Order::kW], 1, kTolerance);
+  EXPECT_NEAR(c[Order::kX], 0, kTolerance);
+  EXPECT_NEAR(c[Order::kY], 0, kTolerance);
+  EXPECT_NEAR(c[Order::kZ], 0, kTolerance);
 }
 
 TEST(AngleAxis, RotatePointGivesSameAnswerAsRotationMatrix) {
@@ -1753,13 +1819,16 @@
   }
 }
 
-TEST(Quaternion, UnitQuaternion) {
+TYPED_TEST(QuaternionTest, UnitQuaternion) {
+  using Order = TypeParam;
   using Jet = ceres::Jet<double, 4>;
-  std::array<Jet, 4> quaternion = {
-      Jet(1.0, 0), Jet(0.0, 1), Jet(0.0, 2), Jet(0.0, 3)};
+  const std::array<Jet, 4> quaternion =
+      MakeQuaternion<Order>(Jet(1.0, 0), Jet(0.0, 1), Jet(0.0, 2), Jet(0.0, 3));
+
   std::array<Jet, 3> point = {Jet(0.0), Jet(0.0), Jet(0.0)};
   std::array<Jet, 3> rotated_point;
-  QuaternionRotatePoint(quaternion.data(), point.data(), rotated_point.data());
+  QuaternionRotatePoint<Order>(
+      quaternion.data(), point.data(), rotated_point.data());
   for (int i = 0; i < 3; ++i) {
     EXPECT_EQ(rotated_point[i], point[i]);
     EXPECT_FALSE(rotated_point[i].v.array().isNaN().any());