Beware the "Natural" Quaternion

possiblywrong1 pts0 comments

Beware the "natural" quaternion | Possibly Wrong

Skip to primary content

Introduction

Rotation math can be confusing. But it didn’t need to be this confusing.

I think the reason that 3D rotations can be tricky to work with is that there are so many choices of convention– in interpretation, notation, and implementation– and we often do not communicate or document all of those choices clearly and completely. Are we "actively" rotating vectors, or "passively" rotating frames? If the latter, are we rotating from the "body" to "world" frame, or from world to body? Are rotations applied as we write them left to right, or right to left? Are the rotations right-handed or left-handed? Etc.

This post is motivated by lost and delayed productivity, stemming from confusion and miscommunication about one of these choices that seems not widely known: there are two different quaternions out there in the wild. The unit quaternion, familiar to most of us as a way to represent rotations in many application domains, has an evil twin. If you didn’t already know this, then hopefully this post is a helpful warning to double-check your documentation and code. But even if this is old news for you, then I will try to argue that use of this evil twin quaternion, advocated by Shuster [1] as the "natural" convention, was an unnecessary choice… and certainly not any more "natural," "physical," or otherwise inherently preferable to the quaternion most of us are familiar with.

Rotation matrices

To try to show more clearly how silly this situation is, let’s think like a software developer, working out how to implement rotations for some application. Let’s start with the interface first, then we will flesh out the implementation. We can view the concept of rotation as a group acting on , with a particular rotation represented as a group element , that transforms a vector into a new rotated vector . For example, in Python, we could make a Rotation object callable:

class Rotation:<br>...

def __call__(self, v):<br>"""Rotate vector v."""<br>return ...

g = Rotation(...)<br>v = (1, 0, 0)<br>v_rotated = g(v)

But wait– we’ve already made several choices of convention here, at least implicitly. First, from this software implementation perspective, we are bypassing the discussion of interpretation altogether. That is, for example, whether the user wants to interpret an argument vector as being "actively" rotated within a fixed coordinate frame, or as a fixed point in space whose coordinates are being "passively" rotated from one frame to another, does not affect the eventual lines of code implementing the arithmetic operations realizing the rotation. (Or put another way, those interpretation details get pushed into the constructor, affecting how we want to interpret the parameters specifying creation of a Rotation. More on this later.)

However, we have made a definite choice about notation: namely, the left group action convention, where we write (from left to right) the rotation group element first, followed by the vector being acted upon.

This is okay; no one is complaining about this choice of convention. For example, if we now consider actually implementing a rotation as a 3×3 matrix (we’ll get to quaternions shortly), pretty much everyone agrees with and has settled on the well-established convention of multiplying the matrix on the left by the column vector on the right, so that the group action corresponds to the matrix multiplication :

import numpy as np

class Rotation:<br>def __init__(self, matrix):<br>"""Create rotation from matrix."""<br>self.r = np.array(matrix)

def __call__(self, v):<br>"""Rotate vector v."""<br>return self.r @ np.array(v) # matrix multiplication

g = Rotation(((0, -1, 0), (1, 0, 0), (0, 0, 1)))<br>v = (1, 0, 0)<br>v_rotated = g(v)

(As an aside, the only reasonably common application context I can think of where it’s more common to multiply a row vector on the left by a matrix on the right is a Markov chain, where we update the probability distribution as a row vector multiplied by the transition matrix. Maybe there are others that I’m not aware of?)

Composition of rotations

Our implementation isn’t quite complete. We still need to work out how to compose multiple rotations. Fortunately, there is again community-wide agreement on how this works with rotation matrices, and it’s consistent with the same left group action convention. Namely, composition (just like the group action on vectors) is denoted and implemented as the usual left-to-right matrix multiplication:

import numpy as np

class Rotation:<br>def __init__(self, matrix):<br>"""Create rotation from matrix."""<br>self.r = np.array(matrix)

def __call__(self, v):<br>"""Rotate vector v."""<br>return self.r @ np.array(v)

def __matmul__(self, r2):<br>"""Compose rotations (this @ r2)."""<br>return Rotation(self.r @ r2.r)

r1 = Rotation(((0, -1, 0), (1, 0, 0), (0, 0, 1)))<br>r2 = Rotation(((0, 0, 1), (0, 1, 0), (-1, 0, 0)))<br>v = (1, 0, 0)

v_rotated = (r1 @ r2)(v)<br>v_rotated = r1(r2(v)) # same result

Note...

rotation matrix self vector left rotations

Related Articles