quaternions — scalar-last quaternion algebra

Purpose

Stateless scalar-last [x, y, z, w] quaternion ops for the attitude path: Hamilton product,
attitude error, exponential-map integration, R↔q conversion, and trajectory sign-continuity.
No config, no state — pure math consumed by the controllers and guidance.

Role in the system

  • Stateless support library (utils/), consumed by the GNC stack — not a pipeline module.
  • quat_eps_eta(R) is the load-bearing export: breve_controller and base_controller call it to
    split the base/EE attitude-error rotation into the vector part ε and scalar η for the pointing loop.
  • R_to_quat / quat_to_R back the frame↔quaternion conversions used across geometry and the
    guidance frame builders (base_guidance SLERP, continuity enforcement before differentiation).
  • Cross-validated against scipy.spatial.transform.Rotation in the __main__ smoke (mul, error,
    integrate, R↔q all match to 1e-6).

Inputs / Outputs

  • In: scalar-last quaternions [x,y,z,w], 3×3 rotation matrices, body rates ω, (M,4) quaternion sequences.
  • Out: quaternions, rotation matrices, rotation vectors, and the (ε, η) attitude-error split.

Key functions

  • quat_mul — Hamilton product q ⊗ rutils/quaternions.py:9
  • quat_error — attitude-error quaternion q_des ⊗ q⁻¹ (shorter-arc sign) — utils/quaternions.py:39
  • quat_integrate — advance q by ω·dt via the exponential map; renormalizes — utils/quaternions.py:94
  • R_to_quat / quat_to_R — 4-branch Shepperd matrix↔quaternion conversion — utils/quaternions.py:59 / :45
  • quat_from_axes — quaternion from three orthonormal body-axis columns (trace formula) — utils/quaternions.py:28
  • enforce_quat_continuity_xyzw — kill double-cover sign flips in a (M,4) sequence — utils/quaternions.py:117
  • quat_eps_eta — shorter-arc (ε col, η scalar) split, the controllers’ entry point — utils/quaternions.py:126

Footguns

Everything here is scalar-LAST [x, y, z, w]

The whole file assumes the scipy …Rotation.as_quat() default layout. Mixing in a scalar-first
[w, x, y, z] quaternion silently produces wrong results — no shape error to catch it. (utils/INSIGHTS.md [convention])

quat_error sign encodes the shorter arc

quat_error returns q_err = q_des ⊗ q⁻¹ (conjugate inverse for unit quats). The sign picks the
shorter rotation; anything integrating this error should check q_err[3] ≥ 0 to stay on the short
path — this is exactly what quat_eps_eta does (if q[3] < 0: q = -q). (utils/INSIGHTS.md [footgun])

Enforce continuity before interpolating or differentiating a quaternion trajectory

enforce_quat_continuity_xyzw flips q[i] whenever q[i-1]·q[i] < 0. Run it before any
trajectory SLERP or finite-difference, or the double-cover discontinuity injects a spurious
180°-class jump. (utils/INSIGHTS.md [convention])

quat_from_axes is undefined near a 180° rotation

It uses the Shepperd trace formula directly (not scipy) and divides by 4·qw, which blows up as
trace → −1 (≈180°). For robustness prefer the scipy-backed R_to_quat below it, which branches on
all four cases. (utils/INSIGHTS.md [footgun])

Pseudocode (exponential-map integration)

quat_integrate(q, ω, dt):
    θ = ‖ω‖ · dt
    if ‖ω‖ < eps:  dq ≈ [0.5·ω·dt, 1]          # small-angle fallback
    else:          dq = [axis·sin(θ/2), cos(θ/2)]   # axis = ω/‖ω‖
    return normalize(q ⊗ dq)                    # left-multiply, renormalize → no drift

geometry · breve_controller · base_controller · base_guidance · robot · terminology