Skip to content

Batch Operations

This tutorial covers efficient batch processing of quaternion arrays in Quatham.

Why Batch Operations?

Quatham is designed for high-performance batch processing using Numba JIT compilation. Processing arrays of quaternions is significantly faster than looping:

import numpy as np
from quatham import Quaternion

# Create 1000 random rotations
angles = np.random.uniform(-np.pi, np.pi, size=(1000, 3))

# Batch creation (fast)
quats = Quaternion.from_euler(angles, seq='xyz')

# Batch rotation (fast)
v = np.array([1.0, 0.0, 0.0])
rotated = quats * v  # Shape: (1000, 3)

Creating Batch Quaternions

From Arrays

# From 2D array of components
quats = Quaternion([
    [1.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0],
    [0.0, 0.0, 0.0, 1.0],
])

# From numpy array
arr = np.random.randn(100, 4)
quats = Quaternion(arr)

From Euler Angles

# Batch of Euler angles
angles = np.array([
    [30, 0, 0],
    [0, 45, 0],
    [0, 0, 60],
])
quats = Quaternion.from_euler(angles, seq='xyz', degrees=True)

# Large batch
n = 10000
angles = np.random.uniform(0, 360, size=(n, 3))
quats = Quaternion.from_euler(angles, seq='zyx', degrees=True)

From Axis-Angle

# Multiple axes, single angle
axes = np.random.randn(100, 3)
quats = Quaternion.from_axisangle(axes, np.pi/4)

# Single axis, multiple angles
angles = np.linspace(0, 2*np.pi, 100)
quats = Quaternion.from_axisangle([0, 0, 1], angles)

# Multiple axes, multiple angles (must match)
axes = np.random.randn(50, 3)
angles = np.random.uniform(0, np.pi, 50)
quats = Quaternion.from_axisangle(axes, angles)

Accessing Batch Elements

quats = Quaternion(np.random.randn(100, 4))

# Single quaternion (returns array)
q0 = quats[0]  # Shape: (4,)

# Slice
subset = quats[0:10]  # Shape: (10, 4)

# Boolean indexing
mask = quats.w > 0
filtered = Quaternion(quats.ndarray[mask])

# Convert single element back to Quaternion
q_single = Quaternion(quats[0])

Component Access

All properties work on batches:

quats = Quaternion([
    [1.0, 2.0, 3.0, 4.0],
    [5.0, 6.0, 7.0, 8.0],
])

# Scalar component (w)
scalars = quats.scalar  # Shape: (2,)
w = quats.w  # Same as scalar

# Vector components (x, y, z)
vectors = quats.vector  # Shape: (2, 3)
x = quats.x  # Shape: (2,)
y = quats.y  # Shape: (2,)
z = quats.z  # Shape: (2,)

# Magnitude
mags = quats.magnitude  # Shape: (2,)

Batch Arithmetic

qa = Quaternion(np.random.randn(100, 4))
qb = Quaternion(np.random.randn(100, 4))

# Element-wise operations
q_sum = qa + qb
q_prod = qa * qb
q_diff = qa - qb
q_div = qa / qb

# Broadcasting with single quaternion
q_single = Quaternion([1.0, 0.0, 0.0, 0.0])
result = qa * q_single

# Scalar operations
q_scaled = qa * 2.0
q_norm = qa / qa.magnitude[:, None]

Batch Conversions

All conversion methods support batches:

# Create batch
angles = np.random.uniform(0, 360, size=(100, 3))
quats = Quaternion.from_euler(angles, seq='xyz', degrees=True)

# Batch rotation matrices
R_batch = quats.to_rotation_matrix()  # Shape: (100, 3, 3)
R_flat = quats.get_rotation_matrix_flat()  # Shape: (100, 9)

# Batch axis-angle
axes, thetas = quats.to_axis_angle()  # Shapes: (100, 3), (100,)

# Batch Euler angles
euler = quats.to_euler(seq='zxz', degrees=True)  # Shape: (100, 3)

# Batch yaw-pitch-roll
ypr = quats.to_yaw_pitch_roll(degrees=True)  # Shape: (100, 3)

Batch Vector Rotation

# Create batch of rotation quaternions
n = 1000
quats = Quaternion.from_euler(
    np.random.uniform(0, 360, size=(n, 3)), 
    seq='zyx', 
    degrees=True
)

# Rotate single vector by all quaternions
v = np.array([1.0, 0.0, 0.0])
rotated = quats * v  # Shape: (1000, 3)

# Rotate batch of vectors (one per quaternion)
vectors = np.random.randn(n, 3)
# Note: Currently rotates each vector by corresponding quaternion
# For advanced use, iterate or use broadcasting

Performance Tips

1. Prefer Batch Operations

# Slower: Python loop
results = []
for i in range(len(quats)):
    results.append(quats[i] * v)
results = np.array(results)

# Faster: Batch operation
results = quats * v

2. Pre-allocate When Possible

# If you know the size, create the full array at once
n = 10000
angles = np.zeros((n, 3))
angles[:, 0] = np.linspace(0, 2*np.pi, n)
quats = Quaternion.from_euler(angles, seq='xyz')

3. Minimise Conversions

# Keep data in quaternion form during computations
q1 = Quaternion.from_euler(angles1, seq='xyz')
q2 = Quaternion.from_euler(angles2, seq='xyz')
q_combined = q1 * q2

# Convert only at the end if needed
result_matrix = q_combined.to_rotation_matrix()

4. Use Numpy Operations on Components

# Access components directly for custom operations
w = quats.w
x = quats.x
y = quats.y
z = quats.z

# Vectorised operations
custom_metric = w**2 + x**2

Practical Example: Animation Interpolation

# Create keyframe rotations
keyframes = Quaternion.from_euler([
    [0, 0, 0],
    [0, 90, 0],
    [0, 180, 0],
    [0, 270, 0],
], seq='zyx', degrees=True)

# Generate interpolated frames
n_frames = 100
t = np.linspace(0, 1, n_frames)

# Simple linear interpolation of components (approximate)
interpolated = []
for i in range(len(keyframes) - 1):
    q_start = keyframes[i]
    q_end = keyframes[i + 1]
    # Linear interpolation (for SLERP, use external library)
    t_local = t[i * 25:(i + 1) * 25]
    for ti in t_local:
        q_interp = Quaternion(
            (1 - ti) * q_start.ndarray[0] + ti * q_end.ndarray[0]
        )
        interpolated.append(q_interp.normalise())

# Convert to rotation matrices for rendering
matrices = [q.to_rotation_matrix() for q in interpolated]

Memory Considerations

# Check memory usage
import sys

n = 1000000
quats = Quaternion(np.random.randn(n, 4))

# ndarray is the main data
print(f"Array shape: {quats.ndarray.shape}")
print(f"Array dtype: {quats.ndarray.dtype}")
print(f"Memory: {quats.ndarray.nbytes / 1e6:.1f} MB")

Best Practices

  1. Use batches for 10+ quaternions — amortise JIT overhead
  2. Keep data in array form — avoid converting to lists
  3. Normalise periodically — prevent numerical drift in long chains
  4. Profile your code — identify bottlenecks with timeit or cProfile

Next Steps