Randomly draw Quaternions within a specific range of Euler angles for rotation

coordinate systemsgeometrypythonquaternionsrotations

I am pretty new to Quaternions so please bear with me. I want to draw random Quaternion samples so that their Euler angle equivalent would range within [-30, +30] degrees on each axis. Currently, I know how to sample Quaternions from the full range ([-180, +180]) using the code below but I don't know how to modify the code so that I can get samples within the range [-30, +30]. Can anyone help me with that?

I'm not sure if this is helpful to answer this question but here's a piece of information: I eventually want to convert the sampled Quaternion to Euler angles an apply the Euler rotation in some 3D shapes. The order of rotation in the software I'm using to do this is XYZ meaning that it first rotates the 3D shape along the X axis, then Y axis and then Z axis.

import numpy as np
def sample_Quaternion():
    r = np.random.uniform(0, 1 - 0.001, 3)
    while np.linalg.norm(r) > 1:
        r = np.random.uniform(0, 1 - 0.001, 3) # Just to keep the L2 norm within [0, 1.0)
    w = [np.sqrt(1 - (r[0]*r[0] + r[1]*r[1] + r[2]*r[2]))]
    r = np.concatenate(r, w) # the output of this would represent (x, y, z, w)
    return r

Best Answer

Empirically, by trying combinations of yaw, pitch, and roll angles ranging independently between $-30$ and $30$ degrees, and converting each sequence of Euler rotations to an equivalent quaternion using the same algorithm as in How to convert Euler angles to Quaternions and get the same Euler angles back from Quaternions?, the real part of the quaternion ranges from approximately $0.88388347648318455$ to $1$ and the other parts each range from approximately $-0.30618621784789724$ to $0.30618621784789724.$

Note that $5/\sqrt{32} \approx 0.88388347648318441$ and $\sqrt{3/32} \approx 0.30618621784789726,$ so I'll use those numbers as the extreme values of the parts of the quaternion and blame the inexactness of numerical computation for the differences with the values in the previous paragraph.

In principle, if we could sample uniformly over all unit-magnitude quaternions in these ranges of values, and reject the ones that convert back to Euler angles outside your desired range, that would be an answer. The tricky part is the uniform sampling over only quaternions with unit magnitude. It might be nice to get those by rejection sampling within the volume $$\left[-\sqrt{\tfrac{3}{32}},\sqrt{\tfrac{3}{32}}\right] \times \left[-\sqrt{\tfrac{3}{32}},\sqrt{\tfrac{3}{32}}\right] \times \left[-\sqrt{\tfrac{3}{32}},\sqrt{\tfrac{3}{32}}\right] \times \left[\tfrac{5}{\sqrt{32}},1\right],$$ that is, letting each of the imaginary parts of the quaternion range from $-\sqrt{\tfrac{3}{32}}$ to $\sqrt{\tfrac{3}{32}}$ and letting the real part range from $\tfrac{5}{\sqrt{32}}$ to $1,$ but since mathematically the unit quaternions occupy zero percent of this volume, that's not a good approach. Instead, you might try to get a sample over a set that includes all the quaternions of the form $r\mathbf q$, where $0 < r \leq 1$ and $\mathbf q$ is one of the unit quaternions producing your desired set of rotations. (Since $r\mathbf q$ describes the same rotation as $\mathbf q$ when $r$ is real and non-zero, this gives the same distribution of rotations as if we sampled only over the suitable unit quaternions.) You can get this larger set of quaternions by sampling over $$\left[-\sqrt{\tfrac{3}{32}},\sqrt{\tfrac{3}{32}}\right] \times \left[-\sqrt{\tfrac{3}{32}},\sqrt{\tfrac{3}{32}}\right] \times \left[-\sqrt{\tfrac{3}{32}},\sqrt{\tfrac{3}{32}}\right] \times \left[0,1\right]$$ (similar to the previous volume but letting the real part range all the way from $0$ to $1$) and rejecting any quaternion whose magnitude is greater than $1$ (less than $1$ is OK) or whose Euler angles are not within the desired range. This will be better than letting all the parts of the quaternion range over $[-1,1],$ because you'll be able to accept a much larger percentage of the random quaternions within that region; but I think you may do even better by accepting only quaternions in a smaller range of magnitudes, perhaps $\frac12$ to $1,$ using an initial sample over a smaller region.

So I might suggest something like this: set $r_\min$ to a suitable value such as $\frac12.$ Independently choose three numbers each in the range $\left[-\sqrt{\tfrac{3}{32}},\sqrt{\tfrac{3}{32}}\right]$ and one number in the range $\left[\tfrac{5}{\sqrt{32}}r_\min,1\right].$ Assign the first three numbers to the imaginary parts of a quaternion and the last number to the real part. If the resulting quaternion has a magnitude less than $r_\min$ or greater than $1,$ reject it and start over. Convert the resulting quaternion to Euler angles; if any of those angles is less than $-30$ degrees or greater than $30$ degrees, reject the quaternion and start over. But if the quaternion passes all these tests, use it as your next random rotation.

I think any method similar to this is going to end up rejecting a large percentage of the quaternions, but as long as you can get a reasonable percentage to be in the "accept" region then it can be a practical way to get a uniform distribution over the desired set of rotations.


For reference, here is the python code I used to explore the range of unit quaternions that you might use:

import math
import numpy as np
def euler_to_quaternion(r):
    (yaw, pitch, roll) = (r[0], r[1], r[2])
    qx = np.sin(roll/2) * np.cos(pitch/2) * np.cos(yaw/2) - np.cos(roll/2) * np.sin(pitch/2) * np.sin(yaw/2)
    qy = np.cos(roll/2) * np.sin(pitch/2) * np.cos(yaw/2) + np.sin(roll/2) * np.cos(pitch/2) * np.sin(yaw/2)
    qz = np.cos(roll/2) * np.cos(pitch/2) * np.sin(yaw/2) - np.sin(roll/2) * np.sin(pitch/2) * np.cos(yaw/2)
    qw = np.cos(roll/2) * np.cos(pitch/2) * np.cos(yaw/2) + np.sin(roll/2) * np.sin(pitch/2) * np.sin(yaw/2)
    return [qx, qy, qz, qw]

qa = [-1,-1,-1,-1]
qb = [1,1,1,1]
for r1 in range(-30,31,15):
    for r2 in range(-30,31,15):
        for r3 in range(-30,31,15):
            r = [math.radians(r1),math.radians(r2),math.radians(r3)]
            q = euler_to_quaternion(r)
            qa = [max(x,y) for x,y in zip(q, qa)]
            qb = [min(x,y) for x,y in zip(q, qb)]
            print ([r1,r2,r3], euler_to_quaternion(r))

print (qa)
print (qb)

The last two lines of the output were

[0.30618621784789724, 0.30618621784789724, 0.30618621784789724, 1.0]
[-0.30618621784789724, -0.30618621784789724, -0.30618621784789724, 0.88388347648318455]