Nyimbi Odero

Series 01 · a numbered run · kept in public

X1.1.1

Chapter: Phase 1 — Foundations of Physical Intelligence · Module 1.1 — Robot Modeling and Classical Control

Lesson 1.1.1 — Forward Kinematics: The Product-of-Exponentials Formula and the Denavit–Hartenberg Convention

Week 5 · Prerequisites: 0.1.1, 0.1.2, 0.1.3, 0.2.1, 0.2.2, 0.2.3, 0.2.4.

Prior results used. This lesson uses 0.2.1’s SE(3) composition and point/vector distinction, 0.2.2’s elementary rotations and the SO(3) exponential, 0.2.3’s twist, product-of-exponentials, and the home configuration M, and 0.2.4’s from-to convention and frame audit. Each is restated at the point of use.

Hands forward. This lesson hands forward one capability: the learner can write the FK of any serial arm in both the DH and PoE forms, verify the two agree, build the geometric Jacobian from the same frame data, and diagnose the four FK-specific bugs (wrong DH sign, wrong α, chain-order, and joint-axis mis-identification) from their end-effector symptoms — the capability that Lesson 1.1.2 (inverse kinematics), 1.1.3 (control), and every Jacobian-based lesson in Phase 1–4 assumes.

Running case. The same 6-DOF MuJoCo arm from 0.2.1–0.3.4, now analyzed exhaustively: six revolute joints, alternating z/y spatial axes at q = 0, equal link lengths a = 0.2, end-effector at the tip of the sixth link. Its FK is written in three equivalent ways — the chain T = ∏ T_i from 0.2.1, the PoE T = ∏ exp(ξ̂_i q_i) · M from 0.2.3, and the DH chain T = ∏ DH_i(θ_i, d_i, a_i, α_i) introduced here — and this lesson’s central result is that all three give the same pose, and each has a distinct debugging and pedagogical advantage.


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

§L.0.1 — Lesson map

PartContent
AThe four DH parameters; frame assignment rules; the DH transform
BPoE vs. DH: the two FK formulations, their equivalence, their trade-offs
CThe geometric Jacobian from the FK frame data (spatial-twist form)
DThe running case in DH form and the PyTorch implementation
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 the four DH parameters (θ_i, d_i, a_i, α_i) and the physical meaning of each: the joint angle, the link offset, the link length, and the link twist.
  2. Apply the DH frame-assignment rules to a given kinematic chain, and derive the DH table for the running case.
  3. Write the DH transform T_i^{i-1} = Rot_z(θ_i) · Trans_z(d_i) · Trans_x(a_i) · Rot_x(α_i) and compute its 4×4 matrix by hand for a specific (θ, d, a, α).
  4. Compare the DH and PoE forms — their storage (a table vs. a set of twists and a home pose), their numerical sensitivity (the DH table’s parameters are physical, while the twists are frame-dependent), and their debugging (DH exposes the individual joint angles; PoE separates the geometry from the configuration).
  5. Derive the geometric Jacobian from either form: J = [J_v; J_ω] with J_ω,i = ξ_i[0:3] and J_v,i = ξ_i[3:6] + ξ_i[0:3] × (p_ee − q_i), where ξ_i is the joint’s spatial twist and q_i is a point on its axis.
  6. Diagnose the four FK-specific bugs — wrong DH α sign, wrong DH a sign, chain order reversed, joint axis mis-identified — from their end-effector symptoms.
  7. Implement the running case’s FK in PyTorch as a DH chain, and verify it matches the PoE from 0.2.3 at a non-trivial configuration.
  8. Produce a “kinematic model audit” of any serial arm: its DH table, its PoE twists and home pose, the geometric Jacobian, and the frame conventions for each.

§L.0.3 — Misconception inventory

