This page was generated from doc/rotation/quaternions.ipynb. Interactive online version: Binder badge.

Quaternions#

We are interested in unit quaternions (see below), because they are a very useful representation of rotations. But before we go into that, we should probably mention what a quaternion is. We don’t need all the details, we just need to know a few facts (without burdening ourselves too much with mathematical rigor):

  • Quaternions live in the four-dimensional Euclidean space \(\mathbb{R}^4\). Each quaternion has exactly one corresponding element of \(\mathbb{R}^4\) and vice versa.

  • Unlike elements of \(\mathbb{R}^4\), quaternions support a special kind of quaternion multiplication.

  • Quaternion multiplication is weird. The order of operands matters (i.e. multiplication is noncommutative).

A Python implementation is available in the class splines.quaternion.Quaternion.

Quaternion Representations#

There are multiple equivalent ways to represent quaternions. Their original algebraic representation is

\begin{equation*} q = w + x\mathbf{i} + y\mathbf{j} + z\mathbf{k}, \end{equation*}

where \(\mathbf{i}^2 = \mathbf{j}^2 = \mathbf{k}^2 = \mathbf{ijk} = -1\). It is important to note that the order in which the basic quaternions \(\mathbf{i}\), \(\mathbf{j}\) and \(\mathbf{k}\) are multiplied matters: \(\mathbf{ij} = \mathbf{k}\), \(\mathbf{ji} = -\mathbf{k}\) (i.e. their multiplication is anticommutative). The information given so far should be sufficient to derive quaternion multiplication, but let’s not do that right now. Quaternions can also be represented as pairs containing a scalar and a 3D vector:

\begin{equation*} q = (w, \vec{v}) = (w, (x, y, z)) \end{equation*}

Sometimes, the scalar and vector parts are also called “real” and “imaginary” parts, respectively. The four components can also be displayed as simple 4-tuples, which can be interpreted as coordinates of the four-dimensional Euclidean space \(\mathbb{R}^4\):

\begin{equation*} q = (w, x, y, z) \quad\text{or}\quad q = (x, y, z, w) \end{equation*}

The order of components can be chosen arbitrarily. In mathematical textbooks, the order \((w, x, y, z)\) is often preferred (and sometimes written as \((a, b, c, d)\)). In numerical software implementations, however, the order \((x, y, z, w)\) is more common (probably because it is memory-compatible with 3D vectors \((x, y, z)\)). In the Python class splines.quaternion.Quaternion, these representations are available via the attributes scalar, vector, wxyz and xyzw.

There are even more ways to represent quaterions, for example as 2x2 complex matrices or as 4x4 real matrices [McD10].

Unit Quaternions#

Quite simply, unit quaternions are the set of all quaternions whose distance to the origin \((0, (0, 0, 0))\) equals \(1\). In \(\mathbb{R}^3\), all elements with unit distance from the origin form the unit sphere (a.k.a. \(S^2\)), which is a two-dimensional curved space. Since quaternions inhabit \(\mathbb{R}^4\), the unit quaternions form the unit hypersphere (a.k.a. \(S^3\)), which is a three-dimensional curved space.

One important unit quaternion is \((1, (0, 0, 0))\), sometimes written as \(\boldsymbol{1}\), which corresponds to the real number \(1\).

A Python implementation of unit quaternions is available in the class splines.quaternion.UnitQuaternion.

Unit Quaternions as Rotations#

Given a (normalized) rotation axis \(\vec{n}\) and a rotation angle \(\alpha\) (in radians), we can create a corresponding quaternion (which will have unit length):

\begin{equation*} q = \left(\cos \frac{\alpha}{2}, \vec{n} \sin \frac{\alpha}{2}\right) \end{equation*}

Unit quaternions are a double cover over the rotation group (a.k.a. SO(3)), which means that each rotation can be associated with two distinct quaternions. More specifically, the antipodal points \(q\) and \(-q\) represent the same rotation – see Negation below.

More details can be found on Wikipedia.

To get a bit of intuition, let’s plot a few quaternion rotations (with the help of helper.py).

[1]:
from helper import angles2quat, plot_rotation

The quaternion \(\boldsymbol{1}\) represents “no rotation at all”.

