Nyimbi Odero

Series 01 · a numbered run · kept in public

X0.2.2

Chapter: Phase 0 — The Bridge · Module 0.2 — Linear Algebra and Geometry for Robotics

Lesson 0.2.2 — Rotation Representations: Euler Angles, Axis-Angle, Quaternions, and Exponential Coordinates

Week 3 · Prerequisites: 0.2.1, 0.1.1–0.1.4.

Prior results used. This lesson uses 0.2.1’s results — SO(3), the 3 degrees of freedom, the RᵀR = I / det R = +1 constraints, the elementary rotations, and the SO(3)-invariant distance θ = arccos((tr(R₁ᵀR₂) − 1)/2) — and 0.1.3’s nn.Linear / parameter-registration model for the learning-parameterization discussion. Each is restated at the point of use.

Hands forward. This lesson hands forward one capability: the learner can convert a rotation between the 3×3 matrix, Euler angles, axis-angle, quaternion, and exponential-coordinate representations, state each representation’s singularity and its gradient-friendliness, and choose the correct representation for a learning, estimation, or interpolation task — the capability that Lesson 0.2.3 (the exponential map and screw theory) and every pose-learning lesson in Phase 2 and Phase 3 assume.

Running case. The same 6-DOF MuJoCo arm from 0.2.1; every example is an instance of a joint’s orientation or the end-effector’s orientation — a rotation about an alternating z / y axis, with joint angles in (−π, π] — and the lesson’s central question is which representation of that rotation is the right one for the task at hand.


§L.0 — Lesson map, objectives, and misconceptions

§L.0.1 — Lesson map

PartContent
AThe parameterization problem: 3 DOF, minimal charts, and what “good” parameterization means
BEuler angles: conventions, composition, and the gimbal-lock singularity
CAxis-angle and quaternions: Rodrigues, Hamilton product, double cover, slerp
DExponential coordinates: so(3), exp/log, gradient-friendliness, the learning choice
EFailure taxonomy, lab synthesis, cheat sheet, exercises, exit test

§L.0.2 — Objectives

By the end of this lesson you will be able to do eight things:

  1. State what it means for a rotation to have 3 degrees of freedom, and explain why any minimal (3-number) parameterization must have at least one singular configuration.
  2. State the 12 Tait-Bryan and 12 proper-Euler conventions, compute a ZYX Euler-angle triple for a given rotation, and identify the gimbal-lock configuration and its Jacobian rank drop.
  3. Apply Rodrigues’ formula to convert axis-angle to a rotation matrix, and derive the quaternion-to-matrix and matrix-to-quaternion conversions.
  4. Compute the Hamilton product of two quaternions, the conjugate, and the rotation a quaternion represents, and state the double cover’s consequences for interpolation and for a loss.
  5. Apply the exponential map exp : so(3) → SO(3) and its inverse log, and state the singularities at ‖ω‖ = 0 and ‖ω‖ = π.
  6. Select the correct representation for a learning, estimation, interpolation, or display task, from a table of storage, singularity, and gradient properties.
  7. Diagnose three silent representation bugs — non-normalized quaternion, Euler interpolation through gimbal lock, and exponential coordinate at θ = π — from their symptoms.
  8. Produce a written “representation audit” of a pose pipeline: which representation is used at each stage, the conversions at the boundaries, and the singular configurations the pipeline could hit.

§L.0.3 — Misconception inventory

Wrong model the learner probably holdsWhy it is attractiveWhere displacedThe correct model in one sentence
M1 — “Euler angles are the natural rotation representation.”They are the one taught first in physics and graphics, and the three angles (roll, pitch, yaw) have intuitive names.§L.2 (Part B)Euler angles are a chart on SO(3), and every chart has at least one singular configuration (gimbal lock); they are the worst choice for interpolation and for learning, and a good choice only for human-readable display.
M2 — “Quaternions are mathematically the same as rotation matrices, just more compact.”They both represent rotations and they convert to each other, and the quaternion’s 4 numbers vs. the matrix’s 9 look like a pure compression.§L.3 (Part C)Quaternions are a double cover of SO(3) — q and −q represent the same rotation — which changes interpolation (slerp must choose the shorter arc), changes loss (a naive quaternion L2 loss has two minima per rotation), and changes composition (the Hamilton product is not the matrix product).
M3 — “Interpolating Euler angles linearly is a smooth rotation interpolation.”Linear interpolation is the default in every plotting library, and for small angles it looks fine.§L.2 and §L.3 (Parts B and C)Linear Euler interpolation passes through non-physical rotations (the orientation rotates about a changing axis, and the angular velocity is not constant), and near gimbal lock it stops rotating; the correct interpolation on SO(3) is slerp (quaternion) or geodesic (exponential coordinate), both of which are constant-angular-velocity.
M4 — “A non-unit quaternion still represents a rotation.”The 4 numbers are the same shape, and a training loss can decrease with a non-unit quaternion.§L.3 and §L.4 (Parts C and D)A quaternion represents a rotation only if ‖q‖ = 1; a non-unit quaternion, converted to a matrix by the standard formula, produces a matrix with scaled entries and possibly wrong determinant — a general linear map, not a rotation — and the fix is to normalize the quaternion before conversion, or to parameterize by so(3) (3 numbers) and exp to a unit quaternion.
M5 — “The exponential coordinate’s singularity at ‖ω‖ = π is not a real problem.”The singularities at ‖ω‖ = 0 are well known (the identity), and π is “far” from most configurations.§L.4 (Part D)At ‖ω‖ = π, the axis is not uniquely determined (both ω and −ω map to the same rotation), and the log map’s gradient is unbounded — a learning or estimation pipeline that operates near π can diverge or oscillate; the fix is to use a two-chart representation (exponential near 0, quaternion near π) or to clip the log’s output.

Each misconception is displaced in the section named, and each is tagged in the concept check that follows it.

Everything is self-contained. Where a prerequisite lesson is used, the result is restated at the point of use.


§L.0.5 — Retrieval warm-up

This is the sixth lesson of the curriculum. The warm-up draws from Lessons 0.2.1, 0.1.3, and 0.1.2 — 0.1.4 is two steps back and is not used, and 0.2.1 is the most recent.

Warm-up 1 (from 0.2.1, the 3-DOF count). A rotation matrix R has 9 entries and satisfies RᵀR = I and det R = +1. State the DOF count and the constraint count. Answer: 9 entries, 6 independent orthogonality constraints (RᵀR is symmetric 3×3, so 6 independent equations), det = +1 selects the rotation component, so 9 − 6 = 3 DOF. How this is used: §L.1’s entire question is “which 3-number chart on SO(3) should I use?”, and the 3-DOF count is the reason a minimal parameterization needs 3 numbers and cannot be globally singularity-free (a chart from a 3-dimensional manifold into ℝ³ must have at least one singular point unless the manifold is ℝ³ itself, which SO(3) is not).

Warm-up 2 (from 0.2.1, the elementary rotations). Write Rz(π/2), Ry(π/2), and Rx(π/2), and state which axis each fixes. Answer: Rz(π/2) = [[0, −1, 0], [1, 0, 0], [0, 0, 1]] (fixes z), Ry(π/2) = [[0, 0, 1], [0, 1, 0], [−1, 0, 0]] (fixes y), Rx(π/2) = [[1, 0, 0], [0, 0, −1], [0, 1, 0]] (fixes x). How this is used: §L.2’s Euler-angle conventions are products of these three elementary rotations, and the order of the product is the convention’s name (e.g., ZYX means Rz · Ry · Rx), which is the 0.2.1 non-commutativity applied to the three axes.

Warm-up 3 (from 0.1.3, nn.Linear weight and registration). A network’s final layer is nn.Linear(64, 9) and the 9 outputs are reshaped to a 3×3 matrix. Is this matrix a valid rotation? If not, what is the minimum change that makes it one? Answer: No — the 9 outputs are unconstrained, so the matrix is a general linear map that stretches and possibly reflects (it almost surely does not satisfy RᵀR = I or det R = +1). The minimum change is to output 3 numbers (a minimal parameterization) and convert them to R via exp or a quaternion-to-matrix formula; the nn.Linear’s last layer becomes nn.Linear(64, 3), and the conversion is a non-learnable operation in the forward pass. How this is used: §L.4’s “the learning choice” is exactly this change, and the lesson’s central practical outcome is the reasoning behind it.

Warm-up 4 (from 0.1.2, autograd through a non-linear conversion). A exp conversion from ω ∈ ℝ³ to R ∈ SO(3) is a non-linear function. Does autograd compute the gradient of a loss through it? Answer: Yes — exp is composed of differentiable operations (sin, cos, matmul, add), and autograd’s chain rule works through them; the gradient is well-defined except at the singularities (‖ω‖ = 0, ‖ω‖ = π), where the Jacobian of exp is singular but the gradient of a scalar loss with respect to ω can still be computed (it may be zero or unbounded depending on the loss). How this is used: §L.4’s “gradient-friendliness” discussion, and the singularities’ role in a learning pipeline.


Part A — The Parameterization Problem

§L.1 — Objects: what a parameterization must do, and what it cannot do

A parameterization of SO(3) is a map f : Θ → SO(3) from a parameter space Θ ⊆ ℝⁿ onto SO(3). The parameter space’s dimension n and the map’s properties determine the parameterization’s character:

  • A minimal parameterization has n = 3. Every minimal parameterization of SO(3) is a chart — a smooth bijection between an open subset of ℝ³ and an open subset of SO(3) — and every chart must have at least one singular configuration where the chart’s derivative is not full rank. This is a topological fact: SO(3) is not homeomorphic to ℝ³ (it is not simply connected — its fundamental group is ℤ₂, the quaternions’ double cover), so no single 3-number chart can cover it without a singularity.
  • A non-minimal parameterization has n > 3. The 3×3 matrix (9 numbers, 6 constraints) and the quaternion (4 numbers, 1 constraint) are non-minimal; their extra numbers are constrained, and the constraint is either imposed (by normalizing or projecting after each update) or ignored (with the consequence that the output is not exactly on SO(3)).

The four desiderata of a parameterization, in order of importance for robot learning:

  1. Global or nearly global coverage — the chart covers as much of SO(3) as possible without singularities, or the singularities are in configurations the task does not visit.
  2. Smoothness of the conversion — f and f⁻¹ are differentiable, with bounded derivatives near the configurations the task visits, so a gradient step on the parameters is a smooth step on the rotation.
  3. Composition and interpolation — there is a natural way to compose two parameterizations and to interpolate between them at constant angular velocity.
  4. Storage and human readability — the parameters are compact and, for a human, interpretable.

No single parameterization is best on all four; the choice is a trade-off, and the trade-off is the content of Parts B–D.

§L.1.1 — Result: minimal parameterizations must have a singularity, and the singularity is the source of learning and interpolation bugs

Claim. Every minimal (3-number) parameterization of SO(3) has at least one configuration at which the parameterization’s Jacobian is not full rank, and the singularity cannot be removed by a cleverer choice of chart.

Proof sketch. SO(3) is a compact, 3-dimensional manifold with fundamental group ℤ₂ (the quaternions SU(2) cover it 2-to-1). A global chart would be a homeomorphism between SO(3) and an open subset of ℝ³, which would make SO(3) homeomorphic to a subset of ℝ³, which would make its fundamental group trivial (subsets of ℝ³ are simply connected). Since π₁(SO(3)) = ℤ₂ ≠ 0, no global chart exists, and every chart must miss at least one configuration. At a missed configuration, the chart’s derivative must be singular, because the chart cannot be smoothly extended to it.

Corollary. A minimal parameterization’s singularity is structural, not an implementation bug, and the only ways to avoid it are (i) a non-minimal parameterization (quaternion, matrix) that covers SO(3) without singularities, or (ii) a two-chart approach that switches charts near the singularity. The quaternion is the standard non-minimal choice, and its double cover is what allows it to cover SO(3) without a singularity — at the cost of the q vs. −q ambiguity.

Corollary. A learning pipeline that uses a minimal parameterization must (i) avoid the singular configuration, or (ii) accept a bounded gradient near it, or (iii) use a non-minimal representation and project back to SO(3) after each step. The three are the design choices, and they are the content of §L.4’s “the learning choice.”

§L.1.2 — Worked-example ladder

WE-L.1.a — Fully worked: the 9-parameter matrix as the naive choice, and the constraint violation

Input. A network’s final layer outputs m = [m₁, …, m₉] and the code reshapes m to M = [[m₁, m₂, m₃], [m₄, m₅, m₆], [m₇, m₈, m₉]]. The training has converged and m happens to be approximately a rotation.

Trace.

  1. MᵀM ≈ I (the training has driven the network toward rotations) but the off-diagonal is not exactly zero (e.g., MᵀM[0, 1] ≈ 0.01).
  2. det M ≈ 1 (the training has driven the determinant toward 1) but not exactly.
  3. The map p ↦ Mp preserves lengths approximately but not exactly: ‖M p‖ ≈ (1 + ε)‖p‖ with ε the constraint violation.
  4. On a held-out point, ‖M p‖ can differ from ‖p‖ by a few percent, and the “rotation” is a stretch by up to a few percent.

