Skip to content
AI360XpertBeta

Robot Orientation and Quaternions

A tool can point the same way under two different quaternion signs. Choosing consistent signs lets a robot interpolate the short rotation instead of spinning the long way around.

The short turn between headings 170 and -170 degrees passes through 180 degrees, not zero.
The short turn between headings 170 and -170 degrees passes through 180 degrees, not zero.

Why Does This Exist?

A wrist needs to turn from a heading of 170 degrees to -170 degrees. Averaging those numbers gives zero: a tool pointing the opposite way from the intended midpoint. The numbers wrap; the physical orientation does not jump.

Orientation describes which way a rigid body's axes point. It has no translation. Coordinate frames already explain rotation matrices and frame composition. Here the missing operation is representing and interpolating orientation without treating angle coordinates as ordinary positions. A quaternion is a compact representation of that same rotation, not an extra physical degree of freedom.

Think of It Like This

Turning a dial across its printed seam

A circular dial has 179 printed just before the seam and -179 just after it. Moving the pointer across the seam is a small turn; sliding a ruler between the printed numbers suggests crossing the whole dial. Quaternion sign alignment chooses neighboring descriptions on the rotation path before interpolation.

The pointer maps to the tool's direction and the seam to a coordinate discontinuity. The limit is dimensional: a dial rotates about one fixed axis, while a wrist can rotate about any axis in space. The diagram deliberately uses the dial's simpler slice.

How It Actually Works

What do the four components mean?

Use active rotations of vectors in a fixed right-handed frame and scalar-first Hamilton quaternions q=(w,v)=(w,x,y,z)q=(w,\mathbf v)=(w,x,y,z). For a unit rotation axis n\mathbf n and angle θ\theta in radians,

q=(cos⁡θ2, nsin⁡θ2),w2+x2+y2+z2=1.q=\left(\cos\frac\theta2,\ \mathbf n\sin\frac\theta2\right),\qquad w^2+x^2+y^2+z^2=1.

The axis sets the turn's direction; the angle sets its size. Four constrained numbers represent three rotational degrees of freedom. SciPy's quaternion API documents both component orders; its default is scalar-last, unlike this page. Label the convention at every interface.

For unit qq, the inverse is q−1=(w,−v)q^{-1}=(w,-\mathbf v). A vector p\mathbf p rotates by taking the vector part of q(0,p)q−1q(0,\mathbf p)q^{-1}. Both qq and −q-q produce the same answer because the two minus signs cancel. This double cover explains why raw quaternion-component differences are poor orientation errors.

For qa=(a,u)q_a=(a,\mathbf u) and qb=(b,v)q_b=(b,\mathbf v), Hamilton multiplication is

qaqb=(ab−u⋅v, av+bu+u×v).q_aq_b=(ab-\mathbf u\cdot\mathbf v,\ a\mathbf v+b\mathbf u+\mathbf u\times\mathbf v).

The dot product is a scalar; the cross product is the perpendicular vector. Under our active convention, qaqbq_aq_b applies qbq_b first. Rotations about different axes generally do not commute. Multiplication order must also agree with the source and destination frame convention.

Which interpolated orientation is halfway?

Normalize both endpoints q0,q1q_0,q_1. If their dot product is negative, replace q1q_1 with −q1-q_1. With Ω=arccos⁡(q0⋅q1)\Omega=\arccos(q_0\cdot q_1) and fraction ss between zero and one, spherical interpolation is

q(s)=sin⁡((1−s)Ω)sin⁡Ωq0+sin⁡(sΩ)sin⁡Ωq1.q(s)=\frac{\sin((1-s)\Omega)}{\sin\Omega}q_0+\frac{\sin(s\Omega)}{\sin\Omega}q_1.

Ω\Omega is the angle between quaternion vectors, half the physical shortest rotation angle. For nearly coincident endpoints, normalized linear interpolation avoids division by a tiny sine. SciPy's SLERP documentation specifies a fixed-axis, constant-angular-speed shortest rotation between each pair of keyframes.

For our yaw-only fixture,

q0=(0.087156,0,0,0.996195),q1=(0.087156,0,0,−0.996195).q_0=(0.087156,0,0,0.996195),\qquad q_1=(0.087156,0,0,-0.996195).

Their dot product is cos⁡170∘=−0.984808\cos170^\circ=-0.984808. Flip q1q_1; the dot becomes 0.9848080.984808 and Ω=10∘\Omega=10^\circ. At s=1/2s=1/2, the scalar parts cancel and the normalized result is (0,0,0,1)(0,0,0,1): a 180-degree yaw. The physical total turn is 2Ω=20∘2\Omega=20^\circ. In the drawing, the short solid arc crosses the left-pointing midpoint. The dashed long arc passes the right-pointing zero heading and represents the unwanted 340-degree route.

What should a controller measure?

For current orientation qcq_c and desired orientation qdq_d in our fixed-frame convention, use qe=qdqc−1q_e=q_dq_c^{-1}. Align its sign so wew_e is nonnegative. Its angle is 2atan2⁡(∥ve∥,we)2\operatorname{atan2}(\lVert\mathbf v_e\rVert,w_e), with axis ve/∥ve∥\mathbf v_e/\lVert\mathbf v_e\rVert away from zero. The angle times that axis is a local three-component error, expressed in the fixed frame for this product order. A body-frame error uses the opposite order.

Quaternions avoid Euler-angle chart singularities but require normalization and sign management. Matrices are often clearer for transforming many vectors. SLERP also supplies no collision check, joint solution or smooth angular acceleration across multiple keyframes; use trajectory generation for timing and derivative constraints. At an exact 180-degree relative rotation, the shortest direction is ambiguous and needs a consistent tie rule.

Code

This dependency-free Python example handles sign equivalence and the near-coincident case. It checks that changing an endpoint's sign doesn't change the physical midpoint.

from math import acos, atan2, cos, degrees, isclose, radians, sin, sqrt
Quat = tuple[float, float, float, float]
def unit(q: Quat) -> Quat:    norm = sqrt(sum(x*x for x in q))    if norm == 0:        raise ValueError("zero quaternion")    return tuple(x / norm for x in q)
def slerp(a: Quat, b: Quat, s: float) -> Quat:    a, b = unit(a), unit(b)    dot = sum(x*y for x, y in zip(a, b))    if dot < 0:        b, dot = tuple(-x for x in b), -dot    dot = min(1.0, max(-1.0, dot))    if dot > 0.9995:        return unit(tuple((1-s)*x + s*y for x, y in zip(a, b)))    angle = acos(dot)    return unit(tuple((sin((1-s)*angle)*x + sin(s*angle)*y) / sin(angle)                      for x, y in zip(a, b)))
def yaw(deg: float) -> Quat:    half = radians(deg) / 2    return cos(half), 0.0, 0.0, sin(half)
a, b = yaw(170), yaw(-170)mid = slerp(a, b, 0.5)same = slerp(a, tuple(-x for x in b), 0.5)assert isclose(abs(sum(x*y for x, y in zip(mid, same))), 1.0)assert isclose(sum(x*x for x in mid), 1.0)assert isclose(abs(slerp(a, a, 0.5)[0]), abs(a[0]))print(f"midpoint yaw: {degrees(2*atan2(mid[3], mid[0])):.1f} deg")

Output: midpoint yaw: 180.0 deg. The yaw extraction here is only for this pure-z rotation, not a general Euler-angle conversion.

Watch Out For

An equivalent sign looks like a huge error

The reported error spikes when a driver changes from qq to −q-q, although the tool stays still. Compare relative rotations and align signs before interpolation. Do not force scalar-positive signs independently throughout a recorded sequence and then interpolate its raw components across the resulting seam.

Four valid numbers describe the wrong convention

A normalized quaternion can still rotate around the wrong axis when scalar-first data enters a scalar-last API, or when a passive frame change is read as an active rotation. Check a known quarter-turn on a known unit vector, composition order and inverse mapping before testing long trajectories.

The Quick Version

  • Unit quaternions represent rotations with four constrained components; opposite signs describe the same orientation.
  • Sign-aligned SLERP takes the 20-degree route from 170 to -170 degrees through 180 degrees.
  • Relative quaternion products give orientation error only after frame and multiplication conventions are fixed.
  • Interpolating orientation does not prove joint reachability, collision clearance or feasible angular acceleration.