Wrong model the learner probably holdsWhy it is attractiveWhere displacedThe correct model in one sentence
M1 — “DH is just a convention; PoE is the real math.”PoE is the modern, frame-free formulation, and DH has four parameters per joint, which looks like notation overhead.§L.2 (Part B)DH and PoE are equivalent FK formulations, and each exposes something the other hides: DH exposes the physical link parameters (a table a machinist can machine to), while PoE exposes the separation between the geometry (the twists and home pose) and the configuration (the angles); the choice is the task, and most industrial robots ship with DH tables because that is what the manufacturer specifies.
M2 — “The DH parameters are unique for a given robot.”A robot’s physical structure is unique, so its DH table feels unique.§L.1 (Part A)The DH parameters are not unique — the frame assignment rules leave two choices at each joint (the direction of z, and the origin along the axis), and different choices give different DH tables that describe the same robot; the Jacobian and the end-effector pose are invariant to the choice, and the DH table’s non-uniqueness is a convention, not a fact about the robot.
M3 — “The α sign is cosmetic.”The α rotation is about the x-axis, and the sign is a small detail.§L.2 and §L.4 (Parts B and D)The α sign determines which spatial axis the next joint rotates about; flipping α from −π/2 to +π/2 makes joint i+1 rotate about −y instead of +y, which reverses the joint’s effect on the end-effector and makes the FK a mirror image of the correct arm — the bug is silent at q = 0 (the rotations are identity) and catastrophic at q ≠ 0.
M4 — “The Jacobian is ∂p/∂q, the derivative of the position.”The FK is a function of q, so the Jacobian feels like its gradient.§L.3 (Part C)The geometric Jacobian is [J_v; J_ω] — six rows (three for the linear velocity of the end-effector, three for its angular velocity) — not the 3×n matrix ∂p/∂q; the 3×n matrix is the position Jacobian, and it is only half of the geometric Jacobian; the angular part J_ω is what the controller needs for orientation, and it is not derivable from ∂p/∂q.
M5 — “The Jacobian’s columns are the joint’s twist.”The joint’s spatial twist ξ_i is a 6-vector, the Jacobian’s column J_i is a 6-vector, and they have the same shape.§L.3 (Part C)The Jacobian’s column J_i is the twist of the joint’s motion evaluated at the end-effector, which is ξ_i transported to the end-effector’s point by the spatial adjoint — `J_i = Ad_{T_{ee

Each misconception is displaced in the section named.

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 thirteenth lesson. The prompt requires drawing from lessons 2–5 steps back; 0.3.4 is one step back and is excluded. The warm-up draws from 0.2.3 (ten steps back), 0.2.2 (eleven), 0.2.1 (twelve), and 0.1.3 (ten), all of which are prerequisites of this lesson.

Warm-up 1 (from 0.2.3, the PoE and the home configuration). Write the PoE’s FK formula, and state what M and each ξ_i represent. Answer: T(q) = exp(ξ̂₁q₁) · exp(ξ̂₂q₂) · … · exp(ξ̂₆q₆) · M, where M is the end-effector’s pose at q = 0 and each ξ_i is the spatial twist of joint i at the home configuration (the joint’s axis and a point on it, in the base frame). How this is used: §L.2’s equivalence proof shows that the DH chain and the PoE chain give the same T(q), and the DH table’s (θ_i, d_i, a_i, α_i) are the parameters of a specific choice of M and ξ_i — the DH convention is a parameterization of the PoE’s geometry.

Warm-up 2 (from 0.2.2, the elementary rotations). Write Rz(θ) and Rx(α), and state which axis each fixes. Answer: Rz(θ) = [[cos θ, −sin θ, 0], [sin θ, cos θ, 0], [0, 0, 1]] (fixes z); Rx(α) = [[1, 0, 0], [0, cos α, −sin α], [0, sin α, cos α]] (fixes x). How this is used: §L.1’s DH transform is Rot_z(θ) · Trans_z(d) · Trans_x(a) · Rot_x(α) — the two rotations are exactly these elementary matrices, and the DH’s four parameters are the arguments of the two rotations and the two translations.

Warm-up 3 (from 0.2.1, the composition rule and the point/vector distinction). For T_A^B = (R_A^B, t_A^B) and T_B^C = (R_B^C, t_B^C), write T_A^C and state which frame each translation is in. Answer: T_A^C = (R_A^B R_B^C, R_A^B t_B^C + t_A^B), and t_B^C is in B’s frame, t_A^B in A’s. How this is used: §L.1’s DH transform’s translation Trans_z(d) · Trans_x(a) is (R_z, [a cos θ, a sin θ, d]) — the a and d are rotated by θ before being added, which is the R_A^B t_B^C term of the composition rule.

Warm-up 4 (from 0.1.3, nn.Module and state_dict). A DH table’s parameters (a_i, α_i, d_i) are fixed for a given robot, and the joint angles θ_i are variable. What is the registration of the fixed parameters in a PyTorch nn.Module? Answer: The fixed parameters are buffers (register_buffer), not parameters — they are not learnable, but they must be moved by .to(device) and saved in state_dict(). The joint angles are inputs (not state). How this is used: §L.4’s DHChain module registers the DH table as buffers and takes q as the forward pass’s input, which is the standard pattern for a kinematic model in PyTorch.


Part A — The Denavit–Hartenberg Parameters

§L.1 — Objects: the four parameters, the frame assignment, and the DH transform

The Denavit–Hartenberg (DH) convention assigns a frame to each link of a serial kinematic chain, with four parameters per frame:

  1. θ_i (joint angle) — the rotation about the z-axis of frame i-1. For a revolute joint, θ_i is the variable (the joint’s angle); for a prismatic joint, θ_i is a constant (usually 0).
  2. d_i (link offset) — the translation along the z-axis of frame i-1. For a prismatic joint, d_i is the variable; for a revolute joint, d_i is a constant.
  3. a_i (link length) — the translation along the x-axis of frame i (which is the common perpendicular to z_{i-1} and z_i). It is the length of the link between joint i-1 and joint i.
  4. α_i (link twist) — the rotation about the x-axis of frame i, aligning z_{i-1} to z_i.

The DH transform from frame i to frame i-1 is:

Tii−1=Rotz(θi)⋅Transz(di)⋅Transx(ai)⋅Rotx(αi)T_i^{i-1} = \text{Rot}_z(\theta_i) \cdot \text{Trans}_z(d_i) \cdot \text{Trans}_x(a_i) \cdot \text{Rot}_x(\alpha_i)

which expands to the 4×4 matrix:

Tii−1=[cos⁡θi−sin⁡θicos⁡αisin⁡θisin⁡αiaicos⁡θisin⁡θicos⁡θicos⁡αi−cos⁡θisin⁡αiaisin⁡θi0sin⁡αicos⁡αidi0001]T_i^{i-1} = \begin{bmatrix} \cos\theta_i & -\sin\theta_i \cos\alpha_i & \sin\theta_i \sin\alpha_i & a_i \cos\theta_i \\ \sin\theta_i & \cos\theta_i \cos\alpha_i & -\cos\theta_i \sin\alpha_i & a_i \sin\theta_i \\ 0 & \sin\alpha_i & \cos\alpha_i & d_i \\ 0 & 0 & 0 & 1 \end{bmatrix}

The frame assignment rules (the standard DH convention, distal parameters):

  1. z_{i-1} is the axis of joint i (the joint’s rotation or translation axis). For a revolute joint, z_{i-1} is the rotation axis; for a prismatic joint, it is the translation direction.
  2. x_i is the common perpendicular to z_{i-1} and z_i, oriented from z_{i-1} toward z_i. If the axes are parallel (a prismatic or a cylindrical joint), x_i is chosen along the perpendicular from z_{i-1}’s origin; if the axes intersect, x_i is along the common normal at the intersection.
  3. y_i completes the right-handed frame (y_i = z_i × x_i).
  4. o_i (the origin) is at the intersection of x_i and z_i (if they intersect) or at the foot of the common perpendicular from z_i to z_{i-1} (if they are parallel).
  5. o_0 and x_0 are chosen to align with the base frame (usually o_0 at the base, z_0 along joint 1’s axis, x_0 arbitrary but fixed).

The physical interpretation of each parameter:

  • θ_i — how much joint i has rotated about its axis. It is the configuration variable for a revolute joint.
  • d_i — how far along the axis the next frame is. It is zero for most revolute arms (unless the joint has a linear offset), and it is the configuration variable for a prismatic joint.
  • a_i — the link’s length (along the common perpendicular). It is the physical dimension of the rigid link.
  • α_i — the link’s twist (the angle between consecutive joint axes). It is zero for parallel axes, ±π/2 for perpendicular axes, and the value is determined by the physical design.

The DH convention’s four parameters encode the entire kinematics of a serial chain, and the FK is the product T_0^n = ∏ T_i^{i-1}.

§L.1.1 — Result: the DH parameters are not unique, but the end-effector pose is invariant to the choice

Claim. For a serial chain with a given physical structure, the DH table (θ_i, d_i, a_i, α_i) is not unique — the frame assignment leaves two choices at each joint (the sign of z_{i-1}, and the origin o_{i-1} along the axis) — but the end-effector pose T_0^n(q) = ∏ T_i^{i-1}(q_i) is invariant to the choice (for the same physical robot and joint angles).

Proof sketch. The two choices at each joint (the sign of z and the origin’s location along z) correspond to a rigid transformation of the frames, which is absorbed by the DH parameters of the adjacent links. The end-effector’s pose in the base frame is a function of the joint angles and the physical link parameters, and the physical parameters are the same regardless of the frame convention; the DH table’s individual entries change, but their product is the same pose. The invariance is the frame-invariance of the pose, applied to the FK.

Corollary. A robot’s DH table is a convention, not a fact — two engineers can produce different tables for the same robot, and both are correct. The table’s parameters (except the angles) are the same physical lengths and twists in a different frame, and the FK is the same pose. The implication for debugging: a DH table that doesn’t match a manufacturer’s is not necessarily wrong; it may be a different convention for the same robot.

Corollary. The non-uniqueness has a practical consequence: the DH table’s entries are hard to compare across sources, and the cross-check must be the FK’s output, not the table’s entries. The correctness test for a DH table is T_0^n(q) = T_0^n(q) for the same q under a reference implementation — the table’s entries are not the test.

§L.1.2 — Worked-example ladder

WE-L.1.a — Fully worked: the DH transform for a single joint, by hand

Input. θ = π/6, d = 0.1, a = 0.2, α = −π/2. Compute T_i^{i-1}’s 4×4 matrix.

Trace.

  1. cos θ = cos(π/6) = √3/2 ≈ 0.866, sin θ = 0.5.
  2. cos α = cos(−π/2) = 0, sin α = −1.
  3. Row 0: [cos θ, −sin θ · cos α, sin θ · sin α, a · cos θ] = [0.866, −0.5·0, 0.5·(−1), 0.2·0.866] = [0.866, 0, −0.5, 0.1732].
  4. Row 1: [sin θ, cos θ · cos α, −cos θ · sin α, a · sin θ] = [0.5, 0.866·0, −0.866·(−1), 0.2·0.5] = [0.5, 0, 0.866, 0.1].
  5. Row 2: [0, sin α, cos α, d] = [0, −1, 0, 0.1].
  6. Row 3: [0, 0, 0, 1].

So T_i^{i-1} = [[0.866, 0, −0.5, 0.1732], [0.5, 0, 0.866, 0.1], [0, −1, 0, 0.1], [0, 0, 0, 1]].

Output and interpretation. The transform’s rotation is Rz(π/6) · Rx(−π/2) — the composition of a 30° rotation about z and a −90° rotation about x. Its translation is [0.1732, 0.1, 0.1] — the a · cos θ and a · sin θ are the x- and y-components of the link’s a along the rotated x-axis, and d = 0.1 is along z. The physical reading: the link is 0.2 long, displaced 0.1 along the joint axis, and twisted by −90° — the next joint’s axis is perpendicular to this one.

Self-explanation 1. Before reading on: in step 3, why is a · cos θ the x-component of the translation, and why is a · sin θ the y-component?

Answer. The Trans_x(a) translates along the intermediate x-axis, which is the x after Rot_z(θ) and Trans_z(d). The Rot_z(θ) rotates the base x into a new x that makes an angle θ with the base x, so a along the new x has base-frame components a cos θ and a sin θ. The rotation of the translation is the composition rule’s R_A^B t_B^C term (from 0.2.1’s warm-up 3), and it is why the DH table’s a is not simply a in the base frame — it is a rotated by θ and α.

Self-explanation 2. Before reading on: in step 5, sin α = −1 and cos α = 0, so the row is [0, −1, 0, d]. What does this rotation Rx(−π/2) do to the z-axis, and why is it called a “link twist”?

Answer. R_x(α)·[0,0,1] = [0, −sin α, cos α]. For α = −π/2 that is [0, −sin(−π/2), cos(−π/2)] = [0, 1, 0] = +y, so R_x(−π/2) sends z to +y (and R_x(+π/2) sends it to −y). That image is the direction of the next joint’s axis: z_i is perpendicular to z_{i-1} whenever |α| = π/2. The “link twist” is the relative orientation of consecutive joint axes, and it is a physical property of the robot — a link’s shape twists its downstream joint out of line with its upstream one.

WE-L.1.b — Completion problem

Input. θ = π/4, d = 0, a = 0.3, α = +π/2.

Trace (final 25% as prompts).

  1. cos θ = sin θ = √2/2 ≈ 0.707.
  2. cos α = 0, sin α = 1.
  3. Row 0: [cos θ, −sin θ·cos α, sin θ·sin α, a·cos θ] = [0.707, 0, 0.707, 0.212]. ⟨your turn: verify⟩
  4. Row 1: [sin θ, cos θ·cos α, −cos θ·sin α, a·sin θ] = [0.707, 0, −0.707, 0.212]. ⟨your turn⟩
  5. Row 2: [0, sin α, cos α, d] = [0, 1, 0, 0]. ⟨your turn⟩
  6. Row 3: [0, 0, 0, 1]. ⟨your turn⟩

Answers.

  1. 0.707 for both. ✓
  2. cos(π/2) = 0, sin(π/2) = 1. ✓
  3. [0.707, −0.707·0, 0.707·1, 0.3·0.707] = [0.707, 0, 0.707, 0.212]. ✓
  4. [0.707, 0.707·0, −0.707·1, 0.3·0.707] = [0.707, 0, −0.707, 0.212]. ✓
  5. [0, 1, 0, 0]. ✓

So T = [[0.707, 0, 0.707, 0.212], [0.707, 0, −0.707, 0.212], [0, 1, 0, 0], [0, 0, 0, 1]].

The lesson. The α sign flips the sin α term and hence the third column of the rotation, which is z_i expressed in frame i−1: with α = +π/2 the third column is [sin θ, −cos θ, 0] = [0.707, −0.707, 0], so the next joint’s axis lies in the x_{i-1}y_{i-1} plane, at −45° from the x axis. With α = −π/2 the same row reads [−0.707, 0.707, 0] — the opposite direction along the same line, which is the sign reversal the misconception M2 warns about. The physical content is fixed (the axes stay perpendicular); only the labeling of the frame flips, which is why the FK pose is invariant.

WE-L.1.c — Faded: the running case’s DH table

Setup. The running case’s 6-DOF arm has alternating z and y spatial axes at q = 0, equal link lengths a = 0.2, and zero offsets d = 0. The end-effector is at the tip of the 6th link.

Question. Write the DH table’s (a_i, α_i, d_i) entries (all θ_i are the joint angles q_i), and verify that the table’s FK at q = 0 gives (6a, 0, 0) = (1.2, 0, 0).

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

Answer. The frame assignment gives z_{i-1} along the joint i’s axis. With joint 1 about z, joint 2 about y, the twist α_1 must align z to y — Rx(α_1)·z = y gives α_1 = −π/2 (from the earlier derivation). Alternating: α_1 = −π/2, α_2 = +π/2, α_3 = −π/2, α_4 = +π/2, α_5 = −π/2, α_6 = +π/2.

The sixth entry is the one that trips people up. The chain’s end-effector frame is the same frame the PoE uses as M = Trans_x(6a), and that frame is not rotated relative to the base — so the total twist ∏ Rx(α_i) must be the identity. The first five alternate −,+,−,+,−, which compose to Rx(−π/2), so α_6 = +π/2 closes the product to the identity. Setting α_6 = 0 leaves the whole chain’s orientation off by Rx(−π/2): the position at q = 0 is still (1.2, 0, 0), but the pose is wrong, and every downstream comparison fails by a constant rotation.

iθ_id_ia_iα_i
1q_100.2−π/2
2q_200.2+π/2
3q_300.2−π/2
4q_400.2+π/2
5q_500.2−π/2
6q_600.2+π/2

At q = 0: T_i = Trans_x(0.2) · Rot_x(α_i). The Trans_x(0.2) puts the next frame’s origin at 0.2 in the current x. The Rot_x(α_i) twists z, but the origin is still at 0.2 in the current frame. The next frame’s x direction is the rotated x — for α = ±π/2, Rx(±π/2) maps x to x (the rotation is about x, so x is fixed). So T_i(0) · [0;0;0;1] = [0.2, 0, 0, 1] for every i, and T_0^6(0) · [0;0;0;1] = (1.2, 0, 0, 1). ✓

The lesson. The α_i = ±π/2 alternating pattern is the DH encoding of the alternating joint axes — the +π/2 and −π/2 are the signs that make the chain’s z’s alternate between z and y. The a_i = 0.2 for all i is the equal link length, and the d_i = 0 is the zero offset (each joint’s origin is on the previous joint’s plane). The chain’s FK at q = 0 is (1.2, 0, 0) — the fully extended arm — and this is the first test any DH table must pass. But a position-only check passes with the wrong α_6 too, which is why the first test must compare the full pose, not just the translation.

WE-L.1.d — Contrastive near-miss: flipping the α signs

Correct solution. The running case’s DH table uses α = [−π/2, +π/2, −π/2, +π/2, −π/2, +π/2], alternating.

Near-miss. The learner uses α = [+π/2, −π/2, +π/2, −π/2, +π/2, 0] — the flipped signs.

Why the near-miss produces a plausible but incorrect number. At q = 0, the two tables give the same end-effector position (1.2, 0, 0), because the Trans_x(a) is rotation-invariant about x, and the Rot_x(±π/2) maps x to x. So the q = 0 test passes for both. At q ≠ 0, the flipped α’s make the next joint’s axis point in the opposite direction, so joint 2 rotates about −y instead of +y, and the end-effector is at a different position — the FK is a mirror image of the correct arm. The error is silent at q = 0 and systematic at q ≠ 0, with the sign of the error reversed for each joint.

The discrepancy, stated. α_i = −π/2 and α_i = +π/2 differ by a rotation of π about the x-axis, which reverses the z_i axis (from y to −y). The joint i+1’s rotation axis is z_i, so the flipped α makes joint i+1 rotate about −y instead of +y — the joint angle’s effect is reversed. The bug is invisible at q = 0 (the joint rotations are identity) and catastrophic at q ≠ 0 (the joint’s motion is backwards).

Self-explanation 3. Before reading on: in the near-miss, at q = [0, π/2, 0, 0, 0, 0], where is the end-effector with the correct α vs. the flipped α?

Answer. With the correct α, joint 2 rotates about +y, and the end-effector is at (a, 0, −5a) = (0.2, 0, −1.0) — the arm is bent downward at joint 2. With the flipped α, joint 2 rotates about −y, and the end-effector is at (a, 0, +5a) = (0.2, 0, +1.0) — the arm is bent upward at joint 2. The two end-effectors are at mirror positions across the z = 0 plane, and the difference is (0, 0, 2·5a) = (0, 0, 2.0) — a full meter of displacement from a sign flip of α.

§L.1.3 — Comparative table: the four DH parameters

ParameterAxis and slot in the transformPhysical meaningVariable or fixedWhat a wrong value breaks
θ_irotation about z_{i-1}; first in the producthow far joint i has turnedvariable for a revolute joint; a constant (usually 0) for a prismatic onethe whole downstream chain turns by the wrong angle; invisible at q = 0, where every θ is zero
d_itranslation along z_{i-1}; secondhow far along the joint axis the next frame sitsvariable for a prismatic joint; a constant for a revolute onethe reach along the axis; invisible when the true offset is zero
a_itranslation along x_i; thirdthe link’s length along the common perpendicularalways fixedthe q = 0 reach, which is Σ a_i — a missing or doubled entry moves the sum
α_irotation about x_i; fourththe twist between consecutive joint axesalways fixedwhich spatial axis the next joint rotates about; the q = 0 translation is unchanged, so a position-only test passes

Design principle (P13.5). The variable slot identifies the joint type. A revolute joint varies θ_i and holds d_i; a prismatic joint varies d_i and holds θ_i; a table that varies both at one joint describes a two-DOF joint, and a table that varies neither describes a rigid link. Read the joint type off the variable slot before computing anything: at q = 0 both candidates give the same transform, so a swapped slot is invisible until the joint moves, and then every downstream frame moves along the wrong axis — the arm translates where it should turn.

§L.1.4 — Concept check

Concept check (displaces M2). A learner says: “My DH table disagrees with the manufacturer’s, so mine must be wrong — I’ll keep changing the frame assignment until the entries match.” What has the learner failed to model, and what class of bug does the chasing produce?

Answer. The learner has failed to model frame non-uniqueness: the assignment rules leave two choices at every joint — the sign of z_{i-1} and the origin’s location along the axis — and each choice rewrites the table while leaving the end-effector pose untouched. The table’s entries are therefore not a test, and matching them is a test of convention rather than of the robot. The bug class is a table tuned to the poses already in front of you: every edit fixes the comparison being looked at and silently changes a downstream link’s twist, so the arm matches at the configurations that were checked and is a different robot at the ones that were not. The fix is to fix the convention in writing, then compare T(q) at a non-zero q against an independent implementation — the pose is invariant, the entries are not.

§L.1.5 — Consolidation checkpoint 1

(a) You should now be able to: (i) name the four parameters, the axis each acts about or along, and its slot in the product; (ii) apply the frame-assignment rules to a chain and read the table off the frames rather than off a known pose; (iii) state which parameter is the variable for a revolute joint and which for a prismatic one.

(b) Transfer probe. A colleague hands you a DH table for a 3-DOF arm with α = [0, 0, 0] and d = [0.1, 0, 0], and says “the q = 0 reach checks out at 0.6, so the table is right.” What has the reach test actually confirmed, and what has it left untouched? Answer sketch: the q = 0 reach is Σ a_i — the Rot_x(α_i) rotations leave the x-axis fixed, so the reach constrains only the link lengths and says nothing about the twists. A table of all-zero twists is a planar arm, and the reach test passes for it exactly as it passes for the spatial arm. The learner who took the reach as evidence the table is right has run a test of the lengths and read it as a test of the geometry.

(c) Remediation rule. If you cannot do (a)(ii) or (b), return to §L.1.2 and redo WE-L.1.a, then re-attempt (b) without notes.


Part B — PoE vs. DH

§L.2 — Objects: the two formulations and their equivalence

The Product-of-Exponentials form (from 0.2.3) is:

T(q)=exp⁡(ξ^1q1)exp⁡(ξ^2q2)…exp⁡(ξ^nqn) MT(q) = \exp(\hat{\xi}_1 q_1) \exp(\hat{\xi}_2 q_2) \dots \exp(\hat{\xi}_n q_n) \, M

where ξ_i = (ω_i, v_i) ∈ ℝ⁶ is the spatial twist of joint i at the home configuration (q = 0), and M ∈ SE(3) is the end-effector’s pose at q = 0.

The Denavit–Hartenberg form is:

T(q)=∏i=1nDHi(θi,di,ai,αi)T(q) = \prod_{i=1}^{n} DH_i(\theta_i, d_i, a_i, \alpha_i)

where each DH_i is a 4×4 transform given by the DH formula, θ_i is the joint angle (for a revolute joint), and (d_i, a_i, α_i) are the fixed link parameters.

The equivalence: for the same robot, there is a bijection between a DH table and a PoE (M, {ξ_i}) — every DH chain has an equivalent PoE, and every PoE with physical twists has an equivalent DH table. The DH table’s parameters determine the twists and M, and the PoE’s twists and M determine the DH table (up to the non-uniqueness of the frame assignment).

The trade-offs:

AspectDHPoE
StorageA table of n rows × 4 columnsn twists (6 numbers each) + M (12 non-trivial numbers)
InterpretabilityThe parameters are physical (link length, twist, offset) — a machinist can machine to themThe twists are frame-dependent — they depend on the base frame’s choice
FK evaluationn DH transforms, each 4×4 (the Rot_z, Trans_z, Trans_x, Rot_x sequence)n exp(ξ̂_i q_i) evaluations (each a 4×4 via Rodrigues), plus M
DifferentiationThe DH transform’s derivative w.r.t. θ_i is simple (a ∂T/∂θ of the Rot_z matrix)The exp map’s derivative w.r.t. q_i is the left Jacobian J(ξ_i q_i) (from 0.2.3)
Manufacturer compatibilityYes — industrial robots ship with DH tablesNo — the manufacturer’s data is in DH, and the PoE twists are derived
Home configuration MImplicit in the table (the a_i, α_i determine it at q = 0)Explicit (a separate matrix)
Underactuated / soft robotsHard to represent (needs a fixed number of joints)Easy — the twists can be learned and the chain’s length can vary
Number of parameters4n (with n variables for revolute joints)6n + 12 (with n variables) — more parameters, but less redundant (each twist has only 3 DOF in the joint’s axis + moment)

The practical guidance:

  • Use DH when the robot is rigid, serial, and physical — the parameters are the manufacturer’s data, and the DH table is the standard input to a controller.
  • Use PoE when the robot is modular, soft, or learned — the twists can be estimated from data, and the chain’s structure is not fixed.
  • Use both for debugging — the two formulations’ independence is a cross-check: a bug in one is not a bug in the other, and the agreement of the two is a strong test of the FK’s correctness.

§L.2.1 — Result: the DH and PoE formulations are equivalent, and each exposes something the other hides

Claim. For any serial chain with revolute or prismatic joints, the DH product ∏ DH_i and the PoE ∏ exp(ξ̂_i q_i) · M give the same T(q) for the same joint configuration, provided the (M, {ξ_i}) are derived from the DH table. The derivation is M = DH_1(0) · DH_2(0) · … · DH_n(0) and ξ_i = Ad_{DH_1(0) · … · DH_{i-1}(0)} · ξ_i^{body}, where ξ_i^{body} is the joint’s body twist (the joint’s axis in its own frame).

Proof sketch. Each DH_i(q_i) can be written as DH_i(0) · exp(ξ̂_i^{body} q_i) where ξ_i^{body} is the body twist of joint i at q = 0 (for a revolute joint, the z-axis through the origin in frame i). Substituting into the product gives ∏ DH_i(0) · exp(ξ̂_i^{body} q_i). Moving the DH_j(0) terms to the left of the exp (which requires the adjoint Ad_{DH_j(0)} to conjugate the twist), the product becomes DH_1(0) · exp(Ad_{DH_1(0)^{-1}} ξ̂_1^{body} q_1) · DH_2(0) · …, and after re-arranging, the spatial twist ξ_i = Ad_{DH_1(0) · … · DH_{i-1}(0)} ξ_i^{body} is the conjugated body twist. The result is the PoE with M = ∏ DH_i(0) and ξ_i as the spatial twists.

Corollary. The DH table’s physical parameters (link length, twist, offset) are readable by a human and matchable to the manufacturer’s data, while the PoE’s twists and M are frame-dependent and not physical. The DH’s interpretability is its main advantage, and the PoE’s frame-freedom is its main advantage.

Corollary. The conversion DH → PoE is algorithmic (the adjoints), and the conversion PoE → DH is not (it requires the frame assignment choices, which are not unique — see M2). The DH’s non-uniqueness is the cost of its physical interpretability.

§L.2.2 — Worked-example ladder

WE-L.2.a — Fully worked: the running case’s PoE twists from the DH table

Input. The running case’s DH table (from WE-L.1.c). Compute M and the six spatial twists ξ_i.

Trace.

  1. At q = 0, each DH_i(0) = Trans_x(0.2) · Rot_x(α_i). The M = ∏ DH_i(0) · Trans_x(0) (no extra end-effector offset; the last a_6 = 0.2 is already in DH_6).
  2. At q = 0, all DH_i(0) have the same Trans_x(0.2) translation (rotations about x don’t change x), so M = Trans_x(6 · 0.2) = Trans_x(1.2). ✓
  3. The body twist of joint i is ξ_i^{body} = (ω_i, 0) where ω_i is the joint axis in frame i’s predecessor. For joint 1, ω_1 = ẑ = (0,0,1). For joint 2, ω_2 = ŷ = (0,1,0) (since joint 2 is about z_1 = y_0 after α_1 = −π/2). Etc.
  4. The spatial twist is ξ_i = Ad_{DH_1(0) · … · DH_{i-1}(0)} ξ_i^{body}. For joint 1, ξ_1 = ξ_1^{body} = (0,0,1, 0,0,0) (no prior transform). For joint 2, Ad_{Trans_x(0.2)} ξ_2^{body} = (ω_2, t × ω_2) where t = (0.2, 0, 0) and ω_2 = (0,1,0), giving t × ω_2 = (0.2, 0, 0) × (0, 1, 0) = (0, 0, -0.2). Wait, (0.2,0,0) × (0,1,0) = (0·0 - 0·1, 0·0 - 0.2·0, 0.2·1 - 0·0) = (0, 0, 0.2). So ξ_2 = (0, 1, 0, 0, 0, 0.2) — matching the 0.2.3 PoE twist ξ_2 = (0, 1, 0, 0, 0, 0.2). ✓
  5. The other twists follow the same pattern, and they match the 0.2.3 twists (the ξ_1 = (0,0,1,0,0,0), ξ_3 = (0,0,1,0,−0.4,0), ξ_4 = (0,1,0,0,0,0.6), etc.).

Output and interpretation. The DH-derived PoE has M = Trans_x(1.2) and twists (0,0,1,0,0,0), (0,1,0,0,0,0.2), (0,0,1,0,−0.4,0), (0,1,0,0,0,0.6), (0,0,1,0,−0.8,0), (0,1,0,0,0,1.0) — exactly the 0.2.3 twists, confirming the equivalence. The (0,0,1) vs (0,1,0) alternation is the DH’s ±π/2 twists, and the a = 0.2 offsets give the 0.2, 0.4, 0.6, 0.8, 1.0 positions.

Self-explanation 4. Before reading on: in step 2, why is M = Trans_x(1.2) rather than Trans_x(6a) · Rot(α_6)?

Answer. The M is the end-effector’s pose at q = 0, and it includes all the fixed rotations and translations. For the running case, the end-effector is at the tip of the 6th link, which is at Trans_x(0.2) after the 6th joint — the 6th a_6 is part of M, not a separate end-effector offset. The six twists compose to ∏ Rx(α_i) = Rx(−π/2 + π/2 − π/2 + π/2 − π/2 + π/2) = I, so the end-effector frame is unrotated relative to the base and M = Trans_x(1.2) is the full pose at q = 0. If α_6 were 0 the product would be Rx(−π/2) and M would carry that rotation too — the frame would not be the PoE’s M, and the two formulations would disagree by a constant rotation. If the end-effector had a different orientation (e.g., a tool flange), M would include that as well.

WE-L.2.b — Completion problem

Input. A 3-DOF planar arm with a_1 = 0.2, a_2 = 0.15, a_3 = 0.1, all revolute about z. The DH table: θ_i = q_i, d_i = 0, a_i = (0.2, 0.15, 0.1), α_i = 0.

Trace (final 25% as prompts).

  1. Write the 3×3 block of T_0^3(q) at q = 0. ⟨your turn⟩
  2. What is the end-effector’s position at q = 0? ⟨your turn⟩
  3. Write the PoE form’s M and ξ_i. ⟨your turn⟩
  4. What is the end-effector’s position at q = [π/2, 0, 0]? ⟨your turn⟩

Answers.

  1. At q = 0, R = I (all rotations identity), and t = [a_1 + a_2 + a_3, 0, 0] = [0.45, 0, 0]. So T_0^3(0) = [[I, [0.45, 0, 0]], [0, 1]].
  2. (0.45, 0, 0).
  3. M = Trans_x(0.45); ξ_1 = (0,0,1,0,0,0) (joint 1 at origin), ξ_2 = (0,0,1,0,−0.2,0) (joint 2 at (0.2, 0, 0)), ξ_3 = (0,0,1,0,−0.35,0) (joint 3 at (0.35, 0, 0)).
  4. T_0^3(π/2, 0, 0) · [0;0;0;1] = rotate the whole arm by π/2 about z at the origin: (0, 0.45, 0). Or from the PoE: exp(ξ_1 π/2) · M · [0;0;0;1] = Rz(π/2) · (0.45, 0, 0) = (0, 0.45, 0). ✓

The lesson. A planar arm with parallel axes has α_i = 0 for all i, and the FK is the sum of the link vectors rotated by the cumulative angles — a 2D problem, which the DH and PoE both handle exactly. The α = 0 is the simplification that a planar arm allows, and its absence is the complication of the 6-DOF arm.

WE-L.2.c — Faded: the DH-PoE equivalence for the 6-DOF arm at a non-zero configuration

Setup. The running case’s DH and PoE forms, both at q = [0.3, 0.5, −0.2, 0.1, 0.4, −0.3].

Question. Compute the end-effector position from both forms, and verify they agree.

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

Answer. The DH form: T_DH = ∏ DH_i(q_i), with the DH matrices computed from the table. The PoE form: T_PoE = ∏ exp(ξ̂_i q_i) · M, with the twists from the running case. Both give the same T_0^6, and the same p_ee. The actual numerical value depends on the arithmetic, but the agreement is the test. For a specific check, the learner can compute T_DH(q) and T_PoE(q) and compare their Frobenius distance; the expected difference is < 1e-10 (both are exact SE(3) computations in float64).

The lesson. The DH and PoE forms are two implementations of the same FK, and their agreement at a non-zero q is the cross-check. A bug in one is not a bug in the other (they use different code paths and different data), so their agreement is strong evidence of correctness. The lab’s Step 2 performs this comparison as the primary test.

WE-L.2.d — Contrastive near-miss: the PoE M as the “end-effector offset” only

Correct solution. M is the end-effector’s pose at q = 0, which includes all the DH’s fixed transforms (the a_i, α_i, and the cumulative effect of the DH chain at q = 0).

Near-miss. The learner uses M = Trans_x(0.2) (only the last link’s offset) and ξ_i at the base frame for each joint (not at the home configuration).

Why the near-miss produces a plausible but incorrect number. The M = Trans_x(0.2) is the last link’s offset, but the full M is Trans_x(1.2) — the cumulative offset of all six links. The near-miss’s M underestimates the end-effector’s reach by 1.0, and its ξ_i (at the base) ignore the offsets of the previous joints. The FK is wrong by the cumulative offsets, and the error is proportional to the arm’s length. The q = 0 test fails (the near-miss gives (0.2, 0, 0) instead of (1.2, 0, 0)), but a learner who tests only non-zero q (where the error is masked by the joint rotations) misses the bug.

The discrepancy, stated. M = Trans_x(1.2) vs. M = Trans_x(0.2) differ by Trans_x(1.0) — the sum of the five upstream links’ offsets. The M is the end-effector’s pose at q = 0, and it is the product of the DH chain’s q = 0 transforms, not the last link’s offset. The ξ_i must be spatial (at the home configuration), not at the base — the Ad chain from 0.2.3 computes them, and using the base-frame axes is the M5 misconception of 0.2.3 (body vs. spatial twist).

Self-explanation 5. Before reading on: in the near-miss, if the arm is at q = 0, does the M = Trans_x(0.2) give the correct end-effector position (1.2, 0, 0)? What if q is all zero except q_1 = π/2?

Answer. At q = 0, the M = Trans_x(0.2) gives (0.2, 0, 0) — wrong (should be (1.2, 0, 0)), so the q = 0 test catches this bug. At q = [π/2, 0, 0, 0, 0, 0], the M = Trans_x(0.2) gives Rz(π/2) · (0.2, 0, 0) = (0, 0.2, 0), while the correct is (0, 1.2, 0) — also wrong, by (0, 1.0, 0). The bug is catastrophic at every configuration, and it is caught by the q = 0 test. The near-miss’s “only the last link” is a conceptual error (the M is the whole chain’s q = 0 pose), not a subtle numerical one.

§L.2.3 — Comparative table: the two formulations on the same robot

QuestionDH formPoE form
What is storedn rows of (θ_i, d_i, a_i, α_i), the θ slot being the variablen spatial twists ξ_i ∈ ℝ⁶ plus the home pose M ∈ SE(3)
What a human can read off itlink lengths, offsets, and twists — a table a machinist can machine tothe joint axes’ directions and moment arms at q = 0 — geometry, not dimensions
What is invariant to the base framenothing; the table is tied to the frame assignmentnothing either; the twists are conjugated by a change of base frame
What survives a redefinition of the basethe pose, after re-assignmentthe pose, after conjugating every ξ_i
q = 0 value∏ DH_i(0), which the table’s fixed entries determine implicitlyM, stated explicitly as one matrix
What the q = 0 test checksthe sum Σ a_i and the total twist ∏ Rx(α_i)the whole of M, translation and rotation at once
Typical failure symptoma table entry that is individually plausible and jointly wrong — a sign, a permutationa M built from the last link instead of the whole chain, or twists taken at the base
How a bug is localizedto the row whose parameter is wrong, by inspection of that joint’s frameto the twist whose adjoint chain is wrong, by comparing against the conversion
Conversion to the otheralgorithmic to PoE: M = ∏ DH_i(0), ξ_i = Ad_{DH_1(0)…DH_{i-1}(0)} ξ_i^{body}not algorithmic to DH: it needs the frame-assignment choices, which are not unique
Where the parameters come fromthe manufacturer’s data sheetestimation from data, or the conversion

Design principle (P13.6). A conversion is a specification, not a test. The DH → PoE conversion is a chain of adjoints, and once it is written it will agree with the DH chain by construction, because both were produced by the same algebra. Agreement between a form and its own conversion therefore proves nothing about the robot. The cross-check earns its strength only when the second implementation was written from the definition rather than from the first — different code, different data, and a discrepancy that is a real signal. Where a conversion is all you have, test the pose against a physical measurement or a third source, and treat the agreement of the two forms as a consistency check on the algebra, not on the model.

§L.2.4 — Concept check

Concept check (displaces M1). A learner says: “The DH table is just bookkeeping for a convention I have to pick anyway. I’ll throw it away and keep the twists and M — PoE is frame-free, so it’s the real math.” What has the learner lost, and what bug class follows?

Answer. The learner has lost the distinction between a convention and a fact: the DH parameters are physical — a link length and a link twist are properties of the machined part — while the PoE twists are frame-dependent, so they are properties of the pair (robot, base frame). Throwing away the table throws away the only representation that can be checked against the manufacturer’s data sheet, and it throws away the only one in which “the link is 0.2 long” is a sentence about the robot. The bug class is a model that is consistent and wrong: the twists can be estimated, learned, or converted, and every one of those paths is self-consistent, so nothing in the PoE form itself flags a base frame that was redefined between calibration and deployment. The fix is not to prefer DH — it is to keep both and know which question each answers, because the conversion DH → PoE is algorithmic and the conversion back is not.

§L.2.5 — Consolidation checkpoint 2

(a) You should now be able to: (i) state the equivalence and write M = ∏ DH_i(0) and ξ_i = Ad_{…} ξ_i^{body} from a table; (ii) say which of the two conversions is algorithmic and why the other is not; (iii) decide, for a given robot, which form is the source of truth and which is the cross-check.

(b) Transfer probe. A 3-DOF planar arm has a = (0.2, 0.15, 0.1), all revolute about z, all α = 0. A colleague derives the PoE form and reports M = Trans_x(0.1), because “the last link is 0.1”. Without computing any twist, say whether M is right, what the correct value is, and which test catches the error first. Answer sketch: M is the pose at q = 0, which is the product of all three DH_i(0), so it is Trans_x(0.45) — the sum of the link lengths. Trans_x(0.1) is the last link alone, and it is wrong by 0.35. The q = 0 test catches it immediately: the PoE form returns (0.1, 0, 0) where the DH chain returns (0.45, 0, 0). The learner who reports Trans_x(0.1) has treated M as an end-effector offset bolted onto the chain rather than as the chain’s own value at zero.

(c) Remediation rule. If you cannot do (a)(i) or (b), return to §L.2.2 and redo WE-L.2.a, then re-attempt (b) without notes.


Part C — The Geometric Jacobian from the FK

§L.3 — Objects: the Jacobian, its six rows, and the two interpretations

The geometric Jacobian J(q) ∈ ℝ^{6×n} maps the joint velocities q̇ to the end-effector’s spatial velocity V_ee = [v_ee; ω_ee] ∈ ℝ⁶:

Vee=J(q) q˙V_{ee} = J(q) \, \dot{q}

The six rows are [ω_ee; v_ee] — three for the angular velocity of the end-effector, three for its linear velocity, both expressed in the base frame (for the spatial Jacobian). The angular block is rows 1–3 and the linear block is rows 4–6, here and in every matrix printed below.

The columns are the joints’ contributions: the i-th column is the twist of joint i’s motion, evaluated at the end-effector’s position:

Ji=[ωivi+ωi×(pee−qi)]J_i = \begin{bmatrix} \omega_i \\ v_i + \omega_i \times (p_{ee} - q_i) \end{bmatrix}

where $ω_i$ is joint i’s axis (a unit vector in the base frame), $q_i$ is a point on joint i’s axis (in the base frame), and $p_ee$ is the end-effector’s position. The $v_i$ is the joint’s linear velocity at its own origin — zero for a revolute joint (the joint rotates in place) and $d_i$ $ω_i$ for a prismatic joint (the joint translates along its axis by $d_i$). Write $v_i$ as the vector $d_i$ $ω_i$, never as the scalar $ω_i$ · $d_i$: the dot product of the axis with a scalar is not defined, and even read charitably as a scalar multiple it would return 0 at a revolute joint and $d_i$ (a length, not a velocity vector) at a prismatic one.

The row order must be fixed once and stated once. The other convention ([v; ω], linear first) is equally common in the literature, and a Jacobian copied from one source and inverted against another will be silently wrong in a way no dimension check catches: both are 6×n. Pick one, write it at the top of the file, and assert it in the test.

The two interpretations:

  1. Velocity propagation: $V_ee$ = $Σ_i$ $J_i$ q̇_i — the end-effector’s velocity is the sum of each joint’s contribution, and the contribution is the joint’s twist translated to the end-effector’s point.
  2. Differential of the FK: J = ∂(pose)/∂q in the Lie algebra sense — the differential of the FK map q ↦ T(q) is the Jacobian, with the angular part being the rotational differential and the linear part the positional differential.

The 3×n position Jacobian $J_v$ is the linear part, and it is the one that the naive “differentiate the FK” gives: $J_v$ = ∂p_ee/∂q. The 3×n angular Jacobian J_ω is the rotational part, and it is not derivable from $p_ee$ alone — it requires the orientations along the chain.

§L.3.1 — Result: the Jacobian’s columns are the joint twists transported to the end-effector, not the joint twists at their own origins

Claim. For a revolute joint i with axis $ω_i$ and point $q_i$ on the axis (both in the base frame), the Jacobian’s i-th column is $J_i$ = [$ω_i$; $ω_i$ × ($p_ee$ − $q_i$)], which is the joint’s spatial twist transported to the end-effector’s point by the moment arm $p_ee$ − $q_i$.

Proof sketch. The end-effector’s velocity from joint i’s rotation is the cross product $ω_i$ × ($p_ee$ − $q_i$) (the velocity of a point at $p_ee$ due to a rotation about the axis through $q_i$ with angular velocity $ω_i$), plus the linear velocity from the joint’s translation (zero for a revolute joint). The angular part is $ω_i$ — the joint’s rotation is the same for all points on the rigid body downstream of joint i. The 6-vector [$ω_i$; $ω_i$ × ($p_ee$ − $q_i$)] is the spatial twist of joint i’s motion at the end-effector’s point, and it is the Jacobian’s column.

Corollary. The Jacobian’s column is not the joint’s twist $ξ_i$ = [$ω_i$; $v_i$] (which is at the joint’s origin), but the transported twist [$ω_i$; $v_i$ + $ω_i$ × ($p_ee$ − $q_i$)]. The difference is the moment arm $ω_i$ × ($p_ee$ − $q_i$), which is zero only when $p_ee$ = $q_i$ (the end-effector is on the joint’s axis). For every other configuration, the difference is non-zero, and using $ξ_i$ as the column is the M5 misconception.

Corollary. The Jacobian’s rank determines the robot’s singularities — configurations where the rank drops below n, and the robot loses a degree of freedom. The singularities are where $det(J_v$) (for a non-redundant arm) is zero, and they are the subject of Lesson 1.1.2 (inverse kinematics). The symptom of a singularity is that some end-effector velocities are unreachable — the Jacobian’s column space is a subspace of ℝ⁶, and the orthogonal complement is the set of unreachable velocities.

§L.3.2 — Worked-example ladder

WE-L.3.a — Fully worked: the Jacobian for a 2-DOF planar arm, by hand

Input. A 2-DOF planar arm with $a_1$ = 0.5, $a_2$ = 0.3, both revolute about z. At q = [π/2, 0].

Trace.

  1. $q_1$ = (0, 0, 0) (joint 1 at origin), $q_2$ = (0.5, 0, 0) (joint 2 at the end of link 1, but at q = [π/2, 0], link 1 is rotated π/2 about z, so $q_2$ = (0, 0.5, 0)).
  2. $p_ee$ at q = [π/2, 0]: link 1 goes from origin to (0, 0.5, 0) (rotated π/2), link 2 goes from (0, 0.5, 0) to (0, 0.5 + 0.3, 0) = (0, 0.8, 0) (link 2 is along y after π/2 rotation). So $p_ee$ = (0, 0.8, 0).
  3. $ω_1$ = (0, 0, 1) (joint 1’s axis).
  4. $ω_2$ = (0, 0, 1) (joint 2’s axis, parallel to joint 1).
  5. $J_1$ = [$ω_1$; $ω_1$ × ($p_ee$ − $q_1$)] = [(0,0,1); (0,0,1) × (0, 0.8, 0)]. (0,0,1) × (0, 0.8, 0) = (0·0 − 1·0.8, 1·0 − 0·0, 0·0.8 − 0·0) = (-0.8, 0, 0). So $J_1$ = [0, 0, 1, -0.8, 0, 0]ᵀ.
  6. $J_2$ = [$ω_2$; $ω_2$ × ($p_ee$ − $q_2$)] = [(0,0,1); (0,0,1) × ((0, 0.8, 0) − (0, 0.5, 0))] = [(0,0,1); (0,0,1) × (0, 0.3, 0)]. (0,0,1) × (0, 0.3, 0) = (-0.3, 0, 0). So $J_2$ = [0, 0, 1, -0.3, 0, 0]ᵀ.
  7. J = [[0, 0], [0, 0], [1, 1], [-0.8, -0.3], [0, 0], [0, 0]] (6 rows, 2 columns).

Output and interpretation. The Jacobian’s angular rows (1–3) are identical for both joints ([1, 1] — both rotate about z with the same angular velocity), which is the parallel-axis property of a planar arm. The linear rows (4 and 5, corresponding to $v_x$ and $v_y$) are [-0.8, -0.3] and [0, 0] — the x-velocity is proportional to the distances from the joints, and the y-velocity is zero at this configuration (the arm is along y, and a small rotation moves it along x). The $v_z$ row is zero (the arm is planar, no z motion). The 3×2 position Jacobian $J_v$ = [[-0.8, -0.3], [0, 0], [0, 0]] is rows 4–6 — the linear part — and the 3×2 angular Jacobian J_ω = [[0, 0], [0, 0], [1, 1]] is rows 1–3. The full 6×2 Jacobian is the geometric one. Note where $J_v$ came from: it is a contiguous block, rows 4 through 6. Reading [[0, 0], [0, 0], [-0.8, -0.3]] off the printed matrix instead — taking the first two rows because they look empty and appending the non-zero one — gives the transpose of the truth, and the end-effector’s velocity comes out as v = (0, 0, -0.8 q̇₁ - 0.3 q̇₂), a motion in z that a planar arm cannot make. The block structure is the check: if $J_v$ is not three contiguous rows of J, the slicing is wrong.

Self-explanation 6. Before reading on: in step 5, $J_1$’s linear part is (-0.8, 0, 0), and the joint’s own twist is [0, 0, 1, 0, 0, 0] (the joint’s angular velocity, no linear velocity at the origin). Why is $J_1$’s linear part (-0.8, 0, 0) and not zero?

Answer. The joint’s twist at its own origin is [0, 0, 1, 0, 0, 0] — no linear velocity at the origin. But the Jacobian’s column is the twist at the end-effector’s point $p_ee$ = (0, 0.8, 0), which is 0.8 away from the joint’s origin. The moment arm $ω_1$ × ($p_ee$ − $q_1$) = (0,0,1) × (0, 0.8, 0) = (-0.8, 0, 0) is the additional linear velocity that the end-effector sees from the joint’s rotation. The joint’s own linear velocity is zero, and the end-effector’s linear velocity is -0.8 in x — the difference is the moment arm, which is exactly the M5 misconception’s correction.

WE-L.3.b — Completion problem

Input. The same 2-DOF arm at q = [π/2, π/2].

Trace (final 25% as prompts).

  1. $q_1$ = (0,0,0). $q_2$ = (0, 0.5, 0) (joint 2, same as before). ⟨your turn: verify⟩
  2. $p_ee$ at q = [π/2, π/2]: link 1 is rotated π/2, so it goes to (0, 0.5, 0); link 2 is rotated π/2 + π/2 = π, so it goes back toward -x: (0, 0.5, 0) + Rz(π) · (0.3, 0, 0) = (0, 0.5, 0) + (-0.3, 0, 0) = (-0.3, 0.5, 0). ⟨your turn⟩
  3. $J_1$ = [$ω_1$; $ω_1$ × ($p_ee$ − $q_1$)]. ⟨your turn: compute⟩
  4. $J_2$ = [$ω_2$; $ω_2$ × ($p_ee$ − $q_2$)]. ⟨your turn⟩

Answers.

  1. $q_2$ = (0, 0.5, 0) — the joint 2 location at q = [π/2, *] is the end of link 1, which is (0, 0.5, 0). ✓
  2. $p_ee$ = (-0.3, 0.5, 0).
  3. $ω_1$ × ($p_ee$ − $q_1$) = (0,0,1) × (-0.3, 0.5, 0) = (0·0 − 1·0.5, 1·(-0.3) − 0·0, 0·0.5 − 0·(-0.3)) = (-0.5, -0.3, 0). $J_1$ = [0, 0, 1, -0.5, -0.3, 0]ᵀ.
  4. $ω_2$ × ($p_ee$ − $q_2$) = (0,0,1) × ((-0.3, 0.5, 0) − (0, 0.5, 0)) = (0,0,1) × (-0.3, 0, 0) = (0·0 − 1·0, 1·(-0.3) − 0·0, 0·0 − 0·(-0.3)) = (0, -0.3, 0). $J_2$ = [0, 0, 1, 0, -0.3, 0]ᵀ.

The lesson. With the [v; ω] convention, the two columns are $J_1$ = [(−0.5, −0.3, 0); (0, 0, 1)] and $J_2$ = [(0, −0.3, 0); (0, 0, 1)]. Stacked, the linear part is

$J_v$ = [[-0.5,  0.0],
       [-0.3, -0.3],
       [ 0.0,  0.0]]

Three things to read off it. The $v_z$ row is identically zero — a planar arm cannot lift the end-effector out of the plane, whatever the joint velocities. Joint 2’s linear column is (0, −0.3, 0): it moves the tip only in y, because at q = [π/2, π/2] link 2 points along −x and the tip lies on the line through joint 2 — the second link is instantaneously perpendicular to its own radius vector, so its whole contribution is tangential. And both angular columns are (0, 0, 1): the two joints rotate about parallel axes, so the arm’s angular velocity is (q̇₁ + q̇₂) about z and cannot be varied independently of itself. The 2×2 block formed by the $v_x$ and $v_y$ rows has determinant (−0.5)(−0.3) − (0)(−0.3) = 0.15 ≠ 0, so this configuration is not singular — a useful contrast with q = [0, 0], where both linear columns are (0, 0.8, 0) and (0, 0.3, 0) (parallel, along y) and the rank drops to 1.

WE-L.3.c — Faded: the 6-DOF arm’s Jacobian at a specific configuration

Setup. The running case’s 6-DOF arm at q = [0, π/2, 0, 0, 0, 0].

Question. Compute $J_1$’s linear part, $J_2$’s linear part, and the end-effector’s position.

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

Answer. $p_ee$ = (a, 0, -5a) = (0.2, 0, -1.0) (from WE-L.1.c’s q = [0, π/2, 0, 0, 0, 0]). $q_1$ = (0, 0, 0) (joint 1 at base origin), $q_2$ = (a, 0, 0) = (0.2, 0, 0) (joint 2 at the base’s x-axis a offset). $ω_1$ = (0, 0, 1), $ω_2$ = (0, 1, 0).

  • $J_1$’s linear: $ω_1$ × ($p_ee$ − $q_1$) = (0,0,1) × (0.2, 0, -1.0) = (0·(-1.0) − 1·0, 1·0.2 − 0·(-1.0), 0·0 − 0·0.2) = (0, 0.2, 0).
  • $J_2$’s linear: $ω_2$ × ($p_ee$ − $q_2$) = (0,1,0) × (0.2 − 0.2, 0 − 0, -1.0 − 0) = (0,1,0) × (0, 0, -1.0) = (1·(-1.0) − 0·0, 0·0 − 0·(-1.0), 0·0 − 1·0) = (-1.0, 0, 0).

The lesson. Joint 1’s linear contribution at the end-effector is (0, 0.2, 0) — a velocity in y (joint 1 rotates about z, and the end-effector is at (0.2, 0, -1.0), so the moment arm is mostly in -z, giving a y velocity). Joint 2’s contribution is (-1.0, 0, 0) — a velocity in -x (joint 2 rotates about y, and the end-effector is -1.0 below joint 2, so the moment arm gives -x). The magnitudes are different (0.2 vs. 1.0), and the directions are perpendicular — the two joints contribute to different velocity components, which is the arm’s dexterity.

WE-L.3.d — Contrastive near-miss: the Jacobian’s column as the joint’s twist

Correct solution. $J_i$ = [$ω_i$; $ω_i$ × ($p_ee$ − $q_i$)] — the joint’s twist transported to the end-effector.

Near-miss. $J_i$ = $ξ_i$ = [$ω_i$; $v_i$] — the joint’s twist at its own origin.

Why the near-miss produces a plausible but incorrect number. The $ξ_i$ is the joint’s twist at the joint’s origin, and its linear part is $v_i$ = 0 for a revolute joint. The near-miss’s $J_i$ has zero linear velocity for every joint, and the $J_v$ is identically zero — the Jacobian predicts that the end-effector’s position does not change when the joints move, which is catastrophically wrong. The near-miss’s J_ω is correct (the joint’s angular velocity is the same at every point on the rigid body), so the angular part is right and the linear part is wrong — a partial bug that is silent in the angular rows and fatal in the linear rows.

The discrepancy, stated. $ξ_i$ and $J_i$ differ by the moment arm $ω_i$ × ($p_ee$ − $q_i$), which is the linear velocity contribution from the joint’s rotation at the end-effector’s point. Omitting it makes the position Jacobian zero, and the velocity propagation $V_ee$ = J q̇ predicts no linear velocity — the end-effector’s velocity is purely angular, which is physically impossible (the end-effector is a point moving in space). The fix is the moment arm, which is exactly the M5 misconception’s correction.

Self-explanation 7. Before reading on: in the near-miss, if the end-effector is on joint 1’s axis (i.e., $p_ee$ = $q_1$), is the moment arm zero?

Answer. Yes — if $p_ee$ = $q_1$, the end-effector is on the joint’s axis, and the rotation about the axis does not move it (a point on the axis is fixed by a rotation about that axis). The moment arm $ω_1$ × ($p_ee$ − $q_1$) = $ω_1$ × 0 = 0, so the joint’s contribution is zero at the end-effector — the joint does not move the end-effector, which is physically correct. The near-miss’s $ξ_i$ is also zero in the linear part (for a revolute joint, $v_i$ = 0), so the near-miss coincides with the correct solution on the axis — but the coincidence is degenerate, and the near-miss fails everywhere else.

§L.3.3 — Comparative table: the candidate “Jacobians”

CandidateShape and rowsWhat it isWhat it gets rightWhat it misses
∂p_ee/∂q3×n, position onlythe position Jacobian $J_v$the linear velocity, exactlythe angular velocity — it needs the orientations along the chain, which a position derivative never touches
$J_i$ = $ξ_i$ = [$ω_i$; $v_i$]6×n, twist at the joint’s own originthe joint’s twist, untransportedthe angular block J_ω,i = $ω_i$, which is the same at every point on the rigid bodythe moment arm; the linear block is $v_i$, which is zero for a revolute joint, so $J_v$ comes out identically zero
$J_i$ = [$ω_i$; $ω_i$ × ($p_ee$ − $q_i$)]6×n, angular rows 1–3, linear rows 4–6the joint’s twist transported to the end-effectorboth blocks; this is the geometric Jacobiannothing — it is the object
$J_i$ = [$v_i$ + $ω_i$ × ($p_ee$ − $q_i$); $ω_i$]6×n, linear rows 1–3, angular rows 4–6the same object in the [v; ω] conventionthe values, permutednothing, provided every consumer in the file uses the same order; the danger is mixing it with the row above
[$ω_i$; $d_i$ $ω_i$] at a prismatic joint6×n, with the prismatic column’s linear part as the vector $d_i$ $ω_i$the prismatic joint’s own twist, before transportthe prismatic column’s translation, when $v_i$ is written as a vectorthe moment arm, as above; and writing $ω_i$ · $d_i$ instead returns a scalar or zero, which is not a velocity at all

Design principle (P13.7). Write the row order once, at the top of the file, and assert it. Both [ω; v] and [v; ω] are 6×n, so no shape check, no norm check, and no comparison against a reference written in the other order will catch a mix-up — the numbers are all finite and all plausible, and the angular velocity is read as a linear one. The row order is a convention in the same sense as the frame assignment: it is not a fact about the robot, it is a fact about the file, and the only defense is to state it where every reader and every test can see it.

§L.3.4 — Concept check

Concept check (displaces M4). A learner says: “The Jacobian is ∂p/∂q — I can get it by autograd on the FK’s translation, so I never need to write the cross-product formula.” What has the learner failed to model, and what bug class does it produce?

Answer. The learner has failed to model that the geometric Jacobian is six rows and that the angular block is not derivable from the position. ∂p/∂q is 3×n; it is the position Jacobian $J_v$, which is half of the object, and it is exactly right for the half it covers — which is what makes the error survive review. The bug class is a controller that regulates position and ignores orientation: a resolved-rate loop built on $J_v$ alone has no row in which an orientation error can enter, so a target that differs only in rotation produces a zero error and a zero step, and the arm stops at the right place pointing the wrong way. The fix is J_ω,i = $ω_i$ as its own block, taken from the joint axes rather than from any derivative of $p_ee$, and a test that perturbs q and checks the rotation’s derivative ∂R/∂q · Rᵀ, not only the translation’s.

§L.3.5 — Consolidation checkpoint 3

(a) You should now be able to: (i) write $J_i$ for a revolute and for a prismatic joint, with the angular rows first; (ii) say which block of J the autograd of $p_ee$ gives you and which block it cannot; (iii) locate $J_v$ as three contiguous rows and say what goes wrong when it is sliced by appearance instead.

(b) Transfer probe. A 2-DOF planar arm sits at q = [π/2, 0], so $p_ee$ = (0, 0.8, 0) with $a_1$ = 0.5, $a_2$ = 0.3. A colleague reports that joint 1’s column is [0, 0, 1, 0, 0, 0] because “the joint rotates in place, so its linear velocity is zero.” Is the column right, and what does the claim confuse? Answer sketch: the column is the joint’s own twist, not the Jacobian’s column. The Jacobian’s column is the twist at the end-effector, and the end-effector is 0.8 away from joint 1’s axis, so the linear part is $ω_1$ × ($p_ee$ − $q_1$) = (0,0,1) × (0, 0.8, 0) = (−0.8, 0, 0). “The joint rotates in place” is true of the joint’s origin and irrelevant to the end-effector; the quantity that carries the difference is the moment arm, and with the near-miss the linear block is zero for every joint and the arm is predicted never to move its tip.

(c) Remediation rule. If you cannot do (a)(iii) or (b), return to §L.3.2 and redo WE-L.3.a, then re-attempt (b) without notes.


Part D — The Running Case in DH Form and the PyTorch Implementation

§L.4 — Objects: the DH chain module and its verification

The DH chain is implemented as an nn.Module with:

  • Buffers: the fixed $a_i$, $α_i$, $d_i$ (from 0.1.3’s buffer distinction — they are not learnable, but they must be moved by .to(device) and saved in state_dict()).
  • Input: the joint angles q (shape (batch, n) or (n,)).
  • Output: the end-effector pose $T_0^n$ (shape (batch, 4, 4) or (4, 4)).

The forward pass computes $T_0^n$ = ∏ $DH_i(q_i$) by iterating the DH transforms, and the DH transform for joint i is the 4×4 matrix from §L.1.

The verification:

  1. q = 0 test: $T_0^n(0$) should equal Trans_x(Σ $a_i$) for the running case.
  2. Non-zero q test: $T_0^n(q$) should match the PoE ∏ exp(ξ̂_i $q_i$) · M for the same q (the 0.2.3 implementation).
  3. Jacobian test: the analytic Jacobian J(q) should match the numerical derivative (T(q+δ) − T(q−δ))/(2δ) in the Lie algebra, for a small δ.

The DH form’s advantage in the implementation: the parameters are buffers (fixed, non-learnable), and the joint angles are the input — the module is a pure function of q, which is what a controller needs. The PoE form’s advantage: the twists and M can be learned (they are the parameters of a learned kinematic model), and the module’s parameters are differentiable — the learned-FK setting.

§L.4.1 — Result: the DH implementation’s q = 0 and non-zero tests are necessary and sufficient for correctness (given the correct table), and the PoE match is a stronger test than either alone

Claim. A DH implementation is correct if and only if it matches the reference at q = 0 (the reach) and at a non-trivial q (the rotations’ effect). The q = 0 test catches the $a_i$ errors and the M errors; the non-zero test catches the $α_i$ errors, the sign errors, and the permutation of the $a_i$. The PoE match is the cross-check, and it is stronger than either alone because it exercises two independent code paths.

Proof sketch. The q = 0 test: at q = 0, all DH_i(0) = $Trans_x(a_i$) · $Rot_x(α_i$), and the product’s translation is Σ $a_i$ (the rotations about x don’t change x). A missing or doubled $a_i$ changes the sum and is caught. The order of the $a_i$ is not caught: the sum is commutative, so any permutation of the link lengths gives the same q = 0 reach. That is the test’s blind spot, and it is why the non-zero test is not optional. The non-zero test: at q ≠ 0, the $θ_i$ rotations are non-identity, each $a_i$ is rotated by the product of the preceding θ’s, and a permutation changes which rotation each length picks up — so the end-effector’s position moves. An error in any $α_i$ or its sign likewise changes the direction of the next joint’s axis, which changes the end-effector’s position at q ≠ 0. The PoE match: the two implementations use different code (the DH chain vs. the exp map), and their agreement is a strong correctness signal.

Corollary. The DH table’s entries are not the test — the output is. A learner who “verifies the table” by reading it is doing a static check that misses the dynamic errors (the α’s sign, the a’s permutation). The runtime test (the FK’s output vs. the reference) is the only correctness check that matters.

Corollary. The Jacobian’s numerical check (T(q+δ) − T(q−δ))/(2δ) is the differential version of the FK’s test, and it is sensitive to the same errors. The δ’s choice is the trade-off: too large → the finite difference’s truncation error; too small → the floating-point’s rounding error. The optimal δ is O(ε^{1/3}) ≈ 10⁻⁵ for float64, and the test should use multiple δ values to confirm.

§L.4.2 — Worked-example ladder

WE-L.4.a — Fully worked: the DH chain’s forward pass for the running case

Input. The running case’s DH table (from WE-L.1.c), q = [0.3, 0.5, −0.2, 0.1, 0.4, −0.3].

Trace (PyTorch pseudocode).

  1. T = eye(4) (start with identity).
  2. for i in range(6): compute $T_i$ = DH(θ=q[i], d=d[i], a=a[i], α=α[i]).
  3. T = T @ $T_i$ — accumulate.
  4. return T — the end-effector pose.

Trace (the expected numerical result). The T is a 4×4 matrix with the rotation $R_0^6$ and the translation $p_ee$. The rotation is the composition of six joint rotations, and the translation is the sum of the six link vectors (the 0.2.3 $p_ee$ = Σ $R_1$...$R_i$ a). The exact numerical values depend on the joint angles, and the verification is against the PoE’s output, not the specific numbers.

Output and interpretation. The DH chain’s output is the end-effector’s pose (R, p), which is the same as the PoE’s output for the same q (up to floating-point). The speed of the DH chain is 6 matrix multiplies of 4×4 matrices — O(6 · 64) = O(384) operations, which is fast for a control loop. The PoE’s speed is 6 exp evaluations (each O(27) for the Rodrigues formula) plus 6 matrix multiplies — slower than the DH chain, but more general (the twists can be learned).

WE-L.4.b — Completion problem

Input. Same DH table, q = 0.

Trace (final 25% as prompts).

  1. $T_i(0$) = $Trans_x(a_i$) · $Rot_x(α_i$) = [[I, [$a_i$, 0, 0]], [0, 1]] · [[$R_x(α_i$), 0], [0, 1]]. ⟨your turn: what is the translation of $T_i(0$)?⟩
  2. $T_0^6(0$) = ∏ $T_i(0$). ⟨your turn: compute the translation⟩
  3. Compare to the PoE’s M = Trans_x(1.2). ⟨your turn: equal?⟩

Answers.

  1. The $Trans_x(a_i$) is the translation, and $Rot_x(α_i$) is a rotation about x, which does not change the x component. So $T_i(0$)’s translation is [$a_i$, 0, 0].
  2. The product’s translation is Σ $a_i$ = 6·0.2 = 1.2 (the rotations about x don’t affect the x-axis).
  3. Trans_x(1.2) = M. ✓

The lesson. The q = 0 test catches a missing or doubled $a_i$ — the sum is different and the test fails. It does not catch a shuffled $a_i$: addition is commutative, so every permutation of the link lengths gives the same 1.2, and the test passes on a robot that is not the one in the table. The q = 0 test is necessary and not sufficient — it does not catch the $α_i$ errors either, which are identity at q = 0 — and the non-zero test is the complement that catches both the permutation and the α’s sign. A gate that cannot fail on a permutation must not be advertised as testing the order.

WE-L.4.c — Faded: the numerical Jacobian check

Setup. The running case’s DH chain, q = [0.3, 0.5, −0.2, 0.1, 0.4, −0.3], δ = 1e-6.

Question. Compute the numerical derivative of $p_ee$ with respect to $q_1$, and compare it to the analytic $J_1$’s linear part.

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

Answer. The numerical derivative is ($p_ee(q$ + δ $e_1$) − $p_ee(q$ − δ e_1))/(2δ), where $e_1$ is the unit vector in $q_1$’s direction. This gives the 3×1 column ∂p_ee/∂$q_1$, which should match the analytic $J_1[0$:3] = $ω_1$ × ($p_ee$ − $q_1$) (the linear part of the geometric Jacobian’s column 1). The match is the test; the value depends on the configuration.

The lesson. The numerical derivative ∂p_ee/∂$q_1$ is the position Jacobian’s column, and it is half of the geometric Jacobian’s column. The angular part $J_1[3$:6] = $ω_1$ is not derivable from the position derivative — it requires the rotation’s derivative. The geometric Jacobian’s six rows are the complete differential, and the position Jacobian’s three rows are the partial differential. The test for the position Jacobian is ∂p/∂q, and the test for the angular Jacobian is ∂R/∂q · Rᵀ (the angular velocity’s matrix).

WE-L.4.d — Contrastive near-miss: the DH chain’s α = 0 for all joints

Correct solution. The running case’s DH table has α = [−π/2, +π/2, −π/2, +π/2, −π/2, +π/2].

Near-miss. The learner uses α = [0, 0, 0, 0, 0, 0] — all joints “parallel”.

Why the near-miss produces a plausible but incorrect number. At q = 0, the near-miss gives Trans_x(1.2) — the same as the correct. At q ≠ 0, the near-miss’s arm is a planar arm (all rotations about the base z), which is a 2-DOF effective arm, not a 6-DOF arm — the end-effector’s position at q = [0, π/2, 0, 0, 0, 0] is (0.2, 1.0, 0) (joint 1 does not move, so its link stays along x; the remaining five links swing π/2 into y), not the correct (0.2, 0, −1.0) (the 6-DOF bend). The near-miss’s arm is topologically different from the correct one, and its FK is wrong at every non-zero configuration.

The discrepancy, stated. α = 0 vs. α = ±π/2 differ by the link twist, which is the relative orientation of consecutive joint axes. With α = 0, all joints rotate about parallel axes (a planar arm), and the arm is 2-DOF effective (3 in-plane joints, but only 2 independent axes). With α = ±π/2, the joints alternate between z and y, and the arm is 6-DOF. The α = 0 is the degenerate case (a planar arm), and the α = ±π/2 is the general case (a spatial arm). The near-miss is a different robot, and its FK is not the running case’s.

Self-explanation 8. Before reading on: in the near-miss, at q = [π/2, π/2, 0, 0, 0, 0], what is the end-effector’s position, and why is it different from the correct?

Answer. For the near-miss’s planar arm with $a_i$ = 0.2 throughout and joints 1 and 2 at π/2 (all rotations about z), the end-effector is Rz(π/2)·(0.2, 0, 0) + Rz(π)·(0.2, 0, 0) + Rz(π)·(0.8, 0, 0) = (0, 0.2, 0) + (−0.2, 0, 0) + (−0.8, 0, 0) = (−1.0, 0.2, 0). The correct chain at the same q puts the end-effector at (0, 0.2, −1.0) — joint 2 rotates about y, so the arm bends out of the plane. The two differ by (−1.0, 0, 1.0), magnitude 1.414: the alternating α’s change the arm’s plane, not just its shape.

§L.4.3 — Comparative table: the three verification tests

TestWhat is computedWhat it catchesWhat it cannot catchCost
q = 0 reach$T_0^n(0$) against Trans_x(Σ $a_i$)a missing or doubled $a_i$, a wrong $d_i$, a wrong Ma permutation of the $a_i$ (addition commutes, so every permutation gives the same sum); every $α_i$ error, which is identity at q = 0one forward pass
Non-zero q$T_0^n(q$) against the reference pose at the same qa permutation of the $a_i$ (each length picks up a different product of preceding θs); an $α_i$ error or sign flip, which changes the next joint’s axisnothing in the table, but only the configurations sampled; a bug that is null at the chosen q survivesone forward pass per configuration
PoE match$T_DH(q$) against $T_PoE(q$)anything one implementation gets wrong and the other does nota shared error — a wrong DH table and the PoE converted from it agree perfectlytwo forward passes per configuration
Numerical Jacobian($p_ee(q+δe_i$) − $p_ee(q$−δe_i))/(2δ) against $J_v$[:, i]a wrong linear column; a wrong $p_ee$ that the pose tests happened to missthe angular block, which no position derivative reaches; and it inherits the finite-difference’s own errortwo forward passes per column

Design principle (P13.8). A test is a claim about what it can fail on, and the claim must be narrower than the test’s name. “The q = 0 test verifies the DH table” is false in two specific ways — it cannot see a permutation of the $a_i$, and it cannot see any $α_i$ — and both are exactly the errors the test is most often credited with finding. Before trusting a gate, name the input it varies and the output it reads, then ask which errors leave both unchanged. The q = 0 gate varies nothing and reads a sum, so it is a test of the sum; the order of the links needs a configuration where the rotations are non-identity, and the twist needs one where the axes differ.

§L.4.4 — Concept check

Concept check (displaces M3). A learner says: “My FK passes the q = 0 check and the reach is exactly 1.2, so the α signs are right — a sign on a rotation about x can’t change where the arm reaches anyway.” What has the learner failed to model, and what bug class follows?

Answer. The learner has failed to model that $α_i$ sets the direction of the next joint’s axis: $R_x(α$)·ẑ = [0, −sin α, cos α], so α = −π/2 sends z to +y and α = +π/2 sends it to −y. The reach is unaffected because Trans_x(a) is invariant under a rotation about x — the claim is true about the reach and false about the robot. The bug class is a mirror-image arm that passes every position test at the home configuration: flipping the signs leaves (1.2, 0, 0) intact at q = 0, and at q = [0, π/2, 0, …] it puts the end-effector at (0.2, 0, +1.0) where the truth is (0.2, 0, −1.0). Two meters of error from a sign the learner called cosmetic, invisible at the configuration they tested.

§L.4.5 — Consolidation checkpoint 4

(a) You should now be able to: (i) say what each of the three tests can and cannot fail on, and why the q = 0 test cannot see a permutation; (ii) register the fixed table as buffers and take q as the forward pass’s input; (iii) choose a finite-difference δ and say which of the two error terms dominates at each end.

(b) Transfer probe. A learner’s DH implementation passes the q = 0 test at 1.2, passes a non-zero q test against a hand-computed pose, and fails the PoE comparison by a constant rotation of Rx(−π/2). Where is the bug, and why did the first two tests miss it? Answer sketch: the constant rotation is the signature of the last twist — $α_6$ = 0 leaves ∏ $Rx(α_i$) = Rx(−π/2) instead of the identity, so the position at every configuration is right and the orientation is wrong by a fixed rotation. The q = 0 reach is unaffected, because Trans_x commutes with a rotation about x. The hand-computed non-zero pose was almost certainly compared on its translation, which is also unaffected at q = 0 and only weakly affected elsewhere. The PoE match is the only one of the three that compares the full pose against an independently derived reference, which is why it is the test that catches a twist that never moves the tip.

(c) Remediation rule. If you cannot do (a)(i) or (b), return to §L.4.2 and redo WE-L.4.b, then re-attempt (b) without notes.


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

§L.5 — Failure taxonomy

SymptomThree most likely causes, rankedDiscriminating testFix
FK at q = 0 is wrong(1) Wrong $a_i$; (2) wrong $d_i$; (3) chain order reversedPrint T(q=0) and compare to Trans_x(Σ $a_i$)Check the $a_i$ and their order
FK at q ≠ 0 is a mirror image(1) α sign flipped; (2) θ sign convention; (3) joint axis direction reversedCompare T(q) for q = [0, π/2, 0, ..., 0] to the expectedFlip the α sign (the one that matches the physical axis)
FK at q ≠ 0 is a planar arm(1) All α = 0; (2) the α’s are the wrong values; (3) the joints’ axes are all parallelCheck α values; if all zero, it is (1)Use the correct $α_i$ from the physical design
The Jacobian’s linear part is zero(1) Using $ξ_i$ instead of the moment arm; (2) $p_ee$ is on the joint axis; (3) the Jacobian is the body Jacobian, not the spatialPrint $J_1[0$:3] and compare to $ω_1$ × ($p_ee$ − $q_1$)Add the moment arm
The Jacobian’s angular part is wrong(1) J_ω uses $p_ee$ instead of $ω_i$; (2) the convention is [v; ω] vs. [ω; v] mixed; (3) the body vs. spatial confusionPrint $J_1[3$:6] and compare to $ω_1$Use J_ω,i = $ω_i$
The DH and PoE FKs disagree(1) Wrong M; (2) wrong $ξ_i$; (3) a bug in one implementationCompare $T_DH(q$) and $T_PoE(q$) at a specific qFix the one whose q = 0 test fails first
The arm is dexterous at one configuration and singular at another(1) The configuration is near a singularity; (2) J’s rank drops; (3) the α’s are wrongCheck rank(J) or $det(J_v$)The singularities are physical (the arm’s structure), not a bug

§L.6 — The lab

§L.6.1 — Task

Implement the running case’s DH chain, verify it against the 0.2.3 PoE, compute the geometric Jacobian, and break the pipeline three ways.

§L.6.2 — Conditions

Python 3.10+, PyTorch 2.2+, CPU. Seed 42, dtype = float64. Runtime under 2 minutes. The test configurations: q = 0, q = [0, π/2, 0, 0, 0, 0], q = [0.3, 0.5, −0.2, 0.1, 0.4, −0.3].

§L.6.3 — Controls held fixed

The seed, dtype, the DH table, and the test q’s are fixed.

§L.6.4 — Metrics table

MetricDefinitionHow measured
fk_q0_error‖$T_DH(0$) − Trans_x(1.2)‖_maxThe q = 0 test
fk_poe_error‖$T_DH(q$) − $T_PoE(q$)‖_maxThe PoE match
jacobian_numeric_error‖$J_analytic$ − $J_numeric$‖_maxThe numerical derivative
mirror_error‖$T_DH_correct(q$) − $T_DH_flipped_alpha(q$)‖The flip’s effect
planar_error‖$T_DH_correct(q$) − $T_DH_alpha0(q$)‖The all-zero α’s effect

§L.6.5 — Pre-registered hypotheses

  • H1. fk_q0_error < 1e-10 — the DH chain’s q = 0 matches the analytic Trans_x(1.2). The prediction is the bound.
  • H2. fk_poe_error < 1e-10 — the DH and PoE match at the test q. The prediction is the bound.
  • H3. mirror_error > 0.5 at q = [0, π/2, 0, 0, 0, 0] — the flipped α’s give a different end-effector position by more than 0.5. The prediction is the bound.
  • H4. planar_error > 0.5 at the same q — the all-zero α’s give a different position. The prediction is the bound.

§L.6.6 — The break-it sequence

  1. Flipped α (silent). Flip the α signs. Predicted symptom: no exception; q = 0 test passes, q ≠ 0 test fails with a mirror image. Actual: as predicted — silent.
  2. All α = 0 (silent). Set all α = 0. Predicted symptom: no exception; q = 0 test passes, q ≠ 0 test fails with a planar arm. Actual: as predicted — silent.
  3. Jacobian without moment arm (silent). Use $J_i$ = $ξ_i$ (no moment arm). Predicted symptom: no exception; the linear part is zero, the position Jacobian is zero. Actual: as predicted — silent.

All three are silent.

§L.6.7 — Acceptance criteria

CriterionThresholdArtefact it is tested on
H1’s fk_q0_error< 1e-10The lab’s q = 0 print
H2’s fk_poe_error< 1e-10The lab’s PoE comparison
H3’s mirror_error> 0.5The lab’s flip test
H4’s planar_error> 0.5The lab’s α = 0 test
The Jacobian’s numerical match< 1e-6The lab’s numerical derivative
Three break-its predicted3/3The prediction file

§L.6.8 — What good looks like

A strong submission has five properties:

  1. The q = 0 test and the PoE match are both passed, with the errors printed, not just asserted.
  2. The Jacobian’s numerical check is done, with the δ’s effect noted (the truncation vs. rounding trade-off).
  3. The break-its are predicted with the symptom (mirror at q ≠ 0, planar at q ≠ 0, zero linear Jacobian) stated before running.
  4. The geometric Jacobian’s six rows are explained (the [v; ω] or [ω; v] convention, the moment arm, the angular part).
  5. The DH vs. PoE trade-off is stated: the DH is the manufacturer’s data, the PoE is the learned representation, and the two are cross-checks for each other.

§L.6.9 — Runnable lab code

# lab_1_1_1.py — The running case's FK in DH form, PoE match, and the geometric Jacobian
# Requires: Python 3.10+, PyTorch 2.2+

import torch, math
torch.manual_seed(42); dtype, device = torch.float64, torch.device("cpu")

# --- The DH table for the running case ---
a = 0.2
DH = [
    (0.0, a, -math.pi/2),  # (d, a, alpha) for joint 1
    (0.0, a, +math.pi/2),  # joint 2
    (0.0, a, -math.pi/2),  # joint 3
    (0.0, a, +math.pi/2),  # joint 4
    (0.0, a, -math.pi/2),  # joint 5
    (0.0, a, +math.pi/2),  # joint 6 — NOT 0: the last frame must carry the
                           # same twist as the rest, or the chain's orientation
                           # is off by Rx(-pi/2) and q=0 gives a rotated pose.
]

def dh_transform(theta, d, $a_i$, alpha):
    ct, st = math.cos(theta), math.sin(theta)
    ca, sa = math.cos(alpha), math.sin(alpha)
    return torch.tensor([
        [ct, -st*ca,  st*sa, $a_i*ct$],
        [st,  ct*ca, -ct*sa, $a_i*st$],
        [0,       sa,     ca,      d],
        [0,        0,      0,      1],
    ], dtype=dtype, device=device)

def fk_dh(q):
    T = torch.eye(4, dtype=dtype, device=device)
    for qi, (d, $a_i$, alpha) in zip(q, DH):
        T = T @ dh_transform(qi, d, $a_i$, alpha)
    return T

# --- The PoE twists and M (from 0.2.3) ---
def make_T(R, t):
    T = torch.eye(4, dtype=dtype, device=device); T[:3, :3] = R; T[:3, 3] = t; return T
def skew(w):
    return torch.tensor([[0, -w[2], w[1]], [w[2], 0, -w[0]], [-w[1], w[0], 0]], dtype=dtype, device=device)
def exp_so3(w):
    th = w.norm()
    if th < 1e-12: return torch.eye(3, dtype=dtype, device=device) + skew(w)
    u = w / th; U = skew(u)
    return torch.eye(3, dtype=dtype, device=device) + math.sin(th)*U + (1 - math.cos(th))*(U @ U)
def exp_se3_unit(xi, q):
    w, v = xi[:3], xi[3:]
    W = skew(w)
    R = torch.eye(3, dtype=dtype, device=device) + math.sin(q)*W + (1 - math.cos(q))*(W @ W)
    J = q*torch.eye(3, dtype=dtype, device=device) + (1 - math.cos(q))*W + (q - math.sin(q))*(W @ W)
    return make_T(R, J @ v)

twists = [
    torch.tensor([0., 0., 1., 0., 0., 0.], dtype=dtype, device=device),
    torch.tensor([0., 1., 0., 0., 0., a], dtype=dtype, device=device),
    torch.tensor([0., 0., 1., 0., -2*a, 0.], dtype=dtype, device=device),
    torch.tensor([0., 1., 0., 0., 0., 3*a], dtype=dtype, device=device),
    torch.tensor([0., 0., 1., 0., -4*a, 0.], dtype=dtype, device=device),
    torch.tensor([0., 1., 0., 0., 0., 5*a], dtype=dtype, device=device),
]
M = make_T(torch.eye(3, dtype=dtype, device=device), torch.tensor([6*a, 0., 0.], dtype=dtype, device=device))

def fk_poe(q):
    T = torch.eye(4, dtype=dtype, device=device)
    for xi, qi in zip(twists, q):
        T = T @ exp_se3_unit(xi, qi)
    return T @ M

# --- Step 1: q = 0 test ---
q0 = torch.zeros(6, dtype=dtype, device=device)
$T_dh_0$ = fk_dh(q0); $T_poe_0$ = fk_poe(q0)
$T_target$ = make_T(torch.eye(3, dtype=dtype, device=device), torch.tensor([1.2, 0., 0.], dtype=dtype, device=device))
print(f"DH(q=0) error: {(T_dh_0 - $T_target$).abs().max().item():.2e}")
print(f"PoE(q=0) error: {(T_poe_0 - $T_target$).abs().max().item():.2e}")

# --- Step 2: non-zero q, DH vs PoE ---
q = torch.tensor([0.3, 0.5, -0.2, 0.1, 0.4, -0.3], dtype=dtype, device=device)
$T_dh$ = fk_dh(q); $T_poe$ = fk_poe(q)
print(f"DH vs PoE error at q: {(T_dh - $T_poe$).abs().max().item():.2e}")

# --- Step 3: flipped alpha (mirror) ---
DH_flipped = [(d, $a_i$, -alpha) for (d, $a_i$, alpha) in DH]
def fk_dh_flipped(q):
    T = torch.eye(4, dtype=dtype, device=device)
    for qi, (d, $a_i$, alpha) in zip(q, DH_flipped):
        T = T @ dh_transform(qi, d, $a_i$, alpha)
    return T
$q_test$ = torch.tensor([0., math.pi/2, 0., 0., 0., 0.], dtype=dtype, device=device)
print$f"\nmirror at q=[0,pi/2,0,...]: {(fk_dh(q_test) - fk_dh_flipped(q_test)).abs().max().item():.4f}"$
print(f"  correct end-effector: {fk_dh(q_test)[:3, 3].tolist()}")
print(f"  flipped end-effector: {fk_dh_flipped(q_test)[:3, 3].tolist()}")

# --- Step 4: all alpha = 0 (planar) ---
DH_planar = [(d, $a_i$, 0.0) for (d, $a_i$, alpha) in DH]
def fk_dh_planar(q):
    T = torch.eye(4, dtype=dtype, device=device)
    for qi, (d, $a_i$, alpha) in zip(q, DH_planar):
        T = T @ dh_transform(qi, d, $a_i$, alpha)
    return T
print$f"\nplanar at q=[0,pi/2,0,...]: {(fk_dh(q_test) - fk_dh_planar(q_test)).abs().max().item():.4f}"$
print(f"  planar end-effector: {fk_dh_planar(q_test)[:3, 3].tolist()}")

# --- Step 5: geometric Jacobian (spatial, position + angular) ---
def frames_dh(q):
    """All frame poses $T_0^i$ for i = 0..6."""
    T = torch.eye(4, dtype=dtype, device=device); out = [T.clone()]
    for qi, (d, $a_i$, alpha) in zip(q, DH):
        T = T @ dh_transform(qi, d, $a_i$, alpha); out.append(T.clone())
    return out

def jacobian(q):
    """6x6 geometric Jacobian, [v; omega] convention.

    Joint i+1's axis is z of frame i, taken AT THE CURRENT q. Using the home
    axes instead is the trap: it is exact only at q = 0.
    """
    F = frames_dh(q)
    $p_ee$ = F[-1][:3, 3]
    $z_local$ = torch.tensor([0., 0., 1.], dtype=dtype, device=device)
    J = torch.zeros(6, 6, dtype=dtype, device=device)
    for i in range(6):
        omega = F[i][:3, :3] @ $z_local$
        $p_i$ = F[i][:3, 3]
        J[:3, i] = torch.cross(omega, $p_ee$ - $p_i$, dim=0)
        J[3:, i] = omega
    return J

def numerical_jacobian(q, delta=1e-6):
    """Numerical [v; omega] Jacobian via finite differences."""
    $J_num$ = torch.zeros(6, 6, dtype=dtype, device=device)
    for i in range(6):
        $q_p$ = q.clone(); $q_p[i$] += delta
        $q_m$ = q.clone(); $q_m[i$] -= delta
        $T_p$ = $fk_dh(q_p$); $T_m$ = $fk_dh(q_m$)
        $J_num$[:3, i] = ($T_p$[:3, 3] - $T_m$[:3, 3]) / (2 * delta)
        # Angular: $R_p$ @ $R_m$.T gives the relative rotation, log gives omega
        $R_p$, $R_m$ = $T_p$[:3, :3], $T_m$[:3, :3]
        dR = ($R_p$ - $R_m$) @ $R_m$.T / (2 * delta)
        # omega = vee(dR @ R.T) ... this is approximate
        $J_num[3$:, i] = torch.tensor([dR[2, 1] - dR[1, 2], dR[0, 2] - dR[2, 0], dR[1, 0] - dR[0, 1]], dtype=dtype) / 2
    return $J_num$

$q_test2$ = torch.tensor([0.3, 0.5, -0.2, 0.1, 0.4, -0.3], dtype=dtype, device=device)
$J_ana$ = $jacobian(q_test2$)
$J_num$ = $numerical_jacobian(q_test2$)
print$f"\nJacobian linear part diff: {(J_ana[:3, :] - J_num[:3, :]).abs().max().item():.2e}"$
print(f"Jacobian angular part diff: {(J_ana[3:, :] - $J_num[3$:, :]).abs().max().item():.2e}")

# --- Step 6: summary ---
print("\n--- lab 1.1.1 summary ---")
print(f"H1: DH(q=0) error = {(T_dh_0 - $T_target$).abs().max().item():.2e} (predicted < 1e-10)")
print(f"H2: DH vs PoE error = {(T_dh - $T_poe$).abs().max().item():.2e} (predicted < 1e-10)")
print(f"H3: mirror error = {(fk_dh(q_test) - $fk_dh_flipped(q_test$)).abs().max().item():.4f} (predicted > 0.5)")
print(f"H4: planar error = {(fk_dh(q_test) - $fk_dh_planar(q_test$)).abs().max().item():.4f} (predicted > 0.5)")

§L.7 — The cheat sheet

Term or resultStatement or definitionRole
DH parameters($θ_i$, $d_i$, $a_i$, $α_i$) — joint angle, link offset, link length, link twistIntroduced here; consumed by 1.1.2 (IK), 1.1.3 (control)
DH transform$Rot_z(θ_i$)·$Trans_z(d_i$)·$Trans_x(a_i$)·$Rot_x(α_i$)Introduced here; consumed by the FK implementation
DH tableThe n×4 array of parametersIntroduced here; consumed by every FK implementation
Frame assignment$z_{i-1}$ = joint i’s axis; $x_i$ = common perpendicularIntroduced here; consumed by 1.1.2 (Jacobian’s columns)
$a_i$Link length along the common perpendicularIntroduced here; consumed by 1.1.2
$α_i$Link twist (the relative orientation of consecutive axes)Introduced here; consumed by 1.1.2
DH non-uniquenessThe table is not unique; the pose is invariantIntroduced here; consumed by the debugging mindset
PoE∏ exp(ξ̂_i $q_i$) · M — the product of exponentialsIntroduced here (restated from 0.2.3); consumed by 1.1.2
MThe end-effector’s pose at q = 0Introduced here; consumed by 1.1.2
$ξ_i$ (spatial)The joint’s twist at the home configuration, in the base frameIntroduced here (restated from 0.2.3); consumed by 1.1.2
DH-PoE equivalenceThe two forms give the same T(q) for the same qIntroduced here; consumed by the cross-check
Geometric JacobianJ(q) ∈ ℝ^{6×n}, $V_ee$ = J q̇Introduced here; consumed by 1.1.2 (IK), 1.1.3 (control)
Jacobian column$J_i$ = [$ω_i$; $ω_i$ × ($p_ee$ − $q_i$)] (revolute)Introduced here; consumed by 1.1.2 (the moment arm)
Moment arm$ω_i$ × ($p_ee$ − $q_i$) — the linear velocity contributionIntroduced here; consumed by 1.1.2
Position Jacobian$J_v$ = ∂p_ee/∂q (3×n)Introduced here; consumed by 1.1.2
Angular JacobianJ_ω = [$ω_i$] (3×n)Introduced here; consumed by 1.1.2
Singularityrank(J) < n; the arm loses a degree of freedomIntroduced here; consumed by 1.1.2
Design principle P13.1DH for physical/machinist, PoE for learned/softIntroduced here; consumed by 1.1.2
Design principle P13.2The q = 0 test and the non-zero test are necessary; both are requiredIntroduced here; consumed by 1.1.2
Design principle P13.3The Jacobian’s column is the twist transported to the end-effectorIntroduced here; consumed by 1.1.2
Design principle P13.4The DH/PoE cross-check is a strong testIntroduced here; consumed by 1.1.2’s lab
Design principle P13.5The variable slot identifies the joint typeIntroduced here; consumed by every DH table read
Design principle P13.6A conversion is a specification, not a testIntroduced here; consumed by 1.1.2’s cross-check
Design principle P13.7Write the Jacobian’s row order once and assert itIntroduced here; consumed by 1.1.2’s iteration
Design principle P13.8A test’s claim must be narrower than its nameIntroduced here; consumed by 1.1.2’s singularity tests

§L.8 — Exercises with solutions

E 1.1.1.1 — The DH transform (numerical). θ = π/3, d = 0.1, a = 0.2, α = −π/2. Compute the 4×4 T.

S 1.1.1.1. cos θ = 0.5, sin θ = 0.866. cos α = 0, sin α = −1. Row 0: [0.5, 0, −0.866, 0.1]; Row 1: [0.866, 0, 0.5, 0.173]; Row 2: [0, −1, 0, 0.1]; Row 3: [0, 0, 0, 1]. The lesson: the sign in row 1 is where this exercise is lost. The (1, 2) entry is −cos θ sin α = −0.5 · (−1) = +0.5, and the (0, 2) entry is sin θ sin α = 0.866 · (−1) = −0.866. Writing −0.5 at (1, 2) flips one entry of a matrix whose other entries are untouched, so the rotation block stops being orthogonal — but the translation column still reads [0.1, 0.173, 0.1], which is the part a learner checks first and it is unchanged. Compare the three rotation columns against each other: (0.5, 0.866, 0), (0, 0, −1), (−0.866, 0.5, 0) are mutually orthogonal and unit-norm; (−0.866, −0.5, 0) is orthogonal to the first but not to the second. That pairwise check costs three dot products and catches the sign.

E 1.1.1.2 — The q = 0 reach (numerical). For the running case’s DH table, what is $p_ee$ at q = 0?

S 1.1.1.2. (6a, 0, 0) = (1.2, 0, 0) — each $a_i$ = 0.2 adds 0.2 along x, and the $α_i$ rotations about x don’t change the x-axis. The lesson: the q = 0 reach is the sum of the link lengths, and the $α_i$’s don’t affect it (they are rotations about x).

E 1.1.1.3 — The flipped α (diagnostic). At q = [0, π/2, 0, 0, 0, 0], what is the end-effector with correct vs. flipped α?

S 1.1.1.3. Correct: (a, 0, −5a) = (0.2, 0, −1.0). Flipped: (a, 0, +5a) = (0.2, 0, 1.0). The difference is (0, 0, 2.0) — a 2 m displacement from the sign flip. The lesson: the α sign determines the direction of the downstream joints’ axes, and flipping it mirrors the arm across the z = 0 plane.

E 1.1.1.4 — The Jacobian’s moment arm (numerical). For a 2-DOF planar arm with $a_1$ = 0.5, $a_2$ = 0.3, at q = [π/2, 0], compute $J_1$’s linear part.

S 1.1.1.4. $p_ee$ = (0, 0.8, 0), $q_1$ = (0,0,0). $ω_1$ × ($p_ee$ − $q_1$) = (0,0,1) × (0, 0.8, 0) = (-0.8, 0, 0). The lesson: the joint at the origin contributes a linear velocity of -0.8 to the end-effector at (0, 0.8, 0), even though the joint’s own linear velocity is zero — the difference is the moment arm ω × $p_ee$.

E 1.1.1.5 — The PoE’s M (diagnostic). A learner’s PoE gives M = Trans_x(0.2) for the running case. What is wrong, and what is the correct M?

S 1.1.1.5. M = Trans_x(0.2) is only the last link’s offset, and the full M is Trans_x(1.2) — the sum of all six link offsets. The error is a factor of 6 underestimate, and the PoE’s q = 0 test fails (gives (0.2, 0, 0) instead of (1.2, 0, 0)). The fix is M = Trans_x(6a) = Trans_x(1.2). The lesson: M is the end-effector’s pose at q = 0, which is the product of the DH chain’s q = 0 transforms, not the last link’s offset.

E 1.1.1.6 — The DH vs. PoE cross-check (constructive). Write the pseudocode for verifying the DH and PoE forms at a specific q.

S 1.1.1.6.

function verify_fk(q):
    $T_dh$ = fk_dh(q)
    $T_poe$ = fk_poe(q)
    error = $max(|T_dh$ - $T_poe|$)
    assert error < 1e-10
    return $T_dh$

The test uses two independent code paths (the DH product and the PoE product), and their agreement is a strong correctness signal. The 1e-10 tolerance is the floating-point limit for float64. The lesson: the cross-check is stronger than either form alone, because a bug in one is not a bug in the other.

E 1.1.1.7 — The singularity (diagnostic, transfer probe). A 2-DOF planar arm with $a_1$ = $a_2$ = 0.5 has $J_v$’s determinant zero when $q_2$ = 0 or π. What is the physical meaning, and why does it matter for control?

S 1.1.1.7. At $q_2$ = 0 (arm fully extended), the two joints’ contributions to the end-effector’s radial velocity are parallel — the arm cannot move radially without moving tangentially, and a radial velocity requires an infinite joint velocity. The determinant $det(J_v$) = $a_1$ $a_2$ $sin(q_2$) = 0 at $q_2$ = 0, π. The physical meaning is the singularity — the arm loses a degree of freedom in the task space, and a controller that commands a radial velocity will saturate or diverge. The lesson: the Jacobian’s determinant is the local dexterity measure, and its zeros are the singularities where the inverse kinematics is undefined.

E 1.1.1.8 — The learned FK (transfer probe). A learner wants to learn the FK from data — they have a dataset of (q, $p_ee$) pairs, and they want a model $p_ee$ = f(q). Would they use the DH or PoE form?

S 1.1.1.8. The PoE form is the natural choice, because the twists $ξ_i$ and the home pose M are parameters that can be learned by gradient descent (they are continuous, unconstrained, and the exp map is differentiable). The DH form’s parameters are physical, and while they can also be learned, they are constrained (e.g., $α_i$ is usually ±π/2 or 0, and learning $α_i$ freely is unusual). The lesson: the PoE form is the learned representation, and the DH form is the physical representation — the choice is the task, and the differentiability of the exp map is the reason PoE is preferred for learning.


§L.9 — Exit test

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

  1. State the four DH parameters and their physical meanings.
  2. Apply the frame-assignment rules to a given chain, and derive its DH table.
  3. Write the DH transform and compute its 4×4 matrix for a given (θ, d, a, α).
  4. State the DH’s non-uniqueness and why the end-effector pose is invariant.
  5. Write the PoE form and state M and $ξ_i$’s roles.
  6. State the DH-PoE equivalence and the trade-offs between the two.
  7. State the geometric Jacobian’s 6×n structure and the [v; ω] vs. [ω; v] convention.
  8. Write the Jacobian’s column for a revolute joint and explain the moment arm.
  9. State the difference between the geometric Jacobian and the position Jacobian.
  10. Diagnose the q = 0 failure (wrong $a_i$), the mirror failure (flipped α), and the planar failure (all α = 0).
  11. State the q = 0 and non-zero tests as necessary and not sufficient alone.
  12. State the learned-FK choice (PoE) and the reason (differentiability).

If any item is not answerable without notes, return to the section that introduced it: 1 to §L.1, 2 to §L.1, 3 to §L.1.2, 4 to §L.1.1, 5 to §L.2, 6 to §L.2.1, 7 to §L.3, 8 to §L.3.1, 9 to §L.3, 10 to §L.5, 11 to §L.4.1, 12 to E 1.1.1.8.


End of Lesson 1.1.1. The learner has now completed the forward kinematics foundation: the DH table, the PoE, their equivalence, and the geometric Jacobian. The next lesson, 1.1.2 — Inverse Kinematics, uses this foundation to solve the inverse problem — given a desired end-effector pose, find the joint angles — and confronts the Jacobian’s singularities, the pseudoinverse, and the numerical methods that are the backbone of every robot controller.

§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. These claims are tier A, status R.

ClaimTierStatusWhere it is reproduced
The DH chain at q = 0 reproduces the analytic Trans_x(1.2): error 0.00e+00ARH1, matching the closed form
DH and PoE agree at the test configuration: error 1.11e-16ARH2
Flipping the α’s moves the end-effector by 2.0000 — a different pose, not a relabelingARH3
Zeroing the α’s (the “planar” variant) moves it by 1.0000ARH4, and the printed planar end-effector [0.2, 1.0, 0.0]
The geometric Jacobian’s linear part matches finite differences to 1.92e-10, and its angular part to 5.24e-11ARThe Jacobian check’s two printed residuals

What this lesson does not reproduce. The claim that the DH convention’s α and the PoE product’s screw axes are related by a specific reparametrization is stated and not derived: the lab checks that the two chains agree at a configuration (H2), which is a weaker statement than an identity between their parameters. That is tier B, status S — a standard relation asserted, with only its consequence tested. A reader who wants the map itself will not find it executed here.

The two negative results are the lesson’s real content. H3 and H4 are > 0.5 bounds, and both are met with room to spare (2.0 and 1.0). Their purpose is not precision but identification: they show that the convention’s choices change the physical pose, so a sign error in α is not a cosmetic difference in a table. A lesson that only checked DH-vs-PoE agreement would pass with the wrong convention applied consistently to both, which is the failure mode the mirror and planar arms exist to catch.

No external sources are cited in this lesson. The DH parameters, the homogeneous transform, and the PoE product are built in Parts A–B and executed by the lab. No paper is named; where the lesson says “the DH convention,” it is naming the construction it has defined, not appealing to a standard it has not stated.