Output and interpretation. The 9-parameter parameterization has 6 redundant directions (the constraints), and the training does not impose the constraints; it encourages them via the loss, and the encouragement is approximate. The output is a near-rotation, not a rotation, and the near-rotation stretches the object. The fix is either to add a constraint penalty (which is approximate) or to parameterize the rotation with 3 numbers and convert (which is exact).

Self-explanation 1. Before reading on: in step 1, why does the training drive M toward a rotation at all, if the loss does not include the constraints?

Answer. Because the loss is a function of the poses the network predicts, and the ground-truth poses are rotations. The network can fit the ground-truth rotations with a matrix that is close to a rotation, and the “close to” is the best fit; the loss’s gradient pushes M toward the ground-truth rotation’s entries, and the constraint violation is a by-product of the fit’s approximation. The fix is to impose the constraints structurally (parameterize 3 numbers and convert), not to add a loss term (which only encourages them).

WE-L.1.b — Completion problem

Input. Same as WE-L.1.a, but the network’s last layer is changed to nn.Linear(64, 3) and the 3 outputs ω are converted to R = exp(ω̂).

Trace (final 25% as prompts).

  1. nn.Linear(64, 3) — the output is ω ∈ ℝ³, 3 unconstrained numbers.
  2. exp(ω̂) — the conversion produces R ∈ SO(3) by construction (the exp map is into SO(3), not onto a superset).
  3. MᵀM for the resulting R — ⟨your turn: is MᵀM = I exactly? Why?⟩
  4. det R for the resulting R — ⟨your turn: is det R = +1 exactly? Why?⟩
  5. The gradient of a loss L(R) with respect to ω — ⟨your turn: is the gradient well-defined? At what ω is it singular?⟩

Answers.

  1. RᵀR = I exactly (up to floating-point), because exp’s output is in SO(3) by construction — the group structure is preserved, and the constraint is structural, not approximate.
  2. det R = +1 exactly (up to floating-point), for the same reason.
  3. The gradient is well-defined by autograd’s chain rule, but the Jacobian of exp is singular at ‖ω‖ = 0 (the identity) and at ‖ω‖ = π (the half-turn, where the axis is ambiguous); at those points, the gradient’s direction is undefined even though its value (from autograd) is defined.

The lesson. The 3-parameter exp conversion is structurally exact — the output is a rotation, and the constraints are satisfied by construction, not by training. The singularities at ‖ω‖ = 0 and ‖ω‖ = π are the price, and they are the subject of §L.4.

WE-L.1.c — Faded: the quaternion as the non-minimal alternative

Setup. The same network, but with nn.Linear(64, 4) and a normalize step: q = q_raw / ‖q_raw‖, then R = quat_to_R(q).

Question. Is RᵀR = I exactly? Is det R = +1 exactly? What is the quaternion’s singularity?

You must produce the intermediate steps. The setup and the answer are given.

Answer. RᵀR = I exactly (up to floating-point) and det R = +1 exactly, because the normalization makes ‖q‖ = 1 and the quaternion-to-matrix formula’s output is in SO(3) for any unit quaternion. The quaternion’s singularity is the double cover — q and −q represent the same rotation — which is not a singularity in the chart (both are valid unit quaternions) but a two-to-one map, and the “singularity” is that a loss in the quaternion space can have two minima for the same rotation (one at q, one at −q), and a gradient step can flip between them.

The lesson. The quaternion has no chart singularity (it covers SO(3) without a missed configuration), but its double cover is a different kind of ambiguity — not a singularity in the chart, but a redundancy in the representation. The 3-parameter exp has a chart singularity but no double cover; the trade-off is the choice between the two.

WE-L.1.d — Contrastive near-miss: the L2 loss on the 9-parameter matrix

Correct solution. A loss on the SO(3)-invariant distance — θ² where θ = arccos((tr(RᵀR_gt) − 1)/2) — which is zero at R = R_gt, depends only on the relative rotation, and grows at a rate proportional to the error.

Near-miss. A loss on the matrix entries — ‖R − R_gt‖_F² — which is also zero at R = R_gt and, restricted to SO(3), is also invariant: ‖R − R_gt‖_F² = 6 − 2tr(RᵀR_gt) = 8sin²(θ/2), a function of θ alone. The near-miss is not a different ranking; it is a different shape. It saturates: bounded by 8 at θ = π, with gradient d/dθ = 4sin θ that vanishes at the half-turn — so the largest errors are the ones the loss can least see. And it is not θ²: at θ = 1.0 rad it reads 1.84 against 1.00, and at θ = 3.0 it reads 7.96 against 9.00, so the gradient it hands back is wrong by a factor that itself varies with the error.

Why the near-miss produces a plausible but incorrect number. Both losses decrease as R → R_gt, so training on either converges and the final R looks right; the entry loss’s plot is monotone and smooth. What differs is the gradient, and it differs most where the error is largest: d/dθ of the entry loss is 4sin θ, which is ≈ 4θ near the minimum (twice the geodesic’s 2θ) and falls to zero at θ = π, so a network that is badly wrong receives almost no correction while a network that is nearly right is over-corrected. The failure is not a wrong minimum; it is a wrongly shaped basin, which shows up as slow or stalled convergence from bad initializations rather than as a wrong answer.

The discrepancy, stated. ‖R − R_gt‖_F² = 6 − 2tr(RᵀR_gt) = 4(1 − cos θ) = 8sin²(θ/2), a linear function of the trace; θ² = arccos((tr(RᵀR_gt) − 1)/2)², the square of the arccos of a linear function. They do not agree even near R = R_gt: the entry loss is ≈ 2θ² there, twice the geodesic’s θ², so every experiment run close to the answer sees a loss that is exactly double and a gradient (4θ against 2θ) that is exactly double. A comparison that only checks “both vanish at R_gt” therefore confirms the minimum and misses the factor of two. They diverge further at large θ: the entry loss saturates at 8 where θ² reaches π² ≈ 9.87, and the value 12 that ‖M − R_gt‖_F² attains at M = −I is outside SO(3) (det(−I) = −1) and so is not a bound on any rotation loss at all.

Self-explanation 2. Before reading on: since ‖R − R_gt‖_F² is invariant on SO(3) and vanishes exactly at R_gt, why is it still the wrong loss?

Answer. Invariance and a correct minimum are necessary but not sufficient. The loss must also be a monotone function whose gradient is usable across the whole range, and 8sin²(θ/2) fails the second half: its derivative 4sin θ is zero at θ = π, so the half-turn — the worst possible error — is a stationary point of the loss. A network initialized near θ = π sees a flat surface and stays there; θ² has derivative 2θ, largest exactly at the worst error. The general rule: two losses that agree at the minimum and differ elsewhere are distinguished by their gradients away from the minimum, so a comparison run only near R_gt tests nothing.

§L.1.3 — Comparative table: what each parameterization provides

ParameterizationStorageDOFChart singularityDouble coverGradient-friendlinessBest for
3×3 matrix93 (6 constraints)NoneNoConstraint is approximateComputation, conversion
Euler angles33Gimbal lockNoPoor (chart-dependent)Display, human-readable
Axis-angle43‖axis‖ = 1 constraint, angle = 0NoMediumInterpretation
Quaternion43 (‖q‖ = 1)NoneYes (q ≡ −q)Good (after normalization)Learning, interpolation
Exponential coordinate33‖ω‖ = 0, ‖ω‖ = πNoGood near 0, poor near πLearning, optimization

Design principle (P6.1). Choose the representation by the task, not by habit. Display → Euler. Storage/computation → matrix (or quaternion). Interpolation → quaternion slerp or exponential geodesic. Learning → exponential coordinate (3, no double cover) with a two-chart fallback near π, or quaternion with normalization. The four tasks have different needs, and the pipeline converts between them at the boundaries.

§L.1.4 — Concept check

Concept check (displaces M1, M4 in part). A learner says: “I’ll use Euler angles for the network’s output, because 3 numbers is minimal and I avoid the quaternion’s normalization.” What are the two problems with this choice?

Answer. First, Euler angles have a gimbal-lock singularity — a configuration where two axes align and the chart’s Jacobian is singular — and a learning pipeline that crosses or approaches the singularity has an unbounded or ill-defined gradient. Second, Euler angles are chart-dependent — different conventions (ZYX vs. XYZ vs. ZYZ) give different triples for the same rotation, so the loss and the gradient depend on the convention in a way the rotation does not — and interpolation in Euler space is not a constant-angular-velocity rotation. The fix is to use the exponential coordinate (3 numbers, no normalization, a singularity at π that is far from most configurations) or a quaternion with normalization, and to convert to Euler only for display.

§L.1.5 — Consolidation checkpoint 1

(a) You should now be able to: (i) state the 3-DOF count and the topological reason a minimal chart must have a singularity; (ii) state the four desiderata and the trade-offs between them; (iii) classify the five representations by storage, singularity, double cover, and gradient-friendliness; (iv) state why the 9-parameter matrix is the wrong learnable choice and why exp of 3 numbers is the right one.

(b) Transfer probe. A learner’s network outputs a quaternion q and trains it with the loss ‖q − q_gt‖_F² where q_gt is the ground-truth quaternion. Does the loss have a unique minimum? Answer sketch: No — q and −q represent the same rotation, so if q_gt is the ground truth, both q_gt and −q_gt have loss zero (both are “correct”), and the loss has two minima. A gradient step near the boundary between the two basins can oscillate (the q → −q flip), and the learner sees a loss that jumps when the quaternion flips sign. The fix is to use a sign-invariant loss (min(‖q − q_gt‖², ‖q + q_gt‖²)) or to use the rotation’s geodesic distance θ² instead of the quaternion’s entries.

(c) Remediation rule. If you cannot do (b), return to §L.1.2 and redo WE-L.1.c, then re-attempt (b) by asking what q and −q represent.


Part B — Euler Angles and Gimbal Lock

§L.2 — Objects: the 24 conventions and the singularity

Euler angles are a triple (α, β, γ) and a convention — a specification of which three axes the rotations are about, and in what order. There are 24 conventions:

  • 12 Tait-Bryan conventions: three distinct axes, e.g., ZYX, ZXY, YXZ, YZX, XYZ, XZY, and the six with the axes reversed. Each is a product of three elementary rotations about different axes.
  • 12 proper-Euler conventions: the first and third axes are the same, e.g., ZXZ, ZYZ, XYX, XZX, YXY, YZY, and the six with the axes’ signs reversed. Each is a product R_axis1(α) · R_axis2(β) · R_axis1(γ).

The convention’s name is the order of the product: ZYX means R = Rz(α) · Ry(β) · Rx(γ), with the z rotation applied first in the product’s right-to-left reading (which is the last in the frame’s construction, or the first depending on the convention — and the ambiguity is why the convention’s name is essential).