[2]:
identity = angles2quat(0, 0, 0)
identity
[2]:
UnitQuaternion(scalar=1.0, vector=(0.0, 0.0, 0.0))
[3]:
a = angles2quat(90, 0, 0)
b = angles2quat(0, 35, 0)
c = angles2quat(0, 0, 45)
[4]:
plot_rotation({
    'identity = 1': identity,
    '$a$': a,
    '$b$': b,
    '$c$': c,
});
../_images/rotation_quaternions_17_0.svg

Axes Conventions#

When converting between rotation angles (see Euler/Tait–Bryan angles) and unit quaternions, we can freely choose from a multitude of axes conventions. Here we choose a (global) coordinate system where the x-axis points towards the right margin of the page and the y-axis points towards the top of the page. We are using a right-handed coordinate system, which leaves the z-axis pointing out of the page, towards the reader. The helper function angles2quat() takes three angles (in degrees) which are applied in this order:

  • azimuth: rotation around the (global) z-axis

  • elevation: rotation around the (previously rotated local) x-axis

  • roll: rotation around the (previously rotated local) y-axis

This is equivalent to applying the angles in the opposite order, but using a global frame of reference for each rotation.

The sign of the rotation angles always follows the right-hand rule.

Quaternion Multiplication#

As mentioned above, quaternion multiplication (sometimes called Hamilton product) is noncommutative, i.e. the order of operands matters. When using unit quaternions to represent rotations, quaternion multiplication can be used to apply rotations to other rotations. Given a rotation \(q_0\), we can apply another rotation \(q_1\) by left-multiplication: \(q_1 q_0\). In other words, applying a rotation of \(q_0\) followed by a rotation of \(q_1\) is equivalent to applying a single rotation \(q_1 q_0\). Note that \(q_1\) represents a rotation in the global frame of reference.

When dealing with local frames of reference, the order of multiplications has to be reversed. Given a rotation \(q_2\), which describes a new local coordinate system, we can apply a local rotation \(q_3\) (relative to this new coordinate system) by right-multiplication: \(q_2 q_3\). In other words, applying a rotation of \(q_2\) followed by a rotation of \(q_3\) (relative to the local coordinate system defined by \(q_2\)) is equivalent to applying a single rotation \(q_2 q_3\).

In general, changing the order of rotations changes the resulting rotation:

\begin{equation*} q_m q_n \ne q_n q_m \end{equation*}

[5]:
plot_rotation({'$ab$': a * b, '$ba$': b * a});
../_images/rotation_quaternions_21_0.svg

However, there is an exception when all rotation axes are the same, in which case the rotation angles can simply be added (in arbitrary order, of course).

The quaternion \(\boldsymbol{1} = (1, (0, 0, 0))\) is the identity element with regards to quaternion multiplication. A multiplication with this (on either side) leads to an unchanged rotation.

Even though quaternion multiplication is non-commutative, it is still associative, which means that if there are multiple multiplications in a row, they can be grouped arbitrarily, leading to the same overall result:

\begin{equation*} (q_1 q_2) q_3 = q_1 (q_2 q_3) \end{equation*}

[6]:
plot_rotation({'$(bc)a$': (b * c) * a, '$b(ca)$': b * (c * a)});
../_images/rotation_quaternions_24_0.svg

Inverse#

The multiplicative inverse of a quaternion is written as \(q^{-1}\). When talking about rotations, this operation leads to a new rotation with the same rotation axis but with negated angle (or equivalently, the same angle with a flipped rotation axis).

[7]:
plot_rotation({'$b$': b, '$b^{-1}$': b.inverse()});
../_images/rotation_quaternions_27_0.svg

By multiplying a rotation with its inverse, the original rotation can be undone: \(q q^{-1} = q^{-1} q = \boldsymbol{1}\). Since both operands have the same rotation axis, the order doesn’t matter in this case.

For unit quaternions, the inverse \(q^{-1}\) equals the conjugate \(\overline{q}\). The conjugate of a quaternion is constructed by negating its vector part (and keeping its scalar part unchanged). This can be achieved by negating the rotation axis \(\vec{n}\). Alternatively, we can negate the rotation angle, since \(\sin(-\phi) = -\sin(\phi)\) (antisymmetric) and \(\cos(-\phi) = \cos(\phi)\) (symmetric).

\begin{equation*} \overline{q} = \left(w, -\vec{v}\right) = \left(\cos \frac{\alpha}{2}, -\vec{n} \sin \frac{\alpha}{2}\right) = \left(\cos \frac{-\alpha}{2}, \vec{n} \sin \frac{-\alpha}{2}\right) \end{equation*}

Relative Rotation (Global Frame of Reference)#

Given two rotations \(q_0\) and \(q_1\), we can try to find a third rotation \(q_{0,1}\) that rotates \(q_0\) into \(q_1\). Since we are considering the global frame of reference, \(q_{0,1}\) must be left-multiplied with \(q_0\):

\begin{equation*} q_{0,1} q_0 = q_1 \end{equation*}

Now we can right-multiply both sides with \({q_0}^{-1}\):

\begin{equation*} q_{0,1} q_0 {q_0}^{-1} = q_1 {q_0}^{-1} \end{equation*}

\(q_0 {q_0}^{-1}\) cancels out and we get:

\begin{equation*} q_{0,1} = q_1 {q_0}^{-1} \end{equation*}

Relative Rotation (Local Frame of Reference)#

If \(q_{0,1}\) is supposed to be a rotation in the local frame of \(q_0\), we have to change the order of multiplication:

\begin{equation*} q_0 q_{0,1} = q_1 \end{equation*}

Now we can left-multiply both sides with \({q_0}^{-1}\):

\begin{equation*} {q_0}^{-1} q_0 q_{0,1} = {q_0}^{-1} q_1 \end{equation*}

\({q_0}^{-1} q_0\) cancels out and we get:

\begin{equation*} q_{0,1} = {q_0}^{-1} q_1 \end{equation*}

Exponentiation#

Raising a unit quaternion to an integer power simply means applying the same rotation multiple times:

[8]:
plot_rotation({
    '$a^0 = 1$': a**0,
    '$a^1 = a$': a**1,
    '$a^2 = aa$': a**2,
    '$a^3 = aaa$': a**3,
});
../_images/rotation_quaternions_34_0.svg

It shouldn’t come as a surprise that \(q^0 = \boldsymbol{1}\) and \(q^1 = q\).

Using an exponent of \(-1\) is equivalent to taking the inverse – see above. Negative integer exponents apply the inverse rotation multiple times. Non-integer exponents lead to partial rotations, with the exponent \(k\) being proportional to the rotation angle. The rotation axis \(\vec{n}\) is unchanged by exponentiation.

\begin{equation*} q^k = \left(\cos \frac{k\alpha}{2}, \vec{n} \sin \frac{k\alpha}{2}\right) \end{equation*}

[9]:
plot_rotation({
    '$a^1 = a$': a**1,
    '$a^{0.5}$': a**0.5,
    '$a^0 = 1$': a**0,
    '$a^{-0.5}$': a**-0.5,
});
../_images/rotation_quaternions_38_0.svg

Negation#

A quaternion can be negated by negating all 4 of its components. This corresponds to flipping its orientation in 4D space (but keeping its direction and length). For unit quaternions, this means selecting the diametrically opposite (antipodal) point on the unit hypersphere.

Due to the double cover property mentioned above, negating a unit quaternion doesn’t change the rotation it is representing.

[10]:
plot_rotation({'$c$': c, '$-c$': -c});
../_images/rotation_quaternions_41_0.svg

One way to negate the scalar part of a unit quaternion is to add \(\pi\) to the argument of the cosine function, since \(\cos(\phi + \pi) = -\cos(\phi)\). Because only half of the rotation appears in the argument of the cosine, we have to add \(2\pi\) to the rotation angle \(\alpha\), which brings us back to the original rotation. Adding \(2\pi\) to the rotation angle also negates the vector part of the unit quaternion (since \(\sin(\phi + \pi) = -\sin(\phi)\)), assuming the rotation axis \(\vec{n}\) stays unchanged.

\begin{equation*} -q = \left(-w, -\vec{v}\right) = \left( \cos \frac{\alpha + 2 \pi}{2}, \vec{n} \sin \frac{\alpha + 2 \pi}{2} \right) \end{equation*}

Canonicalization#

When we are given multiple rotations and we want to represent them as quaternions, we have to take care of the ambiguity caused by the double cover property – see Slerp Visualization for an example of this ambiguity.

One way to do that is to make sure that in a sequence of rotations (which we want to use as the control points of a spline, for example), the angle (in 4D space) between neighboring quaternions is at most 90 degrees (which corresponds to a 180 degree rotation in 3D space). For any pair of quaternions where this is not the case, one of the quaternions can simply be negated. The function splines.quaternion.canonicalized() can be used to create an iterator of canonicalized quaternions from an iterable of arbitrary quaternions.