Examples

Let’s start by importing the quatorch.Quaternion class:

from quatorch import Quaternion

There are two main ways to initialize a quaternion:

  • from four scalars representing WXYZ components

  • from a tensor that has shape \((..., 4)\)

Here are two ways to define a \(45\)-degree rotation around the X axis:

import torch


# Create a quaternion from four scalars (W, X, Y, Z)
q = Quaternion(0.9239, 0.3827, 0.0, 0.0)

# Or from a tensor of shape (..., 4)
q2 = Quaternion(torch.tensor([0.9239, 0.3827, 0.0, 0.0]))

print(f"{(q == q2) = }")
(q == q2) = tensor([True, True, True, True])

Both methods create the same quaternion. Notice that element-wise comparison was performed, as a quaternion is still a subclass of torch.Tensor. Let’s inspect some quaternion operations as multiplication and exponentiation:

q = q.normalize()

print(f"{q*q*q*q             = }")
print(f"{q**4                = }")
print(f"{(4 * q.log()).exp() = }")
print(f"{-q**(-4)            = }")
q*q*q*q             = Quaternion([-2.9832e-05,  1.0000e+00,  0.0000e+00,  0.0000e+00])
q**4                = Quaternion([-2.9846e-05,  1.0000e+00,  0.0000e+00,  0.0000e+00])
(4 * q.log()).exp() = Quaternion([-2.9846e-05,  1.0000e+00,  0.0000e+00,  0.0000e+00])
-q**(-4)            = Quaternion([2.9846e-05, 1.0000e+00, 0.0000e+00, 0.0000e+00])

As expected, all the above returned the same result, which represents a 180 degrees rotation around the X axis. We can verify that by converting it to an axis-angle representation:

axis, angle = (q**4).to_axis_angle()
print(f"Axis: {axis}, Angle (degrees): {angle.rad2deg()}")
Axis: tensor([1.0000, 0.0000, 0.0000]), Angle (degrees): 180.00341796875

Or a rotation matrix:

q.to_rotation_matrix()
tensor([[ 1.0000,  0.0000,  0.0000],
        [ 0.0000,  0.7071, -0.7071],
        [ 0.0000,  0.7071,  0.7071]])

Tip

You may also construct quaternions from rotation matrices or an axis-angle representation using the methods quatorch.Quaternion.from_rotation_matrix and quatorch.Quaternion.from_axis_angle

One interesting property of quaternions is that they take value on a 4D hyper-sphere. This means that we can interpolate between two quaternions using spherical linear interpolation (quatorch.Quaternion.slerp). Let’s see how it works:

q_final = q**4
t = 1 / 3  # interpolation parameter in [0, 1]

q.slerp(q_final, t)
Quaternion([0.7071, 0.7071, 0.0000, 0.0000])

Notice that this (and all other operations documented in the API reference) supports broadcasting. Thus, we can work with batches of quaternions seamlessly. In the example above, let’s use t as a tensor of shape (4, 1) to get a batch of interpolated quaternions:

t = torch.linspace(0, 1, steps=4).unsqueeze(-1)

q.slerp(q_final, t)
Quaternion([[ 9.2388e-01,  3.8269e-01,  0.0000e+00,  0.0000e+00],
            [ 7.0710e-01,  7.0712e-01,  0.0000e+00,  0.0000e+00],
            [ 3.8266e-01,  9.2389e-01,  0.0000e+00,  0.0000e+00],
            [-2.9862e-05,  1.0000e+00,  0.0000e+00,  0.0000e+00]])

Finally let’s use this batch of quaternions to rotate a vector:

t = torch.linspace(0, 1, steps=10).unsqueeze(-1)

batch_q = q.slerp(q_final, t)
batch_q.rotate_vector(torch.tensor([1.0, 0.0, 0.0]))
tensor([[1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000],
        [1.0000, 0.0000, 0.0000]])

Unfortunately, rotating \([1, 0, 0]\) around the X axis does not change it. Let’s try with another vector:

rotated_vectors = batch_q.rotate_vector(torch.tensor([0.0, 1.0, -1.0]))

To better illustrate it, let’s plot the projection of these vectors on the YZ plane, to visualize the rotation:

from matplotlib import pyplot as plt

plt.scatter(rotated_vectors[:, 1], rotated_vectors[:, 2])
plt.axis("equal")
plt.xlabel("Y")
plt.ylabel("Z")
plt.show()
_images/d127dee7db1e2236b0d7925679729a75024432d2b9b15c5d6b2c033ccb2bcf8d.png