The gimbal-lock singularity is the configuration where the second rotation’s axis aligns with the first and third (for a proper-Euler convention) or where the first and third rotations become dependent (for a Tait-Bryan convention). For a ZYX Tait-Bryan convention, gimbal lock occurs at β = ±π/2:

  • At β = π/2, Ry(π/2) maps the x-axis to −z, so Rz(α) · Ry(π/2) · Rx(γ) = Rz(α) · Ry(π/2) · Rz(−γ') for some γ' — the first and third rotations are both about z, and only their sum (or difference) is determined, not the two individually. The chart is singular: two angles are not independent.
  • The Jacobian of the map (α, β, γ) ↦ R drops rank at β = ±π/2, and the inverse map (from R to (α, β, γ)) is not unique — there is a one-parameter family of triples that produce the same R.

§L.2.1 — Result: gimbal lock is a rank drop of the Euler chart’s Jacobian, and interpolation through it is non-physical

Claim. For the ZYX convention, the Jacobian of (α, β, γ) ↦ Rz(α)Ry(β)Rx(γ) drops rank at β = ±π/2, and a linear interpolation in (α, β, γ) that crosses β = ±π/2 produces a non-physical rotation — the angular velocity is not constant, and the axis of rotation swings through a large arc. The velocity itself stays continuous; what changes across the crossing is which rotation the path is taking, not how fast it takes it.

Proof sketch. The Jacobian’s rank is the number of independent infinitesimal rotations the three angles generate at the configuration. At β = ±π/2, the first and third rotations’ axes align, so the three angles generate only two independent infinitesimal rotations; the Jacobian’s rank drops from 3 to 2. The linear interpolation (1−t)(α₀, β₀, γ₀) + t(α₁, β₁, γ₁) produces a path that crosses β = π/2 at some t. Because the angle triple is a smooth function of t, its image under the Euler chart is smooth too, and the body angular velocity dR/dt · Rᵀ inherits that smoothness — the rank drop shows up in the map from angle-rates to angular velocity, not as a jump in the velocity. What it does produce is a path whose instantaneous axis rotates rapidly relative to the net rotation: for the lab’s endpoints, 8.10° apart, the axis sweeps 166°.

Corollary. Euler-angle interpolation is not a rotation interpolation; the correct rotation interpolation is slerp (quaternion, constant angular velocity) or geodesic (exponential coordinate, constant angular velocity), and the difference is visible for large angles or for paths near gimbal lock. A pipeline that interpolates Euler angles and a policy that outputs Euler angles are both vulnerable to the singularity, and the fix is to use quaternion or exponential coordinates for the trajectory and to convert to Euler only for display.

Corollary. The gimbal-lock configuration is not exotic — β = π/2 is a 90° pitch, which is a common orientation in manipulation (e.g., pointing a gripper straight down). A ZYX Euler representation of a downward-pointing gripper is at or near the singularity, and a learning or estimation pipeline that uses ZYX will have an ill-conditioned gradient at that orientation.

§L.2.2 — Worked-example ladder

WE-L.2.a — Fully worked: ZYX Euler angles for a specific rotation, by hand

Input. R = Rz(π/4) · Ry(π/6) · Rx(π/3) (a ZYX triple (π/4, π/6, π/3)).

Trace (compute the matrix).

  1. Rz(π/4) = [[√2/2, −√2/2, 0], [√2/2, √2/2, 0], [0, 0, 1]] ≈ [[0.707, −0.707, 0], [0.707, 0.707, 0], [0, 0, 1]].
  2. Ry(π/6) = [[√3/2, 0, 1/2], [0, 1, 0], [−1/2, 0, √3/2]] ≈ [[0.866, 0, 0.5], [0, 1, 0], [−0.5, 0, 0.866]].
  3. Rx(π/3) = [[1, 0, 0], [0, 1/2, −√3/2], [0, √3/2, 1/2]] ≈ [[1, 0, 0], [0, 0.5, −0.866], [0, 0.866, 0.5]].
  4. Rz·Ry first: Rz·Ry ≈ [[0.707·0.866 + (−0.707)·0, 0.707·0 + (−0.707)·1, 0.707·0.5 + (−0.707)·0], ...] = [[0.612, −0.707, 0.354], [0.612, 0.707, 0.354], [−0.5, 0, 0.866]] (approximate).
  5. Rz·Ry·Rx: the full product, with each entry computed from the ZYX formula (the standard ZYX Euler-to-matrix formula is R = [[cαcβ, cαsβsγ − sαcγ, cαsβcγ + sαsγ], [sαcβ, sαsβsγ + cαcγ, sαsβcγ − cαsγ], [−sβ, cβsγ, cβcγ]]).

Trace (the singularity at β = π/2).

  1. At β = π/2, cβ = 0, sβ = 1, so R[0, 0] = cα·0 = 0 and R[1, 0] = sα·0 = 0; the first column is [0, 0, −1], independent of α and γ.
  2. The α and γ dependence is only through sβsγ − sαcγ = sγ − sαcγ and sβcγ + sαsγ = cγ + sαsγ, which are functions of α ± γ (the sum or difference), not of the two individually.

Output and interpretation. At β = π/2, the first column of R is [0, 0, −1] for every α and γ; the remaining columns depend on α and γ only through their combination, so the chart is singular (two parameters determine one degree of freedom). The rotation is still well-defined — the R matrix is a valid rotation — but the triple (α, π/2, γ) is not unique for the rotation: many triples give the same R.

Self-explanation 3. Before reading on: at β = π/2, what is the relationship between α and γ that gives the same R?

Answer. The R depends on α and γ only through α − γ (or α + γ, depending on the convention’s signs), so two triples (α₁, π/2, γ₁) and (α₂, π/2, γ₂) give the same R if α₁ − γ₁ = α₂ − γ₂. The difference α − γ is the only free combination, and the two angles are not independently recoverable from R. The fix is to choose one of them (e.g., set γ = 0) and recover the other, which is what the inverse map does at the singularity.

WE-L.2.b — Completion problem

Input. R = Rz(π/2) · Ry(π/2) · Rx(0) (a ZYX triple at the singularity).

Trace (final 25% as prompts).

  1. Ry(π/2) = [[0, 0, 1], [0, 1, 0], [−1, 0, 0]].
  2. Rz(π/2) · Ry(π/2) = [[0, −1, 0], [1, 0, 0], [0, 0, 1]] · [[0, 0, 1], [0, 1, 0], [−1, 0, 0]] = [[0, −1, 0], [0, 0, 1], [−1, 0, 0]]. ⟨your turn: verify the product⟩
  3. · Rx(0) = the same (the identity’s product). ⟨your turn: is the result dependent on α and γ independently, or only on α − γ?⟩
  4. A different triple (π/4, π/2, −π/4) gives α − γ = π/4 − (−π/4) = π/2. ⟨your turn: does it give the same R as (π/2, π/2, 0)?⟩

Answers.

  1. The product is [[0, −1, 0], [0, 0, 1], [−1, 0, 0]], and each entry is independent of α and γ because at β = π/2 the cβ = 0 and sβ = 1 collapse the formula.
  2. The result depends only on α − γ (in this convention), not on the two individually.
  3. Yes, (π/4, π/2, −π/4) gives the same R as (π/2, π/2, 0), because both have α − γ = π/2. The two triples are different but represent the same rotation — the non-uniqueness of the Euler representation at the singularity.

The lesson. At gimbal lock, the Euler triple is not unique for the rotation, and the inverse map must choose one representative. The non-uniqueness is the symptom of the singularity, and the Jacobian’s rank drop is the mechanism.

WE-L.2.c — Faded: the XYZ convention and the singularity’s location

Setup. The same rotation R = Rz(π/4) · Ry(π/6) · Rx(π/3), but expressed in the XYZ convention (R = Rx(γ) · Ry(β) · Rz(α)).

Question. What is the XYZ triple for this R? At what β does the XYZ convention have its singularity?

You must produce the intermediate steps. The setup and the answer are given.

Answer. The XYZ triple is not the same as the ZYX triple — the conventions are different charts, and the same rotation has different triples in different conventions. The XYZ triple can be computed by solving the XYZ Euler-to-matrix formula for (γ, β, α), which gives different values. The XYZ singularity is at β = ±π/2 as well — every Tait-Bryan convention has its singularity at the middle angle = ±π/2 — but the axis at which the two rotations align is the x-axis (for XYZ), not the z-axis (for ZYX).

The lesson. The singularity’s location is β = ±π/2 for every Tait-Bryan convention, and the axis of the singularity depends on the convention’s first and third axes. The convention’s choice determines which orientations are near the singularity, and a task that visits β ≈ ±π/2 (e.g., a downward-pointing gripper in ZYX) is near the singularity regardless of the convention, so the representation (quaternion or exponential) is the fix, not the convention.

WE-L.2.d — Contrastive near-miss: linear Euler interpolation

Correct solution. The learner interpolates two orientations R₀ and R₁ by converting each to a quaternion and using slerp, which gives a constant-angular-velocity path.

Near-miss. The learner converts each to a ZYX Euler triple and linearly interpolates the three angles. The path is a smooth curve in Euler space, but the rotation it represents is not a constant-angular-velocity path; near β = ±π/2, the rotation’s axis can swing through a large arc while the endpoints are close together.

Why the near-miss produces a plausible but incorrect number. The Euler interpolation is smooth in Euler space — the three angles move linearly — so the plotted path looks smooth and the intermediate rotations look plausible. Two things are wrong with it, and only one is visible. The speed varies: in the lab’s own example it dips by 21.3% mid-path (0.254 at the ends, 0.200 at the crossing), which reads as a slightly uneven trajectory and nothing worse. The axis is the real defect: for endpoints 8.10° apart, the instantaneous rotation axis swings through 166° — the arm turns the right way, but along the way it rotates about an axis that sweeps almost a half-circle rather than the single axis the shortest rotation uses. A learner watching for a stutter will see a smooth, plausible motion and conclude the interpolation is fine.

The discrepancy, stated. The Euler interpolation’s path is a straight line in ℝ³ (the three angles), and the rotation’s path is the image of that line under the Euler chart. The image is not a geodesic on SO(3) unless the line is in a specific direction, and near the chart’s singularity the image bends away from the geodesic by a large fraction of its own length. The slerp path is a geodesic on SO(3) (constant angular velocity, fixed axis), and its derivative is bounded everywhere.

Self-explanation 4. Before reading on: in the near-miss, at β = π/2, what happens to the angular velocity as the Euler interpolation crosses the singularity — and what happens to the axis?

Answer. At β = π/2, the chart’s rank drops from 3 to 2, so the three angles generate only two independent angular velocities. The linear path continues through the singularity without any discontinuity — the angular velocity’s magnitude is continuous, and merely dips (measured: 0.254 at the ends, 0.200 at the crossing). What goes wrong is not continuity but direction: the instantaneous rotation axis is a smooth function of t, and over the path it sweeps through 166° while the endpoints are 8.10° apart — the axis turns through almost a half-circle to accomplish a rotation of a few degrees. So the correct statement is not “the velocity jumps” but “the velocity is smooth and the path is wrong” — the rotation takes a long way round. That distinction matters because a stutter is visible and a detour is not: the stutter test passes on a broken interpolation.

§L.2.3 — Comparative table: Euler conventions and their singularities

Convention typeNumberSingularityAxes affectedCommon use
Tait-Bryan (ZYX, XYZ, …)12β = ±π/2First and thirdAerospace (yaw-pitch-roll), robotics display
Proper Euler (ZXZ, ZYZ, …)12β = 0 or πFirst and third (the same axis)Orbital mechanics, some graphics
Quaternion (no Euler)—None (double cover)—Learning, interpolation
Exponential coordinate—‖ω‖ = 0, π—Learning, optimization

Design principle (P6.2). Never interpolate in Euler space. Euler angles are a display representation; the trajectory should be interpolated in the quaternion or exponential-coordinate space (both give constant-angular-velocity paths), and the Euler angles should be computed from the interpolated rotation for display only. The pipeline’s internal representation is quaternion or exponential, and Euler is a terminal conversion.

§L.2.4 — Concept check

Concept check (displaces M3). A learner says: “Linear Euler interpolation is fine for small angles; the singularity only matters for large rotations.” Is the statement true, and what is the correct statement?

Answer. The statement is false in general. Linear Euler interpolation is approximately a rotation interpolation for small angles and configurations far from the singularity, but (i) the approximation error grows with the angle, and (ii) if the path crosses β = ±π/2 (even for a small interpolation angle, if the endpoints are near the singularity), the path detours far from the geodesic joining its endpoints and the rotation axis swings through a large arc. The velocity stays continuous — so the failure is silent to any test that watches for a stutter. The correct statement is: “Linear Euler interpolation is a smooth path in Euler space, not a rotation interpolation on SO(3); it is acceptable only for small angles and far from the singularity, and the correct interpolation is slerp or geodesic.”

§L.2.5 — Consolidation checkpoint 2

(a) You should now be able to: (i) state the 24 Euler conventions and the difference between Tait-Bryan and proper-Euler; (ii) compute a ZYX triple’s matrix and identify the singularity at β = ±π/2; (iii) state the rank drop and the non-uniqueness at the singularity; (iv) state why Euler interpolation is not a rotation interpolation and what the correct interpolation is.

(b) Transfer probe. A learner’s ZYX Euler representation of a downward-pointing gripper (β = π/2) is near the singularity. They change to XYZ and find the same problem. Why? Answer sketch: every Tait-Bryan convention has its singularity at β = ±π/2, and a downward-pointing gripper’s β (the middle angle’s magnitude) is π/2 in every Tait-Bryan convention (the middle angle is the angle between the first and third axes’ directions, and a downward gripper’s z-axis is antiparallel to the world z, which is the β = π/2 configuration for any convention that uses z as the first or third axis). The fix is not to change the convention but to change the representation (quaternion or exponential), which has no gimbal lock.

(c) Remediation rule. If you cannot do (b), return to §L.2.2 and redo WE-L.2.b, then re-attempt (b) by asking what the middle angle’s value is for a downward-pointing gripper in any Tait-Bryan convention.


Part C — Axis-Angle and Quaternions

§L.3 — Objects: axis-angle, Rodrigues’ formula, and the Hamilton product

Axis-angle represents a rotation as (u, θ) with u ∈ ℝ³, ‖u‖ = 1 (the axis), and θ ∈ [0, π] (the angle). The rotation matrix is given by Rodrigues’ formula:

R(u,θ)=I+sin⁡θ u^+(1−cos⁡θ) u^2R(u, \theta) = I + \sin\theta \, \hat{u} + (1 - \cos\theta) \, \hat{u}^2

where \hat{u} is the skew-symmetric matrix of u:

u^=[0−uzuyuz0−ux−uyux0]\hat{u} = \begin{bmatrix} 0 & -u_z & u_y \\ u_z & 0 & -u_x \\ -u_y & u_x & 0 \end{bmatrix}

so that \hat{u} v = u × v for any v (the cross product).

Quaternions represent a rotation as a unit quaternion q = (w, x, y, z) = w + xi + yj + zk with ‖q‖² = w² + x² + y² + z² = 1. The rotation a quaternion represents is (u, θ) with u = (x, y, z)/‖(x, y, z)‖ and θ = 2 arccos(w). The quaternion-to-matrix formula is:

R(q)=I+2wv^+2v^2,v=(x,y,z)R(q) = I + 2w \hat{v} + 2 \hat{v}^2, \quad v = (x, y, z)

The Hamilton product (quaternion composition) is:

q1⊗q2=(w1w2−v1⋅v2,  w1v2+w2v1+v1×v2)q_1 \otimes q_2 = (w_1 w_2 - v_1 \cdot v_2, \; w_1 v_2 + w_2 v_1 + v_1 \times v_2)

where v = (x, y, z). The composition corresponds to matrix multiplication of the rotations: R$q_1 \otimes q_2$ = $R(q_1$) $R(q_2$).

The conjugate is q* = (w, -x, -y, -z), and for a unit quaternion, q* = q⁻¹ (the inverse). The double cover: q and −q represent the same rotation (both have the same (u, θ) with θ negated, which is the same rotation because R(u, θ) = R(−u, 2π − θ) = R(u, θ) for θ ∈ [0, π]).

§L.3.1 — Result: quaternions are a double cover, and the double cover’s consequences are interpolation and loss

Claim. The map q ↦ R(q) from unit quaternions to SO(3) is two-to-one: R(q) = R(−q) for every unit quaternion q, and every rotation has exactly two unit-quaternion preimages.

Proof sketch. The quaternion-to-matrix formula R(q) = I + 2w\hat{v} + 2\hat{v}^2 depends on q through w and v, and replacing q with −q replaces (w, v) with (−w, −v), so R(−q) = I + 2(−w)$−\hat{v}$ + 2$−\hat{v}$^2 = I + 2w\hat{v} + 2\hat{v}^2 = R(q). So the map is at most two-to-one; it is exactly two-to-one because q and −q are the only preimages (the map is a covering map of degree 2, SU(2) → SO(3)).

Corollary. A loss on the quaternion’s entries, L(q) = ‖q − $q_gt$‖², has two minima (at $q_gt$ and −$q_gt$) for the same rotation, and a gradient step can flip between the basins. The fix is a sign-invariant loss: L(q) = min(‖q − $q_gt$‖², ‖q + $q_gt$‖²), or a loss on the rotation (the geodesic distance), which is sign-invariant by construction.

Corollary. Interpolation between q₀ and q₁ must choose the shorter arc — if the dot product q₀ · q₁ is negative, the interpolation should use −q₁ (which represents the same rotation) and interpolate from q₀ to −q₁, giving the shorter path. The slerp formula does this: slerp(q₀, q₁, t) = (sin((1−t)Ω)/sin Ω) q₀ + (sin(tΩ)/sin Ω) q₁, with Ω = arccos(|q₀ · q₁|) and the sign of q₁ chosen so that q₀ · q₁ ≥ 0.

§L.3.2 — Worked-example ladder

WE-L.3.a — Fully worked: Rodrigues’ formula and the quaternion for a 90° rotation about z

Input. u = (0, 0, 1), θ = π/2 (a 90° rotation about z).

Trace (Rodrigues).

  1. \hat{u} = [[0, -1, 0], [1, 0, 0], [0, 0, 0]].
  2. \hat{u}^2 = [[-1, 0, 0], [0, -1, 0], [0, 0, 0]] (the z-axis is fixed; the xy-plane is rotated by 90°).
  3. R = I + sin(π/2) \hat{u} + (1 - cos(π/2)) \hat{u}^2 = I + \hat{u} + 1·\hat{u}^2.
  4. I + \hat{u} + \hat{u}^2 = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] + [[0, -1, 0], [1, 0, 0], [0, 0, 0]] + [[-1, 0, 0], [0, -1, 0], [0, 0, 0]] = [[0, -1, 0], [1, 0, 0], [0, 0, 1]]. ✓ (this is Rz(π/2)).

