Quaternion interpolation¶
Quaternions avoid Euler-angle singularities, but introduce a double cover: q
and -q represent the same orientation. InterpolatePy's trajectory classes
choose consistent signs across keyframes.
Construct and convert orientations¶
import numpy as np
from interpolatepy import Quaternion
identity = Quaternion.identity()
about_z = Quaternion.from_angle_axis(
np.pi / 2.0, np.array([0.0, 0.0, 1.0])
)
from_rpy = Quaternion.from_euler_angles(roll=0.2, pitch=0.3, yaw=0.4)
matrix = from_rpy.to_rotation_matrix()
roll, pitch, yaw = from_rpy.to_euler_angles()
axis, angle = about_z.to_axis_angle()
assert matrix.shape == (3, 3)
assert np.isclose(angle, np.pi / 2.0)
assert np.allclose(axis, [0.0, 0.0, 1.0])
Notice the asymmetric naming: from_angle_axis(angle, axis) accepts angle
first, while to_axis_angle() returns axis first.
Normalize arbitrary components with .unit() before treating them as a
rotation.
SLERP between two orientations¶
import numpy as np
from interpolatepy import Quaternion
start = Quaternion.identity()
end = Quaternion.from_angle_axis(np.pi, np.array([0.0, 0.0, 1.0]))
halfway = start.slerp(end, 0.5)
axis, angle = halfway.to_axis_angle()
assert np.isclose(angle, np.pi / 2.0)
The interpolation parameter is dimensionless and lies between zero and one.
QuaternionSpline keyframes¶
from interpolatepy import Quaternion
from interpolatepy import QuaternionSpline
times = [0.0, 1.0, 2.0, 3.0, 4.0]
orientations = [
Quaternion.identity(),
Quaternion.from_euler_angles(0.1, 0.2, 0.1),
Quaternion.from_euler_angles(0.3, 0.5, 0.4),
Quaternion.from_euler_angles(0.2, 0.7, 0.8),
Quaternion.from_euler_angles(0.0, 0.4, 1.0),
]
spline = QuaternionSpline(times, orientations, interpolation_method="auto")
q = spline.evaluate(2.5)
omega = spline.evaluate_velocity(2.5)
alpha = spline.evaluate_acceleration(2.5)
The available method strings are "slerp", "squad", and "auto". In
SQUAD or automatic mode, boundary segments use SLERP and eligible interior
segments use SQUAD. With too few keyframes for SQUAD, evaluation falls back to
SLERP.
interpolate_at_time() also returns a status code for legacy callers;
evaluate() is the protocol-compatible interface.
SquadC2¶
Use SquadC2 when zero-clamped boundaries and smoother angular derivatives are
important:
import numpy as np
from interpolatepy import Quaternion
from interpolatepy import SquadC2
times = [0.0, 1.0, 2.0, 3.0]
orientations = [
Quaternion.identity(),
Quaternion.from_angle_axis(0.4, np.array([1.0, 0.0, 0.0])),
Quaternion.from_angle_axis(0.8, np.array([0.0, 1.0, 0.0])),
Quaternion.from_angle_axis(1.0, np.array([0.0, 0.0, 1.0])),
]
spline = SquadC2(times, orientations)
t = 1.5
q = spline.evaluate(t)
omega = spline.evaluate_velocity(t)
alpha = spline.evaluate_acceleration(t)
assert omega.shape == (3,)
assert alpha.shape == (3,)
Input quaternions are normalized by default. validate_continuity controls the
implementation's construction-time continuity diagnostics.
Logarithmic quaternion interpolation¶
LQI unwraps keyframes into continuous rotation vectors and interpolates those vectors with a B-spline:
import numpy as np
from interpolatepy import LogQuaternionInterpolation
from interpolatepy import Quaternion
times = [0.0, 2.0]
orientations = [
Quaternion.identity(),
Quaternion.from_angle_axis(1.2, np.array([0.0, 0.0, 1.0])),
]
interpolator = LogQuaternionInterpolation(times, orientations, degree=5)
q = interpolator.evaluate(1.0)
rotation_vector_rate = interpolator.evaluate_velocity(1.0)
assert rotation_vector_rate.shape == (3,)
Degrees 3, 4, and 5 all accept two or more keyframes in 3.2.0.
On the Python backend, distinguish rotation-vector derivatives from physical angular kinematics:
import numpy as np
from interpolatepy import Quaternion
from interpolatepy.log_quat import LogQuaternionInterpolation
times = [0.0, 1.0, 2.0]
orientations = [
Quaternion.identity(),
Quaternion.from_euler_angles(0.2, 0.3, 0.1),
Quaternion.from_euler_angles(0.5, 0.4, 0.7),
]
interpolator = LogQuaternionInterpolation(times, orientations)
omega, alpha = interpolator.get_physical_kinematics(1.0)
assert omega.shape == (3,)
assert alpha.shape == (3,)
get_physical_kinematics() and acceleration boundary arguments are currently
Python-backend helpers. Force the fallback for code that relies on them.
Modified logarithmic interpolation¶
mLQI splines the angle and axis separately. Its internal derivative vector has four components: angle plus three axis components.
import numpy as np
from interpolatepy import ModifiedLogQuaternionInterpolation
from interpolatepy import Quaternion
times = [0.0, 1.0, 2.0]
orientations = [
Quaternion.identity(),
Quaternion.from_angle_axis(0.5, np.array([1.0, 0.0, 0.0])),
Quaternion.from_angle_axis(1.0, np.array([0.0, 1.0, 0.0])),
]
interpolator = ModifiedLogQuaternionInterpolation(
times, orientations, degree=3, normalize_axis=True
)
q = interpolator.evaluate(0.75)
coordinate_rate = interpolator.evaluate_velocity(0.75)
assert coordinate_rate.shape == (4,)
Keep normalize_axis=True unless the interpolated axis is already known to
stay unit length.
Compare orientations correctly¶
Component equality rejects the equivalent pair q and -q. For unit
quaternions, use the absolute dot product:
from interpolatepy import Quaternion
q1 = Quaternion.identity()
q2 = -q1
same_orientation = abs(q1.dot_prod(q2)) > 1.0 - 1e-9
assert same_orientation
Choosing a method¶
| Need | Method |
|---|---|
| One segment | Quaternion.slerp() |
| Simple keyframe API | QuaternionSpline |
| Zero-clamped smooth SQUAD construction | SquadC2 |
| Continuous rotation-vector representation | LQI |
| Separate angle/axis state | mLQI |
Rotations close to 180 degrees are intrinsically branch-sensitive. Inspect the actual orientation path, angular velocity, and angular acceleration for the chosen keyframes rather than selecting solely by method name.