This notebook uses the quaternion algebra to rotate vectors and to represent the orientation of a rigid body.
A unit quaternion is the tool we use to represent the orientation of a rigid body or coordinate frame in 3D. Compared with Euler angles it has no gimbal lock (no orientation where a degree of freedom is lost), and compared with a $3\times3$ rotation matrix it is compact (4 numbers instead of 9) and easy to keep normalised and to interpolate. We will see that a unit quaternion both rotates vectors and, read the other way, transforms vector coordinates between frames.
To rotate a 3D vector $\vec v=(v_x,v_y,v_z)$ with the quaternion product, it must first be "promoted" to a pure quaternion — a quaternion with zero scalar part:
$$\mathbf v = \begin{bmatrix} 0 \\ \vec v\end{bmatrix} = 0 + v_x i + v_y j + v_z k .$$
A rotation of angle $\phi$ about a unit axis $\vec u$ is encoded by the unit quaternion
$$\mathbf q = \begin{bmatrix}\cos(\phi/2)\\[2pt] \vec u\,\sin(\phi/2)\end{bmatrix} = \cos\tfrac{\phi}{2} + \big(u_x i + u_y j + u_z k\big)\sin\tfrac{\phi}{2}.$$
Its norm is $\cos^2\tfrac{\phi}{2}+\sin^2\tfrac{\phi}{2}=1$, so it is a unit quaternion, and every unit quaternion can be written in this axis–angle form.
Why the half-angle $\phi/2$? The rotation is applied as a sandwich $\mathbf q\otimes\mathbf v\otimes\mathbf q^*$ (below), so $\mathbf q$ effectively acts twice on the vector. Encoding $\phi/2$ in $\mathbf q$ makes the two half-turns combine into the desired full angle $\phi$.
Because $\mathbf q$ is a unit quaternion, its inverse equals its conjugate: $$\mathbf q^{-1}=\mathbf q^* = \begin{bmatrix}\cos(\phi/2)\\[2pt] -\vec u\,\sin(\phi/2)\end{bmatrix}.$$
The vector $\vec v$ (as the pure quaternion $\mathbf v$) is rotated by
$$\boxed{\;\mathbf v' = \mathbf q \otimes \mathbf v \otimes \mathbf q^*\;}$$
where
The sandwich product preserves the norm ($\lVert\vec v\,'\rVert=\lVert\vec v\rVert$) and leaves the scalar part zero — it is a genuine rotation. Read as "the vector is moved while the frame stays fixed," this is an active rotation.
The full process to rotate $\vec v$ by angle $\phi$ about unit axis $\vec u$:
The same algebra $\mathbf q\otimes(\cdot)\otimes\mathbf q^*$ is used in two ways that are easy to confuse:
So "rotating a vector by $\mathbf q$" and "expressing a body vector in the world via orientation $\mathbf q$" are the same computation — only the interpretation differs.
Following the Hamilton, local→global convention, the orientation quaternion is defined as the transformation from the body frame $B$ to the inertial/world frame $I$:
$$\mathbf q = \mathbf q_{B\to I} = \mathbf q_{GL}.$$
Then a vector's coordinates transform as
$$\mathbf v_I = \mathbf q\otimes\mathbf v_B\otimes\mathbf q^*,\qquad \mathbf v_B = \mathbf q^*\otimes\mathbf v_I\otimes\mathbf q ,$$
where $\mathbf v_I$ and $\mathbf v_B$ are the same physical vector expressed in the two frames, and the inverse transform uses $\mathbf q_{I\to B}=\mathbf q_{B\to I}^*=\mathbf q^*$. (This is the convention used by the IMU/ESKF notebooks and by ROS/Eigen/Ceres.)
The sandwich product is linear in $\vec v$, so it can be written as a single $3\times3$ rotation matrix $\mathbf R\{\mathbf q\}$ acting on the vector:
$$\vec v\,' = \mathbf R\{\mathbf q\}\,\vec v ,\qquad \mathbf R\{\mathbf q\}=\begin{bmatrix} 1-2(q_y^2+q_z^2) & 2(q_xq_y-q_wq_z) & 2(q_xq_z+q_wq_y)\\ 2(q_xq_y+q_wq_z) & 1-2(q_x^2+q_z^2) & 2(q_yq_z-q_wq_x)\\ 2(q_xq_z-q_wq_y) & 2(q_yq_z+q_wq_x) & 1-2(q_x^2+q_y^2) \end{bmatrix}.$$
$\mathbf R\{\mathbf q\}$ is orthonormal with $\det=+1$ (a proper rotation), and $\mathbf R\{\mathbf q\}=\mathbf R\{-\mathbf q\}$ — the quaternion double-covers $SO(3)$, i.e. $\mathbf q$ and $-\mathbf q$ are the same rotation. This is the matrix that appears as $\mathbf R$ in the ESKF equations (digest).
To apply rotation $\mathbf q_1$ first and then $\mathbf q_2$, multiply the quaternions in that order:
$$\mathbf q_c = \mathbf q_2 \otimes \mathbf q_1,\qquad\text{equivalently}\qquad \mathbf R\{\mathbf q_1\otimes\mathbf q_2\}=\mathbf R\{\mathbf q_1\}\,\mathbf R\{\mathbf q_2\}.$$
Since the quaternion product is non-commutative, the order matters — exactly mirroring the fact that $3\times3$ rotation matrices do not commute.
The cell below builds a rotation quaternion from an axis and angle and confirms: the sandwich product equals the matrix action $\mathbf R\{\mathbf q\}\vec v$, the rotation preserves norm, $\mathbf R\{\mathbf q\}$ is a proper rotation, composition matches matrix multiplication, and $\mathbf q,-\mathbf q$ give the same rotation.
import numpy as np
def qmul(a, b): # Hamilton product a ⊗ b
aw, av = a[0], a[1:]; bw, bv = b[0], b[1:]
return np.concatenate([[aw*bw - av @ bv], aw*bv + bw*av + np.cross(av, bv)])
def qconj(q): return np.concatenate([[q[0]], -q[1:]])
def quat_from_axis_angle(u, phi):
u = np.asarray(u, float); u = u / np.linalg.norm(u)
return np.concatenate([[np.cos(phi/2)], u * np.sin(phi/2)])
def rotate(q, v): # sandwich product q ⊗ [0,v] ⊗ q*
return qmul(qmul(q, np.concatenate([[0.0], v])), qconj(q))[1:]
def R_of_q(q): # rotation matrix R{q}
w, x, y, z = q
return np.array([[1-2*(y*y+z*z), 2*(x*y-w*z), 2*(x*z+w*y)],
[2*(x*y+w*z), 1-2*(x*x+z*z), 2*(y*z-w*x)],
[2*(x*z-w*y), 2*(y*z+w*x), 1-2*(x*x+y*y)]])
# concrete example: rotate (1,0,0) by 90° about +z -> (0,1,0)
q90 = quat_from_axis_angle([0, 0, 1], np.pi/2)
print("rotate (1,0,0) by 90° about z :", np.round(rotate(q90, [1, 0, 0]), 6))
q = quat_from_axis_angle([0.3, -0.7, 0.5], 1.1); v = np.array([1.0, 2.0, -0.5])
print("sandwich == R{q}·v :", np.linalg.norm(rotate(q, v) - R_of_q(q) @ v))
print("norm preserved :", abs(np.linalg.norm(rotate(q, v)) - np.linalg.norm(v)))
print("R{q} proper rotation (det,orth):", round(np.linalg.det(R_of_q(q)), 6),
",", round(np.linalg.norm(R_of_q(q).T @ R_of_q(q) - np.eye(3)), 12))
q1 = quat_from_axis_angle([0, 0, 1], 0.5); q2 = quat_from_axis_angle([1, 0, 0], 0.8)
print("compose R{q2⊗q1}=R{q2}R{q1} :", np.linalg.norm(R_of_q(qmul(q2, q1)) - R_of_q(q2) @ R_of_q(q1)))
print("double cover ||R{q}-R{-q}|| :", np.linalg.norm(R_of_q(q) - R_of_q(-q)))
This notebook commits to Joan Solà's Hamilton convention:
Literature