Quaternion Arithmetic¶
This tutorial covers arithmetic operations with quaternions in Quatham.
Addition and Subtraction¶
from quatham import Quaternion
import numpy as np
q1 = Quaternion([1.0, 2.0, 3.0, 4.0])
q2 = Quaternion([5.0, 6.0, 7.0, 8.0])
# Addition
q_sum = q1 + q2 # [6.0, 8.0, 10.0, 12.0]
# Subtraction
q_diff = q1 - q2 # [-4.0, -4.0, -4.0, -4.0]
# With arrays
q_arr = q1 + np.array([1.0, 1.0, 1.0, 1.0])
# With lists
q_list = q1 + [1.0, 1.0, 1.0, 1.0]
# Right-hand operations
q_right = [1.0, 1.0, 1.0, 1.0] + q1
Scalar Multiplication¶
q = Quaternion([1.0, 2.0, 3.0, 4.0])
# Multiply by scalar
q_scaled = q * 2.0 # [2.0, 4.0, 6.0, 8.0]
# Right multiplication
q_scaled = 2.0 * q
# Division by scalar
q_div = q / 2.0 # [0.5, 1.0, 1.5, 2.0]
# Scalar divided by quaternion
q_inv_scaled = 5.0 / q # 5 * q^(-1)
Quaternion Multiplication¶
Quaternion multiplication (Hamilton product) combines rotations:
q1 = Quaternion.from_euler([30, 0, 0], seq='xyz', degrees=True)
q2 = Quaternion.from_euler([0, 45, 0], seq='xyz', degrees=True)
# Combine rotations: first q2, then q1
q_combined = q1 * q2
# Note: quaternion multiplication is NOT commutative
assert not np.allclose((q1 * q2).ndarray, (q2 * q1).ndarray)
# But it IS associative
q3 = Quaternion.from_euler([0, 0, 60], seq='xyz', degrees=True)
assert np.allclose(((q1 * q2) * q3).ndarray, (q1 * (q2 * q3)).ndarray)
Hamilton Product Rules¶
The fundamental quaternion units follow:
- i² = j² = k² = ijk = -1
- ij = k, jk = i, ki = j
- ji = -k, kj = -i, ik = -j
# Verify with Quatham
qi = Quaternion([0.0, 1.0, 0.0, 0.0])
qj = Quaternion([0.0, 0.0, 1.0, 0.0])
qk = Quaternion([0.0, 0.0, 0.0, 1.0])
assert np.allclose((qi * qi).ndarray, [-1, 0, 0, 0])
assert np.allclose((qi * qj).ndarray, qk.ndarray)
assert np.allclose((qj * qi).ndarray, (-qk).ndarray)
Quaternion Division¶
q1 = Quaternion([1.0, 2.0, 3.0, 4.0])
q2 = Quaternion([2.0, -1.0, 0.5, 1.5])
# Divide q1 by q2: q1 * q2^(-1)
q_div = q1 / q2
# Verify: (q1 / q2) * q2 = q1
assert np.allclose((q_div * q2).ndarray, q1.ndarray)
# Self-division gives identity
q_identity = q1 / q1
assert np.allclose(q_identity.ndarray, [1.0, 0.0, 0.0, 0.0])
Conjugate and Inverse¶
The conjugate negates the vector part:
q = Quaternion([1.0, 2.0, 3.0, 4.0])
q_conj = q.conjugate # [1.0, -2.0, -3.0, -4.0]
# For unit quaternions: q^(-1) = q*
q_unit = Quaternion([0.5, 0.5, 0.5, 0.5])
q_inv = q_unit.conjugate
# q * q* = |q|^2 (as a scalar quaternion)
product = q * q.conjugate
assert np.allclose(product.ndarray, [[q.magnitude[0]**2, 0, 0, 0]])
Normalisation¶
Normalise quaternions to unit length:
q = Quaternion([2.0, 2.0, 2.0, 2.0])
# Normalise (British spelling)
q_unit = q.normalise()
assert np.isclose(q_unit.magnitude[0], 1.0)
# Normalize (American spelling alias)
q_unit = q.normalize()
# Original is unchanged (non-destructive)
assert np.allclose(q.ndarray, [2.0, 2.0, 2.0, 2.0])
Magnitude and Norm¶
q = Quaternion([1.0, 2.0, 3.0, 6.0])
# Full magnitude
mag = q.magnitude # sqrt(1 + 4 + 9 + 36) = sqrt(50)
# Vector part norm only
vec_norm = q.vector_norm() # sqrt(4 + 9 + 36) = 7.0
Negation¶
q = Quaternion([1.0, 2.0, 3.0, 4.0])
q_neg = -q # [-1.0, -2.0, -3.0, -4.0]
# Note: q and -q represent the same rotation
assert np.allclose(q.to_rotation_matrix(), (-q).to_rotation_matrix())
Batch Arithmetic¶
All operations work on arrays of quaternions:
qa = Quaternion([
[1.0, 2.0, 3.0, 4.0],
[10.0, 20.0, 30.0, 40.0],
])
qb = Quaternion([
[5.0, 6.0, 7.0, 8.0],
[50.0, 60.0, 70.0, 80.0],
])
# Element-wise operations
q_sum = qa + qb
q_prod = qa * qb
# Broadcasting (single with batch)
q_single = Quaternion([1.0, 1.0, 1.0, 1.0])
q_result = qa + q_single
# Scalar operations on batch
q_scaled = qa * 2.0
Practical Example: Combining Rotations¶
# Start with identity orientation
orientation = Quaternion([1.0, 0.0, 0.0, 0.0])
# Apply successive rotations
pitch = Quaternion.from_euler([30, 0, 0], seq='xyz', degrees=True)
yaw = Quaternion.from_euler([0, 45, 0], seq='xyz', degrees=True)
roll = Quaternion.from_euler([0, 0, 15], seq='xyz', degrees=True)
# Combine (order matters!)
orientation = roll * yaw * pitch
# Get final rotation matrix
R = orientation.to_rotation_matrix()
Best Practices¶
- Normalise after operations to prevent numerical drift
- Remember non-commutativity:
q1 * q2 ≠ q2 * q1 - Use conjugate for inverse when working with unit quaternions
- Batch operations are more efficient than loops
Next Steps¶
- Conversions Tutorial — Convert between representations
- Batch Operations Tutorial — Efficient array processing
- API Reference — Complete documentation