Trace (quaternion).

  1. q = (cos(θ/2), sin(θ/2) u) = (cos(π/4), sin(π/4)·(0, 0, 1)) = (√2/2, 0, 0, √2/2).
  2. ‖q‖² = 1/2 + 1/2 = 1. ✓
  3. q’s rotation: u = (0, 0, 1) (the v part normalized), θ = 2 arccos(√2/2) = 2·(π/4) = π/2. ✓

Output and interpretation. The Rodrigues formula and the quaternion give the same rotation matrix Rz(π/2), and the two representations are equivalent — the quaternion is a different parameterization of the same R, with the q vs. −q redundancy ((√2/2, 0, 0, √2/2) and (−√2/2, 0, 0, −√2/2) are the two quaternions for this rotation).

Self-explanation 5. Before reading on: in step 2, why is \hat{u}^2 = [[-1, 0, 0], [0, -1, 0], [0, 0, 0]]? What does this matrix do?

Answer. \hat{u} is the cross-product matrix, so \hat{u}^2 v = \hat{u}(u × v) = u × (u × v) = u(u·v) - v(u·u) = u(u·v) - v (using the vector triple product). For u = (0, 0, 1), u(u·v) - v gives (0, 0, $v_z$) - ($v_x$, $v_y$, $v_z$) = ($-v_x$, $-v_y$, 0), which is the matrix [[-1, 0, 0], [0, -1, 0], [0, 0, 0]]. This matrix is the projection onto the xy-plane negated — it reflects the xy-plane, which is the second-order effect of the rotation (the sin θ term is the first-order effect, the (1 - cos θ) term is the second-order).

WE-L.3.b — Completion problem

Input. q₀ = (1, 0, 0, 0) (the identity) and q₁ = (0, 0, 0, 1) (a 180° rotation about z).

Trace (final 25% as prompts).

  1. q₁ = (cos(π/2), sin(π/2)·(0, 0, 1)) = (0, 0, 0, 1). ✓
  2. q₁’s rotation: θ = 2 arccos(0) = π, u = (0, 0, 1) — a 180° rotation about z. ⟨your turn: compute R(q₁) from the formula and verify it is Rz(π)⟩
  3. q₀ · q₁ = 0·1 + 0·0 + 0·0 + 1·0 = 0. ⟨your turn: is the dot product 0? What does that imply for the slerp’s arc length?⟩
  4. The slerp: Ω = arccos(|0|) = π/2, and slerp(0.5) = (sin(0.25π)/sin(π/2))·q₀ + (sin(0.25π)/sin(π/2))·q₁ = (√2/2)·q₀ + (√2/2)·q₁ = (√2/2, 0, 0, √2/2). ⟨your turn: what rotation does this represent?⟩

Answers.

  1. R(q₁) = I + 2·0·\hat{z} + 2·\hat{z}^2 = I + 2·[[-1, 0, 0], [0, -1, 0], [0, 0, 0]] = [[-1, 0, 0], [0, -1, 0], [0, 0, 1]] — the 180° rotation about z. ✓
  2. q₀ · q₁ = 0, so the two quaternions are orthogonal, which means the arc between them is π/2 in the quaternion space (4D). The rotation arc is π, because the rotation angle is twice the quaternion’s — 2·arccos|q₀·q₁| = 2·(π/2) = π. The “half” relation doubles the rotation arc; it does not leave it equal.
  3. slerp(0.5) = (√2/2, 0, 0, √2/2), which is the quaternion for a 90° rotation about z (the halfway point in the rotation space). ✓ The slerp gives a constant-angular-velocity path from the identity to the 180° rotation, and the midpoint is the 90° rotation.

The lesson. Slerp interpolates along the quaternion’s great circle, and the rotation’s angle is twice the quaternion’s arc: a quaternion arc of π/2 is a rotation arc of π. The slerp’s halfway point is the halfway rotation, because the factor of two applies to the whole path — which is exactly what the θ/2 in the quaternion’s definition buys, and why the slerp’s constant angular velocity in quaternion space transfers to constant angular velocity in SO(3).

WE-L.3.c — Faded: the double cover and the sign flip

Setup. q = (√2/2, 0, 0, √2/2) (the 90° z rotation) and $q_gt$ = (−√2/2, 0, 0, −√2/2) (the same rotation, but the −q representation).

Question. What is the loss ‖q − $q_gt$‖²? What is the geodesic loss θ²? Which one is the correct loss for learning?

You must produce the intermediate steps. The setup and the answer are given.

Answer. ‖q − $q_gt$‖² = (√2/2 − (−√2/2))² + 0 + 0 + (√2/2 − (−√2/2))² = (√2)² + (√2)² = 2 + 2 = 4. The geodesic loss θ² = 0 (the two quaternions represent the same rotation). The correct loss is the geodesic (or a sign-invariant quaternion loss), and the naive entry loss of 4 is a spurious error — the two quaternions are the same rotation, but the entry loss treats them as different, which is the double cover’s consequence.

The lesson. The naive quaternion loss is not a rotation loss; it has a spurious penalty for the −q representation, which the learning can exploit by flipping the quaternion’s sign (a gradient step in the direction of −q is a “better” fit to the entry loss but the same rotation). The fix is the sign-invariant loss or the geodesic loss.

WE-L.3.d — Contrastive near-miss: the non-normalized quaternion

Correct solution. The network’s output $q_raw$ is normalized (q = q_raw/‖$q_raw$‖), and q is a unit quaternion; the conversion R(q) is a rotation.

Near-miss. The network’s output $q_raw$ is used directly in the quaternion-to-matrix formula, without normalization; ‖$q_raw$‖ ≠ 1, so $R(q_raw$) is not a rotation — it is a matrix with scaled entries and possibly det ≠ +1.

Why the near-miss produces a plausible but incorrect number. The unnormalized $q_raw$ gives a matrix $R(q_raw$) that is close to a rotation if ‖$q_raw$‖ is close to 1, and the loss decreases as $q_raw$ moves toward the ground truth. But the matrix’s entries are scaled by ‖$q_raw$‖², and a network that learns to inflate $q_raw$’s norm can reduce some entries’ error at the cost of others; the resulting matrix stretches the object, and the “rotation” is not in SO(3). The symptom is a stretched object, a det R ≠ +1, and a RᵀR ≠ I; the fix is the normalization, and the normalization must be in the forward pass, not just in the loss.

The discrepancy, stated. $R(q_raw$) with ‖$q_raw$‖ = c is I + 2c²·w\hat{v} + 2c²·\hat{v}² — both terms scale with c², because the formula I + 2w[v]_× + 2[v]_ײ is quadratic in the quaternion’s components, and scaling q → cq scales w and v together. The linear term is quadratic in c, not linear. The normalization q = q_raw/‖$q_raw$‖ is what makes R(q) a rotation; skipping it makes R a general linear map whose “rotation” part is confounded with the scale. For c = 2 on the 90°-about-z quaternion the determinant is 25, while the mis-scaled I + 2c·w\hat{v} + 2c²\hat{v}² gives 13 — the same qualitative symptom (a stretch, det ≠ 1), the wrong number, which is why the lab’s printed 25/37/1 are the check and the algebra is not. The fix is to normalize in the forward pass, and to constrain the network to output unit quaternions (or use the exponential coordinate’s exp, which outputs unit quaternions by construction).

Self-explanation 6. Before reading on: in the near-miss, if the network’s output $q_raw$ has ‖$q_raw$‖ = 2, what is the matrix $R(q_raw$)’s relationship to the rotation R(q_raw/‖$q_raw$‖)?

Answer. $R(q_raw$) is not simply R(q_raw/‖$q_raw$‖) scaled; the quaternion-to-matrix formula is quadratic in q, so R(c·q) = I + 2c²·w\hat{v} + 2c²·\hat{v}², which is a quadratic function of c in both terms. For c = 2, R(2q) = I + 8w\hat{v} + 8\hat{v}², which is a different matrix from R(q) (not a scalar multiple, not a rotation). The correct operation is to normalize before the formula, giving R(q) (the rotation), not R(2q) (the stretched map).


Part D — Exponential Coordinates and the Learning Choice

§L.4 — Objects: so(3), exp, log, and the singularities

The Lie algebra so(3) is the set of 3×3 skew-symmetric matrices \hat{ω} with ω ∈ ℝ³. Its dimension is 3, and it is the tangent space of SO(3) at the identity.

The exponential map exp : so(3) → SO(3) is Rodrigues’ formula with ω = θu:

exp⁡(ω^)=I+sin⁡θθω^+1−cos⁡θθ2ω^2,θ=‖ω‖\exp(\hat{ω}) = I + \frac{\sin θ}{θ} \hat{ω} + \frac{1 - \cos θ}{θ²} \hat{ω}², \quad θ = ‖ω‖

The logarithm log : SO(3) → so(3) is its inverse:

log⁡(R)=θ2sin⁡θ(R−RT),θ=arccos⁡(tr(R)−12)\log(R) = \frac{θ}{2\sin θ}(R - Rᵀ), \quad θ = \arccos\left(\frac{\text{tr}(R) - 1}{2}\right)

The singularities:

  • At θ = 0 (the identity), ω = 0, and the log’s formula has a 0/0; the limit is ω = 0, and the derivative of log with respect to R is finite there (measured ‖∂ log/∂R‖ = 1.0). The left Jacobian’s determinant is 1 at θ = 0, so this is not a rank drop either — it is a chart defect, where the axis u is undefined while ω itself is perfectly well behaved.
  • At θ = π, ω = θu and −ω = −θu both map to the same rotation (the half-turn), so the axis u is not unique; the log’s derivative with respect to R is unbounded (measured ‖∂ log/∂R‖ = 3.1 × 10³ at θ = π − 10⁻³ against 1.57 at θ = π/2), and a gradient step computed through the log near θ = π can be arbitrarily large.

§L.4.1 — Result: exp is a diffeomorphism on ‖ω‖ < π, and the learning pipeline must avoid ‖ω‖ = π or use a two-chart approach

Claim. The exponential map exp : {ω ∈ ℝ³ : ‖ω‖ < π} → SO(3) \ {rotations by π} is a diffeomorphism — a smooth bijection with a smooth inverse — and its Jacobian is invertible on the open ball ‖ω‖ < π. The map fails to be a diffeomorphism at ‖ω‖ = π (the half-turns), where the log’s Jacobian is singular.

Proof sketch. The exp map’s differential at ω = θu is the left Jacobian $J_l(ω$) = I + \frac{1 - \cos θ}{θ}\hat{u} + \frac{θ - \sin θ}{θ}\hat{u}², whose determinant is 2$1 - \cos θ$/θ². That determinant is 1 in the limit θ → 0, is positive everywhere on 0 < θ < 2π, and reaches 0.405 at θ = π — so the exp map is a local diffeomorphism at the half-turn, and the π problem is not a rank drop. It is global injectivity: exp(πu) = exp(−πu) for every u, so the map is not injective on the closed ball ‖ω‖ ≤ π, and log has no unique inverse at the boundary. The two singularities are therefore of different kinds: at θ = 0 the map is injective but the axis is undefined (a chart issue); at θ = π the map is a local diffeomorphism but two-to-one (a covering issue).

Corollary. For learning, the exp map’s singularity at θ = 0 is benign: the left Jacobian’s determinant is 1 there (not zero), the derivative of log is finite (‖∂ log/∂R‖ = 1.0), and what degenerates is only the axis — u is undefined when ω = 0, so axis-angle is a chart with an origin defect while ω itself is perfectly well behaved. The singularity at θ = π is the problematic one, because the log’s output is ambiguous there, and a loss on ω can have two minima for the same rotation.

Corollary. The learning choice is the exponential coordinate for ‖ω‖ < π (the ball where the map is a diffeomorphism) with a two-chart fallback near ‖ω‖ = π (switch to a quaternion chart), or the quaternion with normalization (which has no chart singularity but the double cover). The two-chart approach is the most robust, and the single chart is the simplest; the choice depends on the task’s configurations.

§L.4.2 — Worked-example ladder

WE-L.4.a — Fully worked: exp for a small ω and a π rotation

Input. ω = (0.1, 0, 0) and ω = (π, 0, 0).

Trace (ω = (0.1, 0, 0)).

  1. θ = 0.1, u = (1, 0, 0).
  2. \hat{ω} = [[0, 0, 0], [0, 0, -0.1], [0, 0.1, 0]].
  3. \exp$\hat{ω}$ ≈ I + \hat{ω} + 0.5 \hat{ω}² = I + [[0, 0, 0], [0, 0, -0.1], [0, 0.1, 0]] + 0.5·[[0, 0, 0], [0, -0.01, 0], [0, 0, -0.01]] = [[1, 0, 0], [0, 0.995, -0.1], [0, 0.1, 0.995]] (the small-angle approximation of Rx(0.1)).

Trace (ω = (π, 0, 0)).

  1. θ = π, u = (1, 0, 0).
  2. \exp$\hat{ω}$ = I + \frac{\sin π}{π} \hat{ω} + \frac{1 - \cos π}{π²} \hat{ω}² = I + 0 + \frac{2}{π²} \hat{ω}².
  3. \hat{ω}² = [[0, 0, 0], [0, -π², 0], [0, 0, -π²]].
  4. \frac{2}{π²} \hat{ω}² = [[0, 0, 0], [0, -2, 0], [0, 0, -2]].
  5. I + \frac{2}{π²} \hat{ω}² = [[1, 0, 0], [0, -1, 0], [0, 0, -1]] = Rx(π). ✓

Output and interpretation. At θ = π, \exp gives Rx(π), and the same matrix is given by \exp$−\hat{πu}$ = \exp$\hat{-πu}$ — the −ω representation. The log’s inverse at Rx(π) is ambiguous — both (π, 0, 0) and (−π, 0, 0) are valid preimages — and the axis is not determined by the rotation alone (only the line through u is determined, not the direction). The θ = 0 case is benign (ω = 0 is the unique preimage of the identity), but the θ = π case is the problematic one.

Self-explanation 7. Before reading on: the left Jacobian at θ = π has determinant 0.405, not 0. What then is singular at the half-turn, and which map does the learning pipeline actually feel it in?

Answer. Two different maps are in play. The exp map is a local diffeomorphism at π (its Jacobian is invertible) and is smooth in ω — its gradient with respect to ω is finite, and in fact vanishes there (measured 4.0 at π/2, 0.004 at π − 0.001). The log map is where the trouble is: it has no continuous single-valued inverse at the half-turn (the two preimages ±πu), and its derivative with respect to R diverges as 1/(π − θ) (measured 1.57 at π/2, 3.1 × 10³ at π − 10⁻³). A pipeline that outputs ω and computes R = exp(ω) has a vanishing gradient at the half-turn, which reads as slow learning; a pipeline that supervises ω = log(R) has an exploding gradient there. The fix for the first is a better initialization or a loss on R; the fix for the second is a π clip or a two-chart switch.

WE-L.4.b — Completion problem

Input. ω = (0.1, 0.2, −0.05) (a small rotation).

Trace (final 25% as prompts).

  1. θ = ‖ω‖ = sqrt(0.01 + 0.04 + 0.0025) = sqrt(0.0525) ≈ 0.229.
  2. u = ω/θ ≈ (0.436, 0.873, −0.218). ⟨your turn: verify ‖u‖ = 1⟩
  3. R = \exp$\hat{ω}$. ⟨your turn: compute the first-order approximation I + \hat{ω} and the second-order I + \hat{ω} + 0.5 \hat{ω}², and compare⟩

Answers.

  1. ‖u‖² = 0.436² + 0.873² + 0.218² ≈ 0.190 + 0.762 + 0.047 = 0.999 ≈ 1. ✓
  2. \hat{ω} = [[0, 0.05, 0.2], [-0.05, 0, -0.1], [-0.2, 0.1, 0]]; \hat{ω}² is the outer product ωωᵀ - θ² I, which is a symmetric matrix; the first-order I + \hat{ω} is a rotation approximation with an error of O(θ²) ≈ 0.05, and the second-order I + \hat{ω} + 0.5\hat{ω}² has an error of O(θ³) ≈ 0.01. The second-order is closer to the true R.

The lesson. For small ω, the exp map is approximately linear ‘I+ω^‘`I + \hat{ω}`, and the linear approximation’s error is O(θ²); the second-order approximation is O(θ³), and the full formula is exact. A learning pipeline near the identity can use the linear approximation (which is the left Jacobian’s domain) and still have a smooth gradient; a pipeline near θ = π must use the full formula (and even then, the log’s ambiguity is the issue).

WE-L.4.c — Faded: the gradient at θ = 0 and at θ = π

Setup. A loss L(R) is a smooth function of the rotation, and ω is the exponential coordinate. The gradient ∂L/∂ω is computed by autograd.

Question. At ω = 0 (the identity) and at ω = (π, 0, 0) (a half-turn), is the gradient well-defined? What is the gradient’s magnitude near each?

You must produce the intermediate steps. The setup and the answer are given.

Answer. At ω = 0, the gradient is well-defined (the exp map is smooth, and the loss is smooth, so the composition is smooth); the gradient’s magnitude is finite. At ω = (π, 0, 0), the gradient with respect to ω is also well-defined — and it is smaller than at π/2, because exp has a horizontal tangent at the half-turn (measured 4.0 at π/2 against 0.004 at π − 0.001). What is not well-defined is the inverse: log maps Rx(π) to either +πu or −πu, and its derivative with respect to R diverges as 1/(π − θ) (measured 3.1 × 10³ at π − 10⁻³). So a forward pipeline ω → R sees a vanishing gradient at the half-turn, and a backward pipeline R → ω sees an exploding one; both are the same singularity seen through different maps.

The lesson. The θ = 0 singularity is a chart issue (the axis is undefined, the map is fine); the θ = π singularity is a covering issue (two preimages, and the inverse map’s derivative diverges). The two are different, and the π one is the one a learning pipeline must guard against — but the guard is needed on the log side, not the exp side.

WE-L.4.d — Contrastive near-miss: the exp coordinate without a π guard

Correct solution. The pipeline uses ω = log(R) and clips ‖ω‖ to π − ε (with ε ≈ 0.01), so the ω never reaches the singularity, and a gradient step near π is bounded.

Near-miss. The pipeline uses ω = log(R) without a clip. The gradient that reaches the parameters is ∂L/∂R = (∂L/∂ω)·(∂ log/∂R), and the second factor grows as 1/(π − θ) near the half-turn. A single training step there is amplified into a large parameter change — the rotation jumps far from where it was, and the training oscillates or diverges near the half-turns.

Why the near-miss produces a plausible but incorrect number. The loss decreases over most of the training, and the jump near π is a transient that the optimizer recovers from — the training eventually converges, but the transient loses training time, and the final model can be worse than the clipped version’s. The learner who sees occasional loss spikes and attributes them to “a bad batch” or “a learning-rate issue” has missed the π singularity, and the fix is the clip.

The discrepancy, stated. The log’s θ/(2 sin θ) factor is θ/(2 sin π) = ∞ at θ = π — the inverse map’s derivative is unbounded there (measured ‖∂ log/∂R‖ = 1.57 at π/2, 3.1 × 10³ at π − 10⁻³), and a single step near it can move the parameter far. The clip ‖ω‖ ≤ π − ε makes the factor bounded by θ/(2 sin(π − ε)) ≈ π/(2ε), which is finite (though large), and the optimizer’s step is bounded by the learning rate times that factor. The clip is a regularization at the singularity, and its cost is a small bias near the half-turns; its benefit is the stability of the training.

The asymmetry worth remembering. The clip is needed only on the log side. On the exp side — a network that outputs ω and computes R = exp(ω) — the gradient with respect to ω does not blow up at π; it vanishes (measured 4.0 at π/2, 0.004 at π − 10⁻³). That failure is the opposite symptom: not a spike but a stall, because the half-turn is the exp map’s extreme and its tangent there is horizontal. A learner who applies a π clip to an exp-side pipeline has treated a stall as a spike and clipped the gradient in the direction it was already too small.

Self-explanation 8. Before reading on: in the near-miss, if the training has already converged and the model’s rotations are all far from π, does the missing clip matter?

Answer. No — if every rotation the pipeline encounters is far from π (e.g., ‖ω‖ < π/2), the log’s factor is bounded by θ/(2 sin(π/2)) ≈ π/4, and the derivative is well-behaved. The clip matters only for rotations near the half-turns, and it is a defensive measure: a training that starts near π (from a random initialization with large angles) or crosses π (from a loss that pushes the rotation through the half-turn) needs the clip; a training that stays in ‖ω‖ < π/2 does not. The clip’s cost is negligible when it is inactive, and its benefit is the absence of a catastrophic step when it is active, which is why it is the default on the log side of a robust pipeline.

§L.4.3 — Comparative table: the learning-parameterization choice

ParameterizationStorageSingularityGradient near singularityDouble coverWhen to use
Euler angles3Gimbal lock (β = ±π/2)Unbounded (chart rank drop)NoNever for learning
Exponential coordinate (exp side, ω → R)3‖ω‖ = 0, πBounded at 0; vanishes at πNoLearning with a loss on R
Exponential coordinate (log side, R → ω)3‖ω‖ = 0, πBounded at 0; diverges at πNoSupervision with a π clip
Quaternion (normalized)4NoneBoundedYes (q ≡ −q)Learning with a sign-invariant loss, interpolation
3×3 matrix9NoneApproximate constraintNoComputation, not learning
Two-chart (exp + quaternion)3 or 4None (switch at π/2)BoundedDepends on the chartMost robust

Design principle (P6.3). Know which side of the exp map your gradient lives on. The exponential coordinate has 3 numbers and a smooth exp; on the forward side (ω → R), the gradient is bounded at 0 and vanishes at π, so a stall near half-turns is the symptom and the fix is initialization or a loss on R, not a clip. On the inverse side (R → ω), the derivative diverges at π, and the π clip or the two-chart switch is the fix. The quaternion is the alternative when the double cover’s consequences are acceptable (sign-invariant loss, slerp interpolation); the matrix is never the learning choice (its constraints are approximate).

§L.4.4 — Concept check

Concept check (displaces M5). A learner says: “The exp coordinate’s singularity at π is far from my configurations, so I don’t need to worry about it.” What are the two configurations in which it does matter, even if the target rotations are far from π?

Answer. First, a training that starts from a random initialization can have ‖ω‖ near π (a random rotation has a uniform axis and an angle that can be anywhere, including near π), and the initial gradient is unbounded; the model starts near the singularity even if it ends far from it. Second, a loss that pushes the rotation through the half-turn (e.g., a target that is π away from the current rotation, or a trajectory that crosses the half-turn) has a gradient that diverges at the crossing, and the model crosses the singularity even if the endpoints are far from it. The clip or the two-chart is the fix, and it is a defensive measure for both cases.

§L.4.5 — Consolidation checkpoint 4

(a) You should now be able to: (i) state the exp map and the log map, and compute both for a specific rotation; (ii) state the two singularities (θ = 0 and θ = π) and their different natures (a chart defect vs. a two-to-one covering); (iii) state the learning choice (exp with a π clip, or quaternion with a sign-invariant loss) and the trade-offs; (iv) state why a random initialization and a π-crossing trajectory both need the clip.

(b) Transfer probe. A learner’s network outputs ω, converts to R = exp(ω̂), and trains with a loss on R. The training is stable for the first 1000 steps and then diverges with a large loss spike. What is the most likely cause? Answer sketch: the gradient is blowing up on the log side of the round trip, where ∂ log/∂R grows as 1/(π − θ) (measured 1.57 at π/2, 3141 at π − 0.001). The exp map itself is smooth at the half-turn and its gradient shrinks there (4.0 → 0.004), so a π clip on the head’s output would suppress the wrong quantity. The fix is to keep the target ω — the argument to log_so3 — away from π, or to switch charts to the quaternion near π. The diagnostic is to log the target’s ‖ω‖ during training and observe it approaching π before the spike.

(c) Remediation rule. If you cannot do (b), return to §L.4.2 and redo WE-L.4.d, then re-attempt (b) by asking which side of the map the θ/(2 sin θ) factor lives on, and what that factor does as θ → π.


Part E — Failure Taxonomy, Lab, Cheat Sheet, Exercises, Exit Test

§L.5 — Failure taxonomy

SymptomThree most likely causes, rankedDiscriminating testFix
A learned “rotation” stretches the object(1) 9-parameter matrix output, no constraint; (2) non-normalized quaternion; (3) exponential coordinate unclipped and near πPrint RᵀR − I and det R; if non-zero, it is (1) or (2)Parameterize 3 numbers and exp; normalize the quaternion; clip the ω
Rotation interpolation stutters, reverses, or takes a detour(1) Euler interpolation through gimbal lock; (2) non-normalized quaternion slerp; (3) the wrong quaternion sign in slerpMeasure the path’s distance from the geodesic joining its endpoints; a large detour with smooth velocity is (1)Slerp with the sign choice; exponential geodesic
Training oscillates near half-turns(1) Differentiating through log near π; (2) the θ/(2 sin θ) factor unbounded; (3) the optimizer’s step amplifiedPrint ‖∂ log/∂R‖ near the half-turn — if it grows as 1/(π−θ), it is (1). The exp-side gradient falls there and is not the causeClip on the log side; two-chart switch
Quaternion loss jumps between q and −q(1) Non-sign-invariant entry loss; (2) the double cover; (3) the training’s basin flipPrint the quaternion’s sign at consecutive steps; a flip is the causeSign-invariant loss; geodesic loss
The Euler inverse map has multiple solutions(1) Gimbal lock (β = ±π/2); (2) the convention’s non-uniqueness; (3) the inverse map’s branch choicePrint the triple at β = π/2; the non-uniqueness is the causeUse quaternion or exponential; if Euler is required, fix a branch

The table covers the lab’s three break-it steps (non-unit quaternion, Euler interpolation through gimbal lock, the log-side π singularity) plus two not exercised there (the 9-parameter matrix, the quaternion sign flip).

§L.6 — The lab

§L.6.1 — Task

Implement conversions between the four representations (matrix, ZYX Euler, quaternion, exponential coordinate) and use them to demonstrate gimbal lock, the double cover, the π singularity, and the interpolation differences; break the pipeline three ways.

§L.6.2 — Conditions

Python 3.10+, PyTorch 2.2+, CPU only. All runs use torch.manual_seed(42), dtype = float64. Runtime: under 2 minutes.

§L.6.3 — Controls held fixed

The seed (42), the dtype (float64), and the base rotation (Rz(0.3)·Ry(0.5)·Rx(0.2)) are fixed across all steps.

§L.6.4 — Metrics table

MetricDefinitionHow measured
roundtrip_error‖R - R(R.from_X(X.from_R(R)))‖_max for each representation XThe max entry difference
gimbal_lock_rankThe rank of the Euler chart’s Jacobian at β = π/2The numerical rank of the 3×3 Jacobian
gimbal_lock_singular_valuesThe singular values of the Jacobian at β = π/2The three singular values
slerp_error(t)‖$R_slerp(t$) - $R_geodesic(t$)‖_max for t ∈ {¼, ½}The max entry difference at each t
nlerp_error(t)‖$R_nlerp(t$) - $R_geodesic(t$)‖_max for t ∈ {¼, ½}The max entry difference at each t
nonunit_quaternion_det$det(R(q_raw$)) for ‖$q_raw$‖ = 2, on two axes plus the identityThe determinant
exp_grad‖∂L/∂ω‖ at ‖ω‖ = π − 0.001 vs. ‖ω‖ = π/2The gradient norm at both
log_deriv‖∂ log/∂R‖ (finite difference) at ‖ω‖ = π − 0.001 vs. ‖ω‖ = π/2The difference-quotient norm at both
euler_detourThe max geodesic distance between the Euler-linear path and the geodesic joining its endpointsThe angle between $R_euler(t$) and the closest point on the geodesic

§L.6.5 — Pre-registered hypotheses

  • H1. roundtrip_error is < 1e-10 for the quaternion and exponential representations (they are exact diffeomorphisms away from singularities), and is < 1e-10 for Euler except near β = ±π/2, where it can be larger (the inverse map’s branch choice).
  • H2. gimbal_lock_singular_values has one value ≈ 0 and two > 0.1; the numerical rank is 2, not 3. The prediction is [0, σ₂, σ₃] with σ₂, σ₃ > 0.1.
  • H3. slerp_error is < 1e-10 at both t = ¼ and t = ½ (slerp tracks the geodesic exactly), while nlerp_error is < 1e-10 at t = ½ and > 1e-3 at t = ¼ — at the midpoint the two coincide by symmetry, so the midpoint is the wrong place to look. Measured for a 90° arc: nlerp errs by 1.46e-2 (max entry) at t = ¼ and by 0 at t = ½. The prediction is the pair (quarter > 1e-3, midpoint < 1e-10), and the midpoint figure is the control that shows the test can see the difference and chooses not to.
  • H4. nonunit_quaternion_det for ‖$q_raw$‖ = 2 is not 1, and not a function of ‖$q_raw$‖ alone: it depends on the quaternion’s axis. Writing R(q) = I + 2w[v]_× + 2[v]_ײ for q = (w, v), scaling q → cq gives I + 2c²w[v]_× + 2c²[v]_ײ — both terms quadratic in c, with a constant term, so det R(cq) is neither c⁶ nor any other fixed power of c. For the lab’s 90°-about-z quaternion scaled to norm 2 the determinant is 25; for (0.5, 0.5, 0.5, 0.5) scaled to norm 2 it is 37; for the identity quaternion scaled to norm 2 it is 1 — the unnormalized map leaves the identity untouched while stretching every other rotation. The prediction is det ≠ 1 with the axis-dependent value, and the general lesson is that the unnormalized map is not a similarity transform.
  • H5. The π singularity is in the log map, not the exp map. The exp map’s gradient with respect to ω vanishes at ‖ω‖ = π (measured: 4.0 at π/2, 0.004 at π − 0.001), because exp is smooth and the half-turn is its extreme. The log map’s derivative with respect to R diverges as ‖ω‖ → π (measured: 1.57 at π/2, 3.1e3 at π − 0.001). The prediction is the pair of directions: exp’s gradient falls, log’s derivative grows, and the ratio ‖∂ log/∂R‖ at π − 0.001 over π/2 is > 100.

§L.6.6 — The break-it sequence

  1. The non-unit quaternion (silent). Use $q_raw$ (with ‖$q_raw$‖ = 2) in the quaternion-to-matrix formula without normalization. Predicted symptom: no exception; RᵀR ≠ I, det R ≠ 1, the “rotation” stretches. Actual: as predicted — silent, det = 25 for the z-axis quaternion and 37 for a generic axis.
  2. The Euler interpolation through gimbal lock (silent). Interpolate (α₀, β₀, γ₀) to (α₁, β₁, γ₁) linearly, with β crossing π/2. Predicted symptom: no exception; the rotation path is not the geodesic between its endpoints — it detours by a large fraction of the total rotation. Actual: as predicted — silent; endpoints 8.10° apart, max detour 4.41°, i.e. 54% of the total rotation. Note that the angular velocity does not jump: it is smooth through the crossing and merely dips. The defect is the path, not the speed.
  3. The π singularity in the log map (silent). Train a small network whose loss differentiates through log_so3, with ω near π and no clip. Predicted symptom: no exception at the crossing; the derivative’s norm grows as 1/(π − θ); a step overshoots. Actual: as predicted — silent; the log derivative is 1999× larger at π − 0.001 than at π/2. The exp-side gradient moves the other way (4.0 → 0.004), which is why the clip must go on the right side of the map.

All three are silent, exceeding the minimum of two.

§L.6.7 — Acceptance criteria

CriterionThresholdArtefact it is tested on
Round-trip error for quaternion and exponential< 1e-10The lab’s round-trip table
Gimbal-lock Jacobian rank2 (not 3)The lab’s singular-value print
Slerp error at t = ¼ and t = ½< 1e-10 at bothThe lab’s interpolation table
Nlerp error at t = ¼> 1e-3 (measured 1.46e-2)The lab’s interpolation table
Nlerp error at t = ½< 1e-10 — the control showing the midpoint cannot discriminateThe lab’s interpolation table
Non-unit quaternion’s det≠ 1, axis-dependent (25 z-axis, 37 generic, 1 identity)The lab’s det prints
Log-side derivative ratio ‖∂ log/∂R‖(π−ε) / ‖∂ log/∂R‖(π/2)> 100 (measured 1999)The lab’s log-derivative print
Exp-side gradient ratio ‖∂L/∂ω‖(π−ε) / ‖∂L/∂ω‖(π/2)< 1 (measured 1e-3) — the direction that refutes the naive readingThe lab’s exp-gradient print
Euler-linear detour off the geodesic> 1° (measured 4.41°)The lab’s detour print
Three break-its predicted before running3/3 timestampedThe notebook’s prediction file

§L.6.8 — What good looks like

A strong submission has five properties:

  1. The round-trip conversions are tested for all four representations, with the error printed for each, and the Euler round-trip’s larger error near β = ±π/2 is shown separately.
  2. Gimbal lock is shown as a rank drop, with the Jacobian’s singular values printed and the ≈ 0 value identified.
  3. The interpolation comparison is made at a t where the two differ. A submission that reports only the midpoint has demonstrated nothing, because slerp and nlerp agree there by symmetry; the strong submission prints both t = ¼ and t = ½ and says why the first is the discriminating one.
  4. The π singularity is shown as a ratio on each side of the map, with the exp-side gradient falling and the log-side derivative rising, and the two norms printed. A submission that reports only one direction has not located the singularity.
  5. The Euler detour is measured against the geodesic, not against the per-step change in R — the latter is smooth and would show nothing.
  6. The three break-its are predicted before running, with numeric bounds, and the actuals are compared.

§L.6.9 — Runnable lab code

# lab_0_2_2.py — Rotation representations, gimbal lock, double cover, pi singularity
# Requires: Python 3.10+, PyTorch 2.2+

import torch
import math

torch.manual_seed(42)
dtype = torch.float64
device = torch.device("cpu")

# --- Step 1: elementary rotations and conversions ----------------------
def rot_x(q): c, s = torch.cos(q), torch.sin(q); return torch.tensor([[1, 0, 0], [0, c, -s], [0, s, c]], dtype=dtype, device=device)
def rot_y(q): c, s = torch.cos(q), torch.sin(q); return torch.tensor([[c, 0, s], [0, 1, 0], [-s, 0, c]], dtype=dtype, device=device)
def rot_z(q): c, s = torch.cos(q), torch.sin(q); return torch.tensor([[c, -s, 0], [s, c, 0], [0, 0, 1]], dtype=dtype, device=device)

def $R_to_zyx(R$):
    """Return (alpha, beta, gamma) with R = Rz(alpha) Ry(beta) Rx(gamma)."""
    beta = torch.asin(-R[2, 0].clamp(-1, 1))
    cb = torch.cos(beta)
    if cb.abs() < 1e-9:                       # gimbal lock: choose gamma = 0
        gamma = torch.zeros_like(beta)
        alpha = torch.atan2(R[1, 2], R[0, 2])  # or any branch
    else:
        alpha = torch.atan2(R[1, 0] / cb, R[0, 0] / cb)
        gamma = torch.atan2(R[2, 1] / cb, R[2, 2] / cb)
    return alpha, beta, gamma

def zyx_to_R(a, b, g): return rot_z(a) @ rot_y(b) @ rot_x(g)

def $R_to_quat(R$):
    """Convert R (3,3) to unit quaternion (w, x, y, z)."""
    tr = R[0, 0] + R[1, 1] + R[2, 2]
    if tr > 0:
        s = 2 * torch.sqrt(tr + 1.0)
        w = 0.25 * s
        x = (R[2, 1] - R[1, 2]) / s
        y = (R[0, 2] - R[2, 0]) / s
        z = (R[1, 0] - R[0, 1]) / s
    elif R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
        s = 2 * torch.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2])
        w = (R[2, 1] - R[1, 2]) / s
        x = 0.25 * s
        y = (R[0, 1] + R[1, 0]) / s
        z = (R[0, 2] + R[2, 0]) / s
    elif R[1, 1] > R[2, 2]:
        s = 2 * torch.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2])
        w = (R[0, 2] - R[2, 0]) / s
        x = (R[0, 1] + R[1, 0]) / s
        y = 0.25 * s
        z = (R[1, 2] + R[2, 1]) / s
    else:
        s = 2 * torch.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1])
        w = (R[1, 0] - R[0, 1]) / s
        x = (R[0, 2] + R[2, 0]) / s
        y = (R[1, 2] + R[2, 1]) / s
        z = 0.25 * s
    q = torch.stack([w, x, y, z])
    return q / q.norm()

def quat_to_R(q):
    w, x, y, z = q.unbind()
    return torch.tensor([
        [1 - 2*(y*y + z*z), 2*(x*y - z*w), 2*(x*z + y*w)],
        [2*(x*y + z*w), 1 - 2*(x*x + z*z), 2*(y*z - x*w)],
        [2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x*x + y*y)],
    ], dtype=dtype, device=device)

def skew(w):
    z = torch.zeros((), dtype=dtype, device=device)
    return torch.stack([torch.stack([z, -w[2], w[1]]),
                        torch.stack([w[2], z, -w[0]]),
                        torch.stack([-w[1], w[0], z])])

def exp_so3(w):
    theta = w.norm()
    U = skew(w)
    if float(theta) < 1e-9:
        return torch.eye(3, dtype=dtype, device=device) + U
    return (torch.eye(3, dtype=dtype, device=device)
            + (torch.sin(theta) / theta) * U
            + ((1 - torch.cos(theta)) / theta**2) * (U @ U))

def log_so3(R):
    tr = (R[0, 0] + R[1, 1] + R[2, 2]).clamp(-1, 3)
    theta = torch.acos(((tr - 1) / 2).clamp(-1, 1))
    v = torch.stack([R[2, 1] - R[1, 2], R[0, 2] - R[2, 0], R[1, 0] - R[0, 1]])
    if float(theta) < 1e-9:
        return v / 2
    return (theta / (2 * torch.sin(theta))) * v

# --- Step 2: the base rotation and round trips ------------------------
R = rot_z(torch.tensor(0.3, dtype=dtype)) @ rot_y(torch.tensor(0.5, dtype=dtype)) @ rot_x(torch.tensor(0.2, dtype=dtype))

a, b, g = $R_to_zyx(R$)
q = $R_to_quat(R$)
w = log_so3(R)

print(f"ZYX: ({a.item():.6f}, {b.item():.6f}, {g.item():.6f})")
print(f"quat: {q.tolist()}, ||q|| = {q.norm().item():.6f}")
print(f"omega: {w.tolist()}, ||w|| = {w.norm().item():.6f}")

err_zyx = (zyx_to_R(a, b, g) - R).abs().max().item()
err_q   = (quat_to_R(q) - R).abs().max().item()
err_w   = (exp_so3(w) - R).abs().max().item()
print(f"roundtrip errors: ZYX={err_zyx:.2e}, quat={err_q:.2e}, omega={err_w:.2e}")

# --- Step 3: gimbal lock at beta = pi/2 -------------------------------
$R_gl$ = rot_z(torch.tensor(0.3, dtype=dtype)) @ rot_y(torch.tensor(math.pi/2, dtype=dtype)) @ rot_x(torch.tensor(0.5, dtype=dtype))
# The Jacobian of (a, b, g) -> R at (a, b, g): numerically differentiate
def jacobian_zyx(a, b, g, eps=1e-6):
    J = torch.zeros(9, 3, dtype=dtype, device=device)
    for i, (da, db, dg) in enumerate([(eps, 0, 0), (0, eps, 0), (0, 0, eps)]):
        Rp = zyx_to_R(a + da, b + db, g + dg).reshape(-1)
        Rm = zyx_to_R(a - da, b - db, g - dg).reshape(-1)
        J[:, i] = (Rp - Rm) / (2 * eps)
    return J

$a_gl$, $b_gl$, $g_gl$ = $R_to_zyx(R_gl$)
J = $jacobian_zyx(a_gl$, $b_gl$, $g_gl$)
sv = torch.linalg.svdvals(J)
print(f"gimbal-lock singular values: {sv.tolist()}")
print(f"gimbal-lock rank: {(sv > 0.1).sum().item()}")  # Expected: 2

# --- Step 4: slerp vs. nlerp — measure at t=1/4, where they differ ----
q0 = $R_to_quat(torch$.eye(3, dtype=dtype, device=device))
q1 = $R_to_quat(rot_z(torch$.tensor(math.pi/2, dtype=dtype)))

def slerp(q0, q1, t):
    dot = (q0 * q1).sum()
    if dot < 0:
        q1 = -q1
        dot = -dot
    Omega = math.acos(min(max(float(dot), -1.0), 1.0))
    if Omega < 1e-9:
        return q0
    return (math.sin((1 - t) * Omega) / math.sin(Omega)) * q0 + (math.sin(t * Omega) / math.sin(Omega)) * q1

def nlerp(q0, q1, t):
    q = (1 - t) * q0 + t * q1
    return q / q.norm()

# At t = 1/2 the two coincide by symmetry. The discriminating point is t = 1/4.
interp_err = {}
for t in (0.25, 0.5):
    $R_true$ = rot_z(torch.tensor(t * math.pi / 2, dtype=dtype))
    $R_s$ = quat_to_R(slerp(q0, q1, t))
    $R_n$ = quat_to_R(nlerp(q0, q1, t))
    interp_err[t] = (($R_s$ - $R_true$).abs().max().item(),
                     ($R_n$ - $R_true$).abs().max().item())
    print(f"t={t}: slerp error = {interp_err[t][0]:.2e}, nlerp error = {interp_err[t][1]:.2e}")

# --- Step 5: break-it #1 — the non-unit quaternion (silent) ----------
$q_unit$ = $R_to_quat(rot_z(torch$.tensor(math.pi/2, dtype=dtype)))
$q_raw$ = 2 * $q_unit$  # $||q_raw||$ = 2
$R_raw$ = $quat_to_R(q_raw$)  # NOT normalized
print(f"[break-it #1] $det(R(q_raw$)) = {torch.linalg.$det(R_raw$).item():.6f}")  # Expected: != 1
print(f"[break-it #1] $||R(q_raw)^T$ $R(q_raw$) - I||_max = {(R_raw.T @ $R_raw$ - torch.eye(3, dtype=dtype, device=device)).abs().max().item():.6f}")
# The value is axis-dependent, not a function of $||q_raw||$ alone:
$q_gen$ = torch.tensor([0.5, 0.5, 0.5, 0.5], dtype=dtype, device=device)
print(f"[break-it #1] det(R(2q)) for a generic axis = {torch.linalg.det(quat_to_R(2 * $q_gen$)).item():.6f}")

# --- Step 6: break-it #2 — Euler interpolation through gimbal lock ---
# The endpoints differ only in beta (alpha - gamma is the invariant, and it is 0
# at both ends). Interpolating alpha, beta, gamma linearly looks harmless — but
# the parameter path is not the rotation path. Measure the detour.
a0, b0, g0 = 0.0, math.pi/2 - 0.1, 0.0
a1, b1, g1 = math.pi/2, math.pi/2 + 0.1, math.pi/2
RA = zyx_to_R(*[torch.tensor(v, dtype=dtype) for v in (a0, b0, g0)])
RB = zyx_to_R(*[torch.tensor(v, dtype=dtype) for v in (a1, b1, g1)])

def expm(w):
    t = w.norm()
    U = skew(w)
    if float(t) < 1e-12:
        return torch.eye(3, dtype=dtype, device=device) + U
    return (torch.eye(3, dtype=dtype, device=device)
            + (torch.sin(t) / t) * U + ((1 - torch.cos(t)) / t**2) * (U @ U))

def angle(A, B):
    tr = (A.T @ B)
    return torch.acos(((tr[0, 0] + tr[1, 1] + tr[2, 2] - 1) / 2).clamp(-1, 1))

M = RA.T @ RB
th = angle(torch.eye(3, dtype=dtype, device=device), M)
ax = torch.stack([M[2, 1] - M[1, 2], M[0, 2] - M[2, 0], M[1, 0] - M[0, 1]]) / (2 * torch.sin(th))

detours = []
for i in range(101):
    t = i / 100
    Re = zyx_to_R(*[torch.tensor(v, dtype=dtype) for v in
                    (a0 + t*(a1-a0), b0 + t*(b1-b0), g0 + t*(g1-g0))])
    # the geodesic's closest point to Re is the one at Re's own angle from RA
    Rg = RA @ expm((angle(RA, Re) / th) * th * ax)
    detours.append(angle(Rg, Re).item())
print(f"[break-it #2] endpoint separation: {math.degrees(th.item()):.2f} deg")
print(f"[break-it #2] max detour off the geodesic: {math.degrees(max(detours)):.2f} deg")
print(f"[break-it #2] detour profile (every 10th): {[round(math.degrees(detours[i]), 2) for i in range(0, 101, 10)]}")
# The path wanders 4.41 deg off a geodesic that is only 8.10 deg long. No exception.

# --- Step 7: break-it #3 — the two sides of the pi singularity --------
# exp side: gradient of a loss on R with respect to omega. It VANISHES at pi.
I3 = torch.eye(3, dtype=dtype, device=device)
exp_grads = {}
for mag in (math.pi / 2, math.pi - 0.001):
    w = torch.tensor([mag, 0.0, 0.0], dtype=dtype, device=device, requires_grad=True)
    ((exp_so3(w) - I3) ** 2).sum().backward()
    exp_grads[mag] = w.grad.norm().item()
    print(f"[break-it #3] exp-side grad norm at |w|={mag:.4f}: {exp_grads[mag]:.6f}")

# log side: derivative of log with respect to R. It DIVERGES at pi.
E = torch.tensor([[0.0, 1, 0], [-1, 0, 0], [0, 0, 0]], dtype=dtype, device=device) * 1e-6
log_derivs = {}
for mag in (math.pi / 2, math.pi - 0.001):
    R0 = exp_so3(torch.tensor([mag, 0.0, 0.0], dtype=dtype))
    log_derivs[mag] = ((log_so3(R0 + E) - log_so3(R0 - E)) / 2e-6).norm().item()
    print(f"[break-it #3] log-side |d log/dR| at |w|={mag:.4f}: {log_derivs[mag]:.4f}")

# --- Step 8: summary ------------------------------------------------
print("\n--- lab 0.2.2 summary ---")
print(f"H1: roundtrip ZYX={err_zyx:.2e}, quat={err_q:.2e}, omega={err_w:.2e}")
print(f"H2: gimbal singular values={sv.tolist()}, rank={(sv > 0.1).sum().item()}")
print(f"H3: slerp t=1/4={interp_err[0.25][0]:.2e}, nlerp t=1/4={interp_err[0.25][1]:.2e} "
      f"(t=1/2: slerp={interp_err[0.5][0]:.2e}, nlerp={interp_err[0.5][1]:.2e})")
print(f"H4: det(R(2q))={torch.linalg.$det(R_raw$).item():.4f}, generic axis={torch.linalg.$det(quat_to_R(2*q_gen$)).item():.4f}")
print(f"H5: exp grad {exp_grads[math.pi - 0.001]:.6f} at pi vs {exp_grads[math.pi / 2]:.4f} at pi/2 (falls); "
      f"log deriv ratio = {log_derivs[math.pi - 0.001] / log_derivs[math.pi / 2]:.1f}")
print(f"break-it #2: {math.degrees(th.item()):.2f} deg endpoints, {math.degrees(max(detours)):.2f} deg detour")
print("silent break-its: #1 (non-unit quat), #2 (Euler gimbal), #3 (pi singular)")

§L.7 — The cheat sheet

Term or resultStatement or definitionRole: lesson of origin, lesson that consumes it
Minimal parameterizationA 3-number chart on SO(3); every minimal chart has a singularityIntroduced here; consumed by 3.1.2 (VLA pose head)
Gimbal lockThe rank drop of a Tait-Bryan chart at the middle angle ±π/2Introduced here; consumed by 2.1.1 (pose estimation diagnostics)
Rodrigues’ formulaR = I + sin θ \hat{u} + (1 − cos θ) \hat{u}²Introduced here; consumed by 0.2.3 (exp map), 1.1.1 (joint FK)
QuaternionA unit 4-vector (w, x, y, z); R(q) via the formulaIntroduced here; consumed by 2.1.1 (pose representation), 2.2.1 (averaging)
Hamilton productThe quaternion composition, matching R(q₁ ⊗ q₂) = R(q₁)R(q₂)Introduced here; consumed by 2.2.1 (interpolation), 3.1.2
Double coverq and −q represent the same rotationIntroduced here; consumed by 2.2.1 (sign-invariant loss)
SlerpConstant-angular-velocity quaternion interpolationIntroduced here; consumed by 4.3.x (trajectory evaluation)
so(3)The 3-dimensional Lie algebra of skew-symmetric matricesIntroduced here; consumed by 0.2.3 (screw theory)
Exp mapexp : so(3) → SO(3), Rodrigues with ω = θuIntroduced here; consumed by 0.2.3, 1.1.1 (twist)
Log maplog : SO(3) → so(3), inverse of expIntroduced here; consumed by 1.1.2 (IK)
π singularityThe log’s ambiguity and unbounded gradient at half-turnsIntroduced here; consumed by 2.2.1 (policy gradient)
Learning choiceExp coordinate, clipping the log side near π, or quaternion with a sign-invariant lossIntroduced here; consumed by 3.1.2 (VLA action parameterization)
Design principle P6.1Choose the representation by the taskIntroduced here; consumed by 2.1.1
Design principle P6.2Never interpolate in Euler spaceIntroduced here; consumed by 4.3.2
Design principle P6.3Use exp for learning; clip the log side, or switch charts near πIntroduced here; consumed by 3.1.2

§L.8 — Exercises with solutions

E 0.2.2.1 — The ZYX triple (numerical). R = Rz(π/2) · Ry(0) · Rx(0). Compute the ZYX triple and state the singularity’s value of β.

S 0.2.2.1. R = Rz(π/2), so the ZYX triple is (π/2, 0, 0); β = 0, far from the singularity ±π/2. The lesson: the triple’s β is the middle angle, and the singularity is at β = ±π/2; a rotation about the first axis has β = 0 and is far from the singularity.

E 0.2.2.2 — The quaternion for a 60° rotation (numerical). u = (0, 1, 0), θ = π/3. Compute q, verify ‖q‖ = 1, and compute R(q).

S 0.2.2.2. q = (cos(π/6), sin(π/6)·(0, 1, 0)) = (√3/2, 0, 1/2, 0); ‖q‖² = 3/4 + 1/4 = 1. R(q) = I + 2w\hat{v} + 2\hat{v}² with w = √3/2, v = (0, 1/2, 0): \hat{v} = [[0, 0, 1/2], [0, 0, 0], [-1/2, 0, 0]], \hat{v}² = [[-1/4, 0, 0], [0, 0, 0], [0, 0, -1/4]]. R = I + 2·(√3/2)·\hat{v} + 2·(-1/4)·... = [[1/2, 0, √3/2], [0, 1, 0], [-√3/2, 0, 1/2]] — Ry(π/3). ✓ The lesson: the quaternion’s angle is θ/2, and the matrix formula recovers Ry(θ).

E 0.2.2.3 — The gimbal-lock rank drop (diagnostic). A ZYX chart’s Jacobian at β = π/2 has singular values [0, 1.0, 1.0]. What is the rank, and what does the zero singular value mean for a gradient step?

S 0.2.2.3. Rank 2. The zero singular value means the Jacobian’s column space is 2-dimensional — the three angles generate only two independent rotation directions — so the inverse Jacobian is unbounded, and a gradient step on the angles is unbounded for a given loss in SO(3). The fix is to avoid the singularity (use quaternion or exp) or to switch charts. The lesson: the singularity is a rank issue, and the gradient’s magnitude near it is 1/σ_min, which is unbounded.

E 0.2.2.4 — The double cover (diagnostic). A learner’s quaternion loss is ‖q − $q_gt$‖². The training oscillates between two values of q that represent the same rotation. What is the cause, and what is the fix?

S 0.2.2.4. The double cover — q and −q are the same rotation, and the entry loss has two minima (one at $q_gt$, one at −$q_gt$). The training can flip between the basins, and the loss jumps when it flips. The fix is a sign-invariant loss (min(‖q − $q_gt$‖², ‖q + $q_gt$‖²)) or a rotation loss (θ² with θ = arccos((tr(R(q)ᵀ $R(q_gt$)) − 1)/2)), which is sign-invariant by construction. The lesson: the quaternion’s entries are not the rotation; a loss on the entries has a spurious −q minimum, and the rotation loss is the fix.

E 0.2.2.5 — The slerp midpoint (numerical). q₀ = (1, 0, 0, 0), q₁ = (√2/2, 0, 0, √2/2) (a 90° z rotation). Compute the slerp midpoint.

S 0.2.2.5. q₀ · q₁ = √2/2. Ω = arccos(√2/2) = π/4. slerp(0.5) = (sin(π/8)/sin(π/4))·q₀ + (sin(π/8)/sin(π/4))·q₁. sin(π/8) = √(2 − √2)/2 ≈ 0.3827, sin(π/4) = √2/2 ≈ 0.7071. The coefficient is 0.3827/0.7071 ≈ 0.5412. slerp(0.5) ≈ 0.5412·(1, 0, 0, 0) + 0.5412·(0.7071, 0, 0, 0.7071) ≈ (0.5412 + 0.3827, 0, 0, 0.3827) = (0.9239, 0, 0, 0.3827). This is (cos(π/8), 0, 0, sin(π/8)), which is a 45° z rotation — the halfway rotation. ✓ The lesson: the slerp’s halfway quaternion is the halfway rotation, and the coefficient’s shape comes from the quaternion sphere’s great circle.

E 0.2.2.6 — The exp’s π singularity (diagnostic). A learner’s training is stable for 1000 steps and then has a large loss spike at step 1001. Their ω is the network’s output, and the loss is ‖exp(ω) − $R_gt$‖². What is the most likely cause, and what is the fix?

S 0.2.2.6. The ω has drifted near ‖ω‖ = π, and the spike comes from the log side of the round trip, not the exp side — provided the loss differentiates through log. The exp map is perfectly smooth at the half-turn: its gradient shrinks (measured 4.0 at π/2, 0.004 at π − 0.001), because the half-turn is exp’s extreme and the map is flat there. The unbounded factor is in log, where ∂ log/∂R grows as 1/(π − θ) (measured 1.57 at π/2, 3141 at π − 0.001), so a target near the half-turn produces an enormous gradient on whatever the loss differentiates. The fix is to clip on the log side — keep the target ω away from π — or use a two-chart switch to the quaternion near π. The diagnostic is to log ‖ω‖ of the target, not of the network’s output, and observe it approaching π before the spike. The lesson: the π singularity is a value issue on the log side, and knowing which side matters — a clip on ω when the gradient lives in ∂ log/∂R suppresses a gradient that was already too small.

E 0.2.2.7 — The interpolation choice (transfer probe). A learner wants to interpolate between two orientations for a robot arm’s end-effector. They use Euler angles because “it’s the representation I know.” What are the two failure modes of this choice, and what is the correct choice?

S 0.2.2.7. (i) Non-constant angular velocity — the Euler interpolation’s path is a straight line in the angle space, not a geodesic on SO(3), so the arm’s angular velocity varies along the path. (ii) A detour off the geodesic — if the path crosses β = ±π/2, the angles are no longer independent and the path wanders far from the shortest rotation between its endpoints; in the lab’s own example the endpoints are 8.10° apart and the Euler-linear path strays 4.41°, more than half the total rotation. Note that the speed does not jump — measured along the path it is smooth and merely dips. The failure is geometric, and a “look for the stutter” test will miss it. The correct choice is slerp (quaternion) or geodesic (exp coordinate), both of which give constant-angular-velocity paths and have no gimbal lock. The lesson: the interpolation is on SO(3), not on ℝ³, and the geodesic is the right path.

E 0.2.2.8 — The learning parameterization (constructive). A network’s final layer is nn.Linear(64, 3), and the 3 outputs are converted to R = exp(ω̂). Write the pseudocode for the forward pass and state what the output’s constraints are (guaranteed or not).

S 0.2.2.8.

class RotationHead(nn.Module):
    def __init__(self, in_dim):
        super().__init__()
        self.fc = nn.Linear(in_dim, 3)
    def forward(self, x):
        omega = self.fc(x)
        omega = clip_omega(omega, max_norm=pi - eps)   # optional but recommended
        R = exp_so3(omega)                              # (B, 3, 3)
        return R

The output R is guaranteed to satisfy RᵀR = I and det R = +1 (up to floating point), because exp maps any 3-vector to SO(3). The clip_omega is optional and its value depends on which side of the map the gradient lives: it bounds the exp-side gradient, which is already small near π (measured 0.004 at π − 0.001 against 4.0 at π/2), so clipping ω there suppresses a gradient that needs no suppression. What does need a bound is the log-side derivative, which diverges as 1/(π − θ) — so the clip belongs on any ω that will be fed into log_so3, not on the head’s output. The gradient ∂L/∂ω is well-defined by autograd’s chain rule, and it is bounded near ω = 0 (the common case). The lesson: the 3-parameter exp parameterization is the structurally correct learnable rotation; the defensive clip is for the log side, and putting it on the exp side is a plausible-looking mistake.


§L.9 — Exit test

You are ready for Lesson 0.2.3 when you can, without notes:

  1. State what a minimal parameterization of SO(3) is, and explain why every minimal chart has a singularity.
  2. State the four desiderata of a parameterization, and the trade-offs between them.
  3. State the 24 Euler conventions and the singularity at β = ±π/2 for a Tait-Bryan convention.
  4. Compute a ZYX triple’s matrix, and identify the singularity’s rank drop and non-uniqueness.
  5. State why Euler interpolation is not a rotation interpolation, and name the correct one.
  6. Apply Rodrigues’ formula to convert axis-angle to a rotation matrix, and verify the result for a 90° z rotation.
  7. State the quaternion-to-matrix formula and compute R(q) for a specific q.
  8. Compute the Hamilton product of two quaternions, and state the composition’s correspondence to matrix multiplication.
  9. State the double cover and its two consequences (sign-invariant loss, slerp’s sign choice).
  10. State the exp and log maps, and compute both for a specific rotation.
  11. State the two exp singularities (θ = 0 a chart defect, θ = π a two-to-one covering), and the learning choice (clip the log side, or use two charts).
  12. Diagnose a non-unit-quaternion, a gimbal-lock detour, and a π-singularity loss spike from their symptoms — including which side of the exp/log map each one lives on.

If any of items 1–12 is not answerable without notes, return to the section that introduced it: item 1 to §L.1.1, item 2 to §L.1, item 3 to §L.2, item 4 to §L.2.2, item 5 to §L.2.3, item 6 to §L.3.2, item 7 to §L.3.2, item 8 to §L.3, item 9 to §L.3.1, item 10 to §L.4, item 11 to §L.4.1, and item 12 to §L.5.


§L.10 — Provenance and evidence tiers

What this lesson reproduces. Every hypothesis in §L.6.5 is executed by the lab in §L.6.9. Tier A, status R.

ClaimTierStatusWhere it is reproduced
The Euler chart’s Jacobian drops rank at gimbal lock, and the numerical rank is 2, not 3ARH2’s singular values [2.0, 1.414, 2.76e-12], rank 2
Slerp tracks the geodesic exactly; nlerp is exact at the midpoint and errs off itARH3’s slerp t=¼: 0.00e+00, nlerp t=¼: 1.46e-02, both ≈0 at t=½
A non-unit quaternion produces a matrix that is not a rotation, and its determinant depends on the axis, not on ‖q‖ aloneARH4’s det(R(2q)) = 25.0 for z vs 37.0 for a generic axis
The π singularity lives in the log map; the exp map’s gradient vanishes thereARH5’s exp grad 0.004 at π vs 4.0 at π/2; log derivative ratio 1999.4
Euler interpolation is not a rotation interpolation: it leaves the geodesic and takes a detourARbreak-it #2’s 8.10° endpoint error and 4.41° detour

What this lesson does not reproduce. The claim that a random initialization can place ‖ω‖ near π — and therefore that the log-side singularity is a training hazard rather than a curiosity — is stated as a plausibility argument, not measured. It is tier C, status P. The lab measures the singularity’s existence and its direction (exp’s gradient falls, log’s rises); it does not measure how often training lands near it. A learner who wants that number should instrument their own training run.

Two results here are exact and two are not, and the lesson separates them. H2’s rank, H3’s slerp error, and H4’s determinants are exact statements about floating-point arithmetic on specific inputs — they have no noise, and 2.76e-12 is a zero. H5’s derivative ratio 1999.4 is a finite-difference estimate at π − 0.001, so it is a measurement of the rate at one offset, not a proof of divergence; the divergence is analytic, and the number is a check on it. The lesson quotes the number for the check, not as the result.

No external sources are cited. Rodrigues’ formula, the Hamilton product, and slerp are stated as formulas and then evaluated; the reader can check every one against the lab. Where the lesson attributes a construction to a name, the name identifies the formula, not an authority the argument leans on.