Nyimbi Odero

Series 01 · a numbered run · kept in public

X1.1.2

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

Lesson 1.1.2 — Inverse Kinematics: The Analytical and Numerical Solutions, and the Singularities That Break Both

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

Prior results used. This lesson uses 0.2.1’s SE(3) inverse and point/vector distinction, 0.2.2’s SO(3) log and its π-singularity, 0.2.3’s twist and adjoint, 0.2.4’s from-to convention, and 1.1.1’s DH table, PoE form, and geometric Jacobian J(q) ∈ ℝ^{6×n} with its moment-arm column J_i = [ω_i; ω_i × (p_ee − q_i)]. Each is restated at the point of use.

Hands forward. This lesson hands forward one capability: the learner can formulate the inverse kinematics problem, derive the closed-form solution for the running case’s simplified 2-link arm, implement a damped-least-squares numerical IK that converges from a far initial guess and stays stable at singularities, identify the running case’s singular configurations from the Jacobian’s singular values, and diagnose the four IK-specific bugs (single-branch assumption, undamped pseudoinverse, stationary residual, wrong-axis Jacobian) from their end-effector symptoms — the capability that Lesson 1.1.3 (control), 2.3.2 (motion planning), and every pose → joint-angle pipeline in Phases 1–4 assumes.

Running case. The same 6-DOF MuJoCo arm from 1.1.1, but with two readings of the IK problem: (i) the full 6-DOF IK (given a target end-effector pose, find q ∈ ℝ⁶), which has no closed form for the general 6R arm (Pieper’s condition does not apply — the axes are not in the spherical-wrist configuration) and requires numerical methods; and (ii) the 2-link planar sub-arm (q₁, q₂ only, a₁ = a₂ = 0.2), which has a closed-form analytical IK with the classic two-branch (elbow-up/elbow-down) solution. The lesson’s central result is that the numerical IK converges from most initial guesses but fails at the arm’s singularities, and the fix is the damped least-squares (DLS) update that trades convergence speed for stability at the cost of a systematic end-effector error.


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

§L.0.1 — Lesson map

PartContent
AThe IK problem: existence, uniqueness, and the three methods (analytical, numerical, optimization)
BAnalytical IK: the 2-link planar arm, the two branches, the workspace boundary
CNumerical IK: Newton–Raphson, the Jacobian pseudoinverse, the iterative loop, convergence
DSingularities and DLS: the Jacobian’s SVD, manipulability, damping, the null space
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 IK problem q* = f⁻¹(T_target) and the three questions of existence (is there a solution?), uniqueness (how many?), and reachability (is the target in the workspace?).
  2. Derive the closed-form IK for the 2-link planar arm (q₁, q₂ from (x, y)), including the two branches (elbow-up and elbow-down) and the workspace annulus |a₁ − a₂| ≤ √(x² + y²) ≤ a₁ + a₂.
  3. Implement the Newton–Raphson iteration q_{k+1} = q_k + J(q_k)⁺(T_target − T(q_k)) with the Jacobian pseudoinverse, and state the convergence criterion (the pose error’s norm below a tolerance).
  4. State the three failure modes of the numerical IK — a far initial guess (slow or no convergence), a singularity (J⁺ unbounded), and a stationary residual (Jᵀe = 0 with ‖e‖ > 0) — and their discriminating tests (‖Jᵀe‖ for stationarity, then σ_min(J) to tell a singularity from a genuine local minimum).
  5. Diagnose the four IK-specific bugs — single-branch assumption, undamped pseudoinverse, stationary residual, wrong-axis Jacobian — from the end-effector’s behavior (a jump, a blow-up, a stall, a systematic offset).
  6. State the damped least-squares (DLS) update q_{k+1} = q_k + Jᵀ(JJᵀ + λ²I)⁻¹ e and its trade-off (convergence speed vs. stability at singularities).
  7. Compute the manipulability w(q) = √(det(JJᵀ)) and identify the running case’s singular configurations from the Jacobian’s smallest singular value.
  8. Produce a written “IK audit” of a target pose: its reachability, its branch (if multiple solutions exist), the initial guess and its convergence basin, the singularity proximity, and the final pose error.

§L.0.3 — Misconception inventory

Wrong model the learner probably holdsWhy it is attractiveWhere displacedThe correct model in one sentence
M1 — “Inverse kinematics has a unique solution.”The forward kinematics is a function (one output per input), and the inverse feels like a function too, with one input per output.§L.1 and §L.2 (Parts A and B)IK is a relation, not a function — the two-branch (elbow-up/elbow-down) solution of the 2-link arm is the simplest example, and the 6-DOF arm has up to 8 solutions (elbow × shoulder × wrist flips); the choice of branch is a decision the planner makes (which branch is collision-free? which is closest to the current config?), not a fact the IK returns.
M2 — “The Jacobian pseudoinverse J⁺ is the IK solution.”q = f⁻¹(T) feels like q = J⁻¹·(T_target − T_current) — the Jacobian is the FK’s derivative, and its inverse should “undo” the FK.§L.3 (Part C)J⁺ is the differential step, not the solution — it gives the direction to move in q-space to reduce the current error, but the FK is nonlinear, and a single J⁺ step does not reach the target (the error after one step is O(‖e‖²)); the IK is an iteration q_{k+1} = q_k + J⁺(q_k)·e_k, and the convergence is local (in the basin of the linearization’s validity).
M3 — “The pseudoinverse is the numerically stable choice at singularities.”J⁺ is the minimum-norm solution of Jx = e, and “minimum norm” sounds like “well-behaved.”§L.4 (Part D)At a singularity, the Jacobian’s smallest singular value is zero, and J⁺ amplifies the error’s component in the null direction by 1/σ_min = ∞ — the pseudoinverse is unbounded, not stable; the fix is damped least-squares Jᵀ(JJᵀ + λ²I)⁻¹, which bounds the amplification at the cost of a systematic error O(λ²).
M4 — “Numerical IK always converges for a reachable target.”The target is reachable, and the iteration reduces the error at each step, so it should converge.§L.3 (Part C)The iteration is local. It stops where the gradient dies — Jᵀe = 0 — and that can happen with ‖e‖ > 0 for two different reasons: the arm is at a singular configuration (the residual lies in a rank-deficient J’s null space) or the target is outside the reachable set. Neither is fixed by iterating longer; both need a different seed, which is what multiple restarts provide.
M5 — “The analytical IK exists for any arm.”The 2-link arm has a closed form, and the 6-DOF arm has six equations and six unknowns, so the closed form “should” exist.§L.2 (Part B)The general 6R arm’s IK is a degree-16 polynomial (the 6R’s “kinematic equation” is a high-degree system), and it has a closed form only under Pieper’s condition — three consecutive joint axes intersect (a spherical wrist) or three are parallel; the running case’s alternating z/y axes do not satisfy it, and its IK has no closed form (numerical or optimization is required).

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 fourteenth lesson. The prompt requires drawing from lessons two to five steps back; 1.1.1 is one step back and is excluded. The warm-up draws from 0.3.1–0.3.4 (two to five steps back).

Warm-up 1 (from 0.3.1, Bayes’ rule). State the four terms of Bayes’ rule and which of them is the normalizer. Answer: prior p(x), likelihood p(y|x), posterior p(x|y), evidence p(y) = ∫ p(y|x)p(x)dx — the evidence is the normalizer. How this is used: §L.5’s optimization-based IK can be viewed as a MAP problem — the prior is a preference for configurations near the current one, the likelihood is the pose error’s Gaussian, and the MAP solution is a regularized IK that prefers small joint movements. The IK’s “regularization” is the MAP’s prior, and the DLS’s damping is the prior’s precision.

Warm-up 2 (from 0.3.2, the log-likelihood and gradient). A loss L(θ) = ‖f(θ) − y‖² has gradient ∇L = 2 Jᵀ(f(θ) − y) where J = ∂f/∂θ. What is the Gauss–Newton step? Answer: θ_{k+1} = θ_k − (JᵀJ)⁻¹ Jᵀ(f(θ_k) − y), which is the pseudoinverse step θ_{k+1} = θ_k + J⁺(y − f(θ_k)) for the residual. How this is used: §L.3’s Newton–Raphson IK is the Gauss–Newton step applied to the pose residual e = pose(T_target) − pose(T(q)), and the pseudoinverse J⁺ is the same object as the Gauss–Newton’s (JᵀJ)⁻¹Jᵀ — the IK is a least-squares problem.

Warm-up 3 (from 0.3.3, the Kalman filter’s gain). The KF’s gain K = Σ⁻Hᵀ(HΣ⁻Hᵀ + R)⁻¹ has a +R in the denominator. What does the +R do, and why is it a regularizer? Answer: the +R (the measurement noise’s covariance) bounds the gain when HΣ⁻Hᵀ is small (i.e., when the measurement is uninformative), preventing the K from blowing up; the R is a Tikhonov regularization of the least-squares problem, and its role is the same as the DLS’s damping λ². How this is used: §L.4’s DLS update Jᵀ(JJᵀ + λ²I)⁻¹ is the same structure as the KF’s gain — the λ² is the “measurement noise” of the IK’s differential step, and its value is the damping that trades convergence speed for stability.

Warm-up 4 (from 0.3.4, the Mahalanobis gate). The Mahalanobis distance d² = νᵀS⁻¹ν divides by S (the innovation’s covariance). What is the inverse of a matrix with a zero eigenvalue, and what is the pseudoinverse’s behavior there? Answer: a matrix with a zero eigenvalue is singular and has no inverse; its pseudoinverse replaces 1/σ with 0 for the zero singular values, which is the minimum-norm solution — the zero direction is ignored. How this is used: §L.4’s singular Jacobian has a zero singular value, and the pseudoinverse ignores that direction (the arm cannot move in the null direction). The damping does not differ there — at σ = 0 the DLS gain σ/(σ² + λ²) is also 0. What the damping changes is the neighborhood: at σ = 10⁻³ the pseudoinverse gain is 1000 while the damped gain is 0.1; at σ = 10⁻² it is 100 against 0.99. The damping caps the amplification near a singularity, not at it — which is why §L.6.6 measures the trade-off at q₂ = 10⁻³ rather than at q₂ = 0.


Part A — The Inverse Kinematics Problem

§L.1 — Objects: the problem, the three questions, and the three methods

The inverse kinematics problem is: given a desired end-effector pose T_target ∈ SE(3), find a joint configuration q* ∈ ℝⁿ such that the forward kinematics T(q*) = T_target:

q∗=f−1(Ttarget)q^* = f^{-1}(T_{\text{target}})

The three questions:

  1. Existence — does a q* exist? The target must be in the robot’s workspace (the set of reachable poses), which is the image of the FK f: ℝⁿ → SE(3).
  2. Uniqueness — how many q* satisfy f(q*) = T_target? The FK is non-injective (multiple configurations give the same pose), and the number of solutions is the degree of the IK, which is up to 8 for a general 6-DOF arm (2 elbow × 2 shoulder × 2 wrist flips) and infinite for a redundant arm (n > 6).
  3. Reachability — is the target inside the workspace (a solution exists) or outside (no solution)? The workspace’s boundary is the set of poses at the arm’s maximum reach (fully extended) and minimum reach (fully folded), and the boundary is not convex in SE(3) — the arm’s orientation’s constraints shrink the reachable positions.

The three methods:

  1. Analytical (closed-form) — solve the FK’s equations exactly, using the arm’s geometry (the DH parameters’ structure). This gives all the solutions’ branches in closed form, and it is fast and exact. It is available only for arms that satisfy Pieper’s condition (three consecutive axes intersecting or parallel), which the running case does not (its axes alternate z/y and the distances between them are non-zero).
  2. Numerical (iterative) — start from an initial guess q₀ and iterate q_{k+1} = q_k + Δq_k with Δq_k computed from the Jacobian, until T(q_k) ≈ T_target. This is general (works for any arm), local (converges to the nearest solution, not the global one), and it fails at singularities without damping.
  3. Optimization-based — formulate the IK as a non-linear least-squares problem min_q ‖T_target − T(q)‖² + λ‖q − q₀‖², and solve it with a general optimizer (Levenberg–Marquardt, Gauss–Newton). This is a special case of the numerical method, with the regularization λ‖q − q₀‖² added, and it is the same structure as the DLS’s damping.

The choice of method:

ConditionMethodReason
Spherical wrist or parallel axes (Pieper)AnalyticalFast, exact, all branches
General 6R (like the running case)NumericalGeneral, local, damping required
Redundant arm (n > 6)Numerical + null-spaceThe extra DOF for the secondary objective
Offline (planning)OptimizationThe global solution, with the regularization
Online (control)Analytical or fast numericalThe real-time constraint, the warm start

§L.1.1 — Result: IK is a relation, not a function; the number of solutions is the FK’s degree, and it is not unique

Claim. For a general 6-DOF arm with spherical wrist (Pieper’s condition), the IK has up to 8 solutions (2 shoulder × 2 elbow × 2 wrist); for an arm without the spherical wrist (like the running case), the IK has up to 16 solutions (a degree-16 polynomial); for a redundant arm (n > 6), the IK has infinitely many solutions (a continuous family). The IK is a relation f⁻¹(T_target) ⊆ ℝⁿ, not a function, and the “solution” is a set.

Proof sketch. The FK is a system of 6 equations (the pose’s 6 DOF) in n unknowns (n joint angles). For n = 6, the system is square (6 equations, 6 unknowns), and its solutions are the roots of a polynomial system; Bezout’s theorem bounds the number of roots by the product of the degrees, which is 16 for the 6R’s kinematic equation (each joint contributes a degree-2 in the half-angle parameterization). For n > 6, the system is under-determined (fewer equations than unknowns), and the solution set is a manifold of dimension n − 6. The spherical wrist’s structure reduces the degree (the wrist’s q₄, q₅, q₆ can be solved independently of the position, giving 2 × 2 × 2 = 8 solutions instead of 16).

Corollary. The branch choice is a decision, not a fact — a planner chooses the branch that is collision-free, closest to the current configuration, or reachable within the joint limits. The IK’s output is a set of solutions, and the single solution is a post-selection. A learner who calls an IK library and receives a single q* has received one of the branches (usually the one closest to the seed), and the other branches are discarded.

Corollary. The workspace is the set of T_target that have a solution, and its boundary is the set of poses at the arm’s extremes — the fully extended arm (max reach Σ a_i) and the fully folded arm (min reach, which is not zero for a general arm, because the links have thickness and the joints have limits). A target outside the workspace has no solution, and the IK’s failure is physical, not a bug — the controller must reject the target or saturate the arm.

§L.1.2 — Worked-example ladder

Input. A 2-link planar arm with a₁ = a₂ = 0.5, target (x, y) = (0.6, 0.3).

Trace (existence).

  1. r = √(x² + y²) = √(0.36 + 0.09) = √0.45 ≈ 0.671.
  2. Workspace annulus: |a₁ − a₂| = 0 ≤ r ≤ a₁ + a₂ = 1.0.
  3. r = 0.671 is inside the annulus → a solution exists. ✓

Trace (uniqueness).

  1. The 2-link arm has two solutions for the elbow angle q₂: cos(q₂) = (r² − a₁² − a₂²)/(2a₁a₂) = (0.45 − 0.25 − 0.25)/(2·0.25) = (-0.05)/0.5 = -0.1.
  2. q₂ = ± arccos(-0.1) = ± 1.671 rad ≈ ± 95.7°. Two solutions (+ is elbow-up, − is elbow-down).
  3. For each q₂, there is one q₁ (the shoulder angle) — the two q₂’s give two q₁’s.
  4. Total: 2 solutions. ✓

Output and interpretation. The target (0.6, 0.3) is reachable (inside the annulus), and there are two IK solutions (q₂ = +1.671 and q₂ = −1.671). The branch choice is the planner’s — if the arm’s joint 2 has limits [−π/2, π/2], only the +1.671 branch is valid (−1.671 is outside the limits), and the IK’s solution set is filtered to the one valid branch. The q₁’s are atan2(y, x) − atan2(a₂ sin q₂, a₁ + a₂ cos q₂) for each q₂.

Self-explanation 1. Before reading on: in step 5, q₂ = ±1.671 rad. What if cos(q₂) were outside [-1, 1], e.g., cos(q₂) = 1.5?

Answer. cos(q₂) = 1.5 is outside the range, so there is no real q₂ — the target is unreachable. The arccos is undefined for |x| > 1, and the existence condition is exactly |cos(q₂)| ≤ 1, which is the workspace annulus |a₁ − a₂| ≤ r ≤ a₁ + a₂ in disguise (cos(q₂) = (r² − a₁² − a₂²)/(2a₁a₂) ∈ [−1, 1] iff r ∈ [|a₁ − a₂|, a₁ + a₂]). The existence check is the arccos’s domain, and the failure is the NaN from arccos(1.5).

Self-explanation 2. Before reading on: in step 5, q₂ = ±1.671 rad is almost ±95.7°. What does q₂ = 0 correspond to, and where is the arm?

Answer. q₂ = 0 is the fully extended arm (both links aligned), and the end-effector is at r = a₁ + a₂ = 1.0 — the maximum reach. At q₂ = π (the other boundary), the arm is fully folded (the links fold back on themselves), and the end-effector is at r = |a₁ − a₂| = 0 — the minimum reach. For q₂ between 0 and π, the arm is bent, and the reach is between the extremes. The q₂ = ±1.671 is a middle bend, giving r = 0.671, which is between 0 and 1.0 — a typical reach.

WE-L.1.b — Completion problem

Input. Same 2-link arm, target (x, y) = (1.2, 0).

Trace (final 25% as prompts).

  1. r = √(1.44) = 1.2. ⟨your turn: is this inside the annulus [0, 1.0]?⟩
  2. cos(q₂) = (r² − a₁² − a₂²)/(2a₁a₂) = (1.44 − 0.5)/0.5 = 1.88. ⟨your turn: is this in [-1, 1]?⟩
  3. What does the answer to step 2 mean? ⟨your turn⟩

Answers.

  1. r = 1.2 > 1.0 — outside the annulus.
  2. cos(q₂) = 1.88 > 1 — outside [-1, 1].
  3. The target is unreachable — the arm’s maximum reach is a₁ + a₂ = 1.0, and 1.2 > 1.0. The arccos(1.88) is undefined (a NaN), and the IK fails. The controller must reject the target or saturate the arm to the nearest reachable pose ((1.0, 0) for this direction).

The lesson. The existence check is the arccos’s domain |cos(q₂)| ≤ 1, which is the workspace annulus r ≤ a₁ + a₂. The symptom of an unreachable target is the NaN from arccos, and the fix is the saturating command (the nearest reachable target), not the damping.

WE-L.1.c — Faded: the 6-DOF arm’s IK solution count

Setup. The running case’s 6-DOF arm, with alternating z/y axes and no spherical wrist (a_i = 0.2 for all i).

Question. How many solutions does its IK have? Does the closed form exist?

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

Answer. The arm has no spherical wrist (the last three axes do not intersect at a point — the a_i = 0.2 offsets put each joint at a different location), so Pieper’s condition is not satisfied. The IK’s degree is up to 16 (the generic 6R’s bound), and it has no closed form in elementary functions. The IK is numerical (Part C) or optimization-based (Part D). The solutions (up to 16) are the branches — for a given target, there are up to 16 configurations that achieve it, and the numerical method converges to one of them (the one in its basin).

The lesson. The spherical wrist is the practical condition for a closed form — it decouples the position IK (the first 3 joints) from the orientation IK (the last 3 joints), reducing the degree from 16 to 8. The running case lacks the spherical wrist, so its IK is numerical. The trade-off: the closed-form arm (like a PUMA 560) is faster and exact, while the numerical arm (like the running case) is general but iterative and local.

WE-L.1.d — Contrastive near-miss: assuming the IK returns the “closest” solution

Correct solution. The IK returns one of the branches — the one in the iteration’s basin — and the planner must explicitly choose the branch (by validating the returned q* against the joint limits, the collision constraints, and the continuity with the previous q).

Near-miss. The learner assumes the IK returns the closest solution to the current configuration (because they seeded the iteration with it), and they do not validate the returned q* — it may be on a different branch if the target jumped across a workspace boundary, causing a sudden arm motion that violates the joint limits or collides with obstacles.

Why the near-miss produces a plausible but incorrect number. The IK returns a q* that exactly satisfies f(q*) = T_target — the pose is correct, and the test ‖f(q*) − T_target‖ < ε passes. The q* is far from the current q, but the learner’s assumption is that it is close (since the seed was the current q). The symptom is the arm’s jump — a large motion that the controller commands, which is valid as a pose but invalid as a trajectory (the joint limits, the collisions, the velocity limits). The bug is silent in the pose test and visible in the arm’s behavior.

The discrepancy, stated. The IK’s pose test ‖f(q*) − T_target‖ < ε and the trajectory test ‖q* − q_current‖ < δ are different tests, and the first passes while the second fails. The branch is the source — the IK found a solution, but not the one the planner wanted (the one close to the current configuration). The fix is the continuity check, the joint limits filter, or the multi-restart with the closest q* selected.

Self-explanation 3. Before reading on: in the near-miss, if the target jumps from one side of the workspace to the other, is the “closest” q* the one the IK returns?

Answer. No — the IK returns the q* in its iteration’s basin, and a basin is not a ball in joint space. Measured on the 2-link arm at target (0.6, 0.3): a seed closer to q⁻ converges to q⁺ in 47 of 400 random cases, and a seed closer to q⁺ converges to q⁻ in 59 (§WE-L.3.c). The “nearest branch” heuristic fails in both directions on more than a quarter of seeds. When the target jumps across a workspace boundary, the iteration may land in the other basin and return a branch far from the seed; the only reliable check is to validate the returned q* against the joint limits and the previous configuration, not to infer it from the distance.

§L.1.3 — Comparative table: the three methods

MethodWhat it solvesBranches returnedWorks whenFails whenCost
Analytical (closed form)the FK’s equations exactly, from the arm’s geometryall of them, in closed formPieper’s condition holds — three consecutive axes intersect (spherical wrist) or are parallelthe arm is a general 6R; the running case’s alternating z/y axes do not satisfy itclosed-form evaluation, one pass
Numerical (iterative)a sequence of local linearizationsone — whichever lies in the iteration’s basinthe seed is inside the basin and J is well conditionedthe seed is far, J is singular, or the target is outside the reachable settens of iterations, each a Jacobian and a solve
Optimization-baseda regularized least-squares objectiveone — the minimizer nearest the priora secondary objective or a prior on q is wantedthe same local minima; the regularizer biases the answera full solver, the most expensive of the three

Design principle (P14.6). The method is chosen by the arm and the deadline, not by preference. A spherical-wrist arm in an offline planner wants the closed form because it returns every branch and the planner can then choose among them; the same arm online wants the closed form warmed from the last cycle; a general 6R wants a numerical method with damping and a seed that is the previous configuration, because a cold seed buys nothing but iterations. The regularity that makes the closed form possible is a property of the mechanism, and no amount of solver tuning substitutes for it — which is why the running case is numerical from the first line.

§L.1.4 — Concept check

Concept check (displaces M5). A learner says: “The 6-DOF arm has six equations and six unknowns, so the closed form must exist — I just need a better symbolic solver.” What has the learner failed to model, and what bug class follows?

Answer. The learner has failed to model that a square system is not a solvable one: six equations in six unknowns is a polynomial system whose roots number up to 16, and a closed form in elementary functions exists only under Pieper’s condition — three consecutive axes intersecting at a point, or three parallel. The running case’s alternating z/y axes with a_i = 0.2 offsets satisfy neither, so its IK is a degree-16 system with no elementary solution. The bug class is a solver run until it returns something: the symbolic package will either run forever or return a numeric root dressed as a closed form, and the learner will believe the second because the first was expected to be slow. The practical consequence is not an error but a missing capability — no branch enumeration, so no planner choice among the up-to-16 solutions — and it is why the lesson spends Part C on iteration rather than on algebra.

§L.1.5 — Consolidation checkpoint 1

(a) You should now be able to: (i) state the three questions — existence, uniqueness, reachability — and which object answers each; (ii) say how many solutions a general 6R has and why the spherical wrist reduces the count; (iii) choose a method from the arm’s structure and the deadline, and say what each method cannot give you.

(b) Transfer probe. A 2-link arm has a_1 = a_2 = 0.5. A target sits at r = 0.671. A colleague says “the arm has two solutions, so I’ll take whichever the numerical solver finds first.” Is the count right, and what has the choice skipped? Answer sketch: the count is right — r is strictly inside the annulus [0, 1.0], so cos q_2 = −0.1 gives q_2 = ±1.671 and two configurations. The choice has skipped the selection: the two branches give the same pose and differ in q_1 by more than a radian, so whichever one the solver returns determines the arm’s posture, its clearance, and whether it is inside the joint limits. Taking the solver’s output is taking a branch, not avoiding the decision; a planner that never asks which branch it got has delegated the decision to the seed.

(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.


§L.2 — Objects: the closed-form solution and the two branches

The 2-link planar arm’s FK is x = a₁ cos q₁ + a₂ cos(q₁ + q₂), y = a₁ sin q₁ + a₂ sin(q₁ + q₂). The IK is:

r2=x2+y2,cos⁡q2=r2−a12−a222a1a2r^2 = x^2 + y^2, \quad \cos q_2 = \frac{r^2 - a_1^2 - a_2^2}{2 a_1 a_2} q2=±arccos⁡(cos⁡q2)q_2 = \pm \arccos(\cos q_2) q1=atan2⁡(y,x)−atan2⁡(a2sin⁡q2,  a1+a2cos⁡q2)q_1 = \operatorname{atan2}(y, x) - \operatorname{atan2}(a_2 \sin q_2, \; a_1 + a_2 \cos q_2)

The two branches are the ± in q₂: the + branch is elbow-up (the elbow is above the line from shoulder to end-effector), and the − branch is elbow-down (below).

The workspace annulus:

∣a1−a2∣≤r≤a1+a2|a_1 - a_2| \le r \le a_1 + a_2

The r < |a₁ − a₂| is inside the inner boundary (the arm cannot fold that tightly), and r > a₁ + a₂ is beyond the outer boundary (the arm cannot reach that far). Both give no solution.

The atan2’s role: it resolves the quadrant of the angle, which a naive arctan(y/x) cannot (the arctan is periodic in π and undefined at x = 0). The atan2(y, x) returns the angle in (−π, π] with the correct quadrant for every (x, y) — the 0-to-2π atan2 is the standard for IK.

Claim. For the 2-link planar arm the solution set has two points in the interior of the annulus (the two branches), one at the outer boundary r = a₁ + a₂, and none outside the annulus. At the inner boundary the count depends on the arm: for a₁ ≠ a₂ it is one point; for a₁ = a₂ it is a one-parameter family (r = 0, every q₁ with q₂ = π). The branch choice is the sign of q₂, and the two interior solutions have the same r but different q₁ and q₂.

Proof sketch. The cos(q₂) is the law of cosines applied to the triangle with sides a₁, a₂, r: r² = a₁² + a₂² − 2a₁a₂ cos(π − q₂) = a₁² + a₂² + 2a₁a₂ cos q₂, which rearranges to cos q₂ = (r² − a₁² − a₂²)/(2a₁a₂). The cos q₂ is in [−1, 1] iff r ∈ [|a₁ − a₂|, a₁ + a₂], which is the annulus. The q₂ = ± arccos(...) gives the two solutions, and the q₁’s follow from the geometry (the angle from the shoulder to the elbow, plus the angle from the elbow to the end-effector).

Corollary. At the outer boundary r = a₁ + a₂ (fully extended), cos q₂ = 1, q₂ = 0, and the two branches coincide — the solution count drops from 2 to 1, and this is a singularity of the Jacobian (Part D). At the inner boundary r = |a₁ − a₂| the two branches coincide only when a₁ ≠ a₂: there cos q₂ = −1, q₂ = ±π is the same angle, and atan2(a₂ sin q₂, a₁ + a₂ cos q₂) = atan2(0, a₁ − a₂) = 0 for both signs. For the lesson’s arm (a₁ = a₂) the inner boundary is r = 0, the denominator a₁ + a₂ cos q₂ = 0 vanishes, and the solution set becomes a one-parameter family — every q₁ with q₂ = π puts the end-effector at the origin, because the elbow can sit anywhere on the circle of radius a₁. The closed form still returns two points there (atan2(0, 0) = 0), which is the trap: it silently reports two solutions where there are infinitely many.

Corollary. The atan2 is essential — the arctan alone is ambiguous (the same y/x for opposite quadrants) and undefined at x = 0. The atan2’s range is (−π, π], and the angle wrapping (the q₁’s ± 2π for the joint limits) is the post-selection the planner does.

§L.2.2 — Worked-example ladder

WE-L.2.a — Fully worked: the two branches for a specific target

Input. 2-link arm with a₁ = a₂ = 0.5, target (x, y) = (0.6, 0.3).

Trace (the closed form).

  1. r² = 0.6² + 0.3² = 0.36 + 0.09 = 0.45, r ≈ 0.671.
  2. cos q₂ = (0.45 − 0.25 − 0.25)/(2·0.5·0.5) = (-0.05)/0.5 = -0.1.
  3. q₂ = ± arccos(-0.1) = ± 1.671 rad ≈ ± 95.7°.
  4. Branch + (q₂ = 1.671):
    • a₂ sin q₂ = 0.5·sin(1.671) ≈ 0.5·0.995 ≈ 0.498.
    • a₁ + a₂ cos q₂ = 0.5 + 0.5·(-0.1) = 0.45.
    • atan2(0.498, 0.45) ≈ 0.836 rad ≈ 47.9°.
    • atan2(y, x) = atan2(0.3, 0.6) = 0.464 rad ≈ 26.6°.
    • q₁ = 0.464 − 0.836 = −0.372 rad ≈ −21.3°.
    • Solution: q = (−0.372, 1.671).
  5. Branch − (q₂ = −1.671):
    • a₂ sin q₂ = 0.5·sin(−1.671) ≈ −0.498.
    • atan2(−0.498, 0.45) ≈ −0.836 rad.
    • q₁ = 0.464 − (−0.836) = 1.300 rad ≈ 74.5°.
    • Solution: q = (1.300, −1.671).

Output and interpretation. The two solutions are q⁺ = (−0.372, 1.671) (elbow-up) and q⁻ = (1.300, −1.671) (elbow-down). Both give the same end-effector (0.6, 0.3), verified by the FK. The branch choice: if the joint 1 has limits [−π/2, π/2], both are valid (−0.372 and 1.300); if joint 2 has limits [0, π], only the + branch is valid (the − branch has q₂ = −1.671 outside). The continuity with the previous q decides the final choice: the branch closer to the current configuration.

Self-explanation 4. Before reading on: in step 4, q₁ = -0.372 and q₂ = 1.671 — the arm’s elbow is up. What does “elbow-up” mean geometrically, and how does the sign of q₂ determine it?

Answer. “Elbow-up” means the elbow (the joint 2 location) is above the line from the shoulder (origin) to the end-effector. The q₂ > 0 means the second link is rotated counterclockwise from the first link’s direction (by the right-hand rule about z), which bends the arm upward if the first link points in the +x-direction. The q₂ < 0 bends it downward. The geometric test: the sign of q₂ (or, equivalently, the sign of the cross product (elbow − shoulder) × (end-effector − elbow)), and the two branches are the mirror images across the line from shoulder to end-effector.

Self-explanation 5. Before reading on: in step 3, cos q₂ = -0.1 is close to 0, so q₂ ≈ ±90°. What does this say about the arm’s shape, and where is it in the workspace?

Answer. q₂ ≈ ±90° means the two links are perpendicular, so the arm is in a right-angle bend. The reach is r = √(a₁² + a₂²) = √(0.25 + 0.25) = √0.5 ≈ 0.707 — close to the r = 0.671 we computed, and between the extremes (0.0 and 1.0). The right-angle configuration is the maximum manipulability for the 2-link arm (the Jacobian’s determinant is maximal there), which is why the target (0.6, 0.3) is easy to reach with good dexterity. The workspace’s “easy” region is the mid-radius (the right-angle bends), and the “hard” regions are the boundaries (the fully extended or fully folded arms).

WE-L.2.b — Completion problem

Input. Same arm, target (x, y) = (0.0, 0.5) (on the y-axis).

Trace (final 25% as prompts).

  1. r² = 0 + 0.25 = 0.25, r = 0.5. ⟨your turn: inside [0, 1]?⟩
  2. cos q₂ = (0.25 − 0.5)/0.5 = -0.5. ⟨your turn⟩
  3. q₂ = ± arccos(-0.5) = ± 2π/3 = ± 2.094 rad. ⟨your turn: verify⟩
  4. atan2(y, x) = atan2(0.5, 0) = π/2 = 1.571 rad. ⟨your turn⟩
  5. a₂ sin q₂ = 0.5·sin(2.094) ≈ 0.5·0.866 ≈ 0.433. a₁ + a₂ cos q₂ = 0.5 + 0.5·(-0.5) = 0.25. ⟨your turn⟩
  6. q₁ = atan2(y, x) − atan2(0.433, 0.25). ⟨your turn: compute for the + branch⟩
  7. For the − branch: ⟨your turn⟩

Answers.

  1. r = 0.5 is inside [0, 1.0]. ✓
  2. cos q₂ = -0.5. ✓
  3. arccos(-0.5) = 2π/3 ≈ 2.094. ✓
  4. atan2(0.5, 0) = π/2. ✓
  5. a₂ sin q₂ ≈ 0.433, a₁ + a₂ cos q₂ = 0.25. ✓
  6. atan2(0.433, 0.25) ≈ 1.047 rad = π/3 ≈ 60°. q₁ = 1.571 − 1.047 = 0.524 rad ≈ 30°. Solution: q⁺ = (0.524, 2.094).
  7. a₂ sin(-2.094) ≈ −0.433. atan2(-0.433, 0.25) ≈ -1.047 rad. q₁ = 1.571 − (−1.047) = 2.618 rad ≈ 150°. Solution: q⁻ = (2.618, -2.094).

The lesson. The two solutions are q⁺ = (0.524, 2.094) and q⁻ = (2.618, -2.094) — the + branch has the shoulder at 30° and the elbow at 120°, and the − branch has the shoulder at 150° and the elbow at −120°. The two are mirror images across the y-axis (the target’s line), and the arm’s shape is the same reflected — the geometry is the same, and the choice is the planner’s.

WE-L.2.c — Faded: the workspace’s inner boundary

Setup. Same arm (a₁ = a₂ = 0.5), target (x, y) = (0.0, 0.0) (the origin).

Question. Is the target reachable? What does the cos q₂ give?

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

Answer. r = 0. The annulus’s inner boundary is |a₁ − a₂| = 0, and the target’s r = 0 is on the boundary. The cos q₂ = (0 − 0.25 − 0.25)/0.5 = -1, and q₂ = ± arccos(-1) = ± π = ± 180° — the arm is fully folded (the two links are antiparallel, one over the other). The q₁ is undefined — the elbow is at (0, 0) (the origin), and the end-effector is also at (0, 0), so the arm’s two links are overlapping, and any q₁ (with q₂ = π) gives the same end-effector at (0, 0). The IK has infinitely many solutions at the inner boundary (the q₁ is free).

The lesson. The workspace’s boundary is where the IK’s solution count changes, but the two boundaries behave differently. At the outer boundary (r = a₁ + a₂), q₂ = 0 and the two branches coincide — one solution. At the inner boundary the answer depends on whether the links are equal. With a₁ ≠ a₂ the two branches also coincide (q₂ = ±π is one angle) and the solution count is one. With a₁ = a₂ — the lesson’s arm — the inner boundary is r = 0, the atan2’s denominator a₁ + a₂ cos q₂ vanishes, and the count is ∞: every q₁ with q₂ = π works. The general rule is that the solution count drops by one at the boundary unless the boundary is a degenerate point of the mechanism, which is what a₁ = a₂ makes r = 0. The Jacobian’s singularities are exactly these boundaries (§L.4).

WE-L.2.d — Contrastive near-miss: the arctan instead of atan2

Correct solution. q₁ = atan2(y, x) − atan2(a₂ sin q₂, a₁ + a₂ cos q₂) — the two atan2’s resolve the quadrants.

Near-miss. q₁ = arctan(y/x) − arctan(a₂ sin q₂ / (a₁ + a₂ cos q₂)) — the naive arctan’s.

Why the near-miss produces a plausible but incorrect number. The arctan’s range is (−π/2, π/2), and it cannot distinguish the first quadrant from the third (the same y/x) or the second from the fourth. For a target in the second quadrant (x < 0, y > 0), the arctan(y/x) returns a negative angle (the fourth quadrant’s), and the q₁ is off by π — the arm points to the opposite side. The error is silent (the IK returns a valid q₁), and the end-effector is at (−x, −y) instead of (x, y) — the mirror across the origin. The atan2’s range is (−π, π], which covers all four quadrants, and it is the only correct choice.

The discrepancy, stated. arctan(y/x) and atan2(y, x) agree only in the first and fourth quadrants (where x > 0). In the second and third quadrants (x < 0), the arctan is off by π — the atan2 adds π to correct the quadrant. The arctan’s range is half the atan2’s, and the missing half is the bug.

Self-explanation 6. Before reading on: in the near-miss, if the target is on the y-axis (x = 0), what does the arctan(y/0) give, and what does the atan2(y, 0) give?

Answer. arctan(y/0) is undefined (a ZeroDivisionError or ∞), so the naive IK crashes at x = 0. atan2(y, 0) is defined: it returns π/2 for y > 0, −π/2 for y < 0, and 0 for y = 0 (with x = 0, the origin). The atan2’s definition handles the x = 0 case gracefully, and the naive arctan fails. The atan2 is not a convenience — it is the correct function for the polar angle.

§L.2.3 — Comparative table: the candidates for the shoulder angle

CandidateExpressionCorrect quadrantDefined at x = 0What it returns in the second quadrant (x < 0, y > 0)
arctan(y/x)the naive single-argument arctangentno — range is (−π/2, π/2)no — divides by zeroa negative angle, off by π, pointing the arm to the opposite side
atan2(y, x)the two-argument arctangentyes — range is (−π, π]yes — returns ±π/2 for y ≠ 0the correct obtuse angle
atan2(y, x) − atan2(a₂ sin q₂, a₁ + a₂ cos q₂)the closed-form q₁yes, when both terms are atan2yes, except at the inner boundary where both arguments vanishthe correct q₁ for that branch
±arccos(cos q₂)the elbow anglen/an/athe two branches, which must be carried, not collapsed to one

Design principle (P14.7). A closed form is only as correct as its domain. The 2-link IK’s arithmetic is exact, and every failure in it is a domain failure: the arccos outside [−1, 1] is an unreachable target, the arctan outside the first and fourth quadrants is a reflected arm, and the atan2(0, 0) at the inner boundary is a free parameter reported as a value. Check the domain of each inverse function before trusting the value it returns, and treat the checks as part of the solution rather than as defensive code around it — the reachability test and the branch test are the same test as the arccos’s domain.

§L.2.4 — Concept check

Concept check (displaces M1). A learner says: “My IK function returns a q that puts the end-effector exactly on the target, so the problem is solved — there’s nothing left to decide.” What has the learner failed to model, and what bug class follows?

Answer. The learner has failed to model that IK is a relation: the pose test ‖f(q) − T_target‖ < ε passes for every branch, and a pass on that test says only that some configuration was found. The bug class is a valid pose delivered as an invalid trajectory: the target jumps across a workspace boundary, the solver lands in the other basin, and the returned q is correct as a pose and far from the current configuration as a motion — a commanded jump that violates the joint limits or passes through an obstacle. Nothing in the pose residual can see this, because the residual is measured in pose space and the defect is in joint space. The fix is a second test on the returned q — joint limits, continuity with the previous configuration, collision — and it must be a separate test, because a branch is a decision and the solver never made it.

§L.2.5 — Consolidation checkpoint 2

(a) You should now be able to: (i) derive cos q₂ from the law of cosines and read the workspace annulus off its domain; (ii) carry both branches through the q₁ formula and say what distinguishes them geometrically; (iii) state the solution count at each boundary, including the degenerate case a₁ = a₂ at r = 0.

(b) Transfer probe. A 2-link arm with a₁ = a₂ = 0.5 is asked for the target (0, 0). A colleague’s solver returns q = (0, π) and reports success. Is the answer wrong, and what did the solver not say? Answer sketch: q = (0, π) is a genuine solution — the links fold back and the tip sits at the origin — so the pose test passes. What the solver did not say is that the solution set is a one-parameter family: r = 0 is the inner boundary, a₁ + a₂ cos q₂ = 0 vanishes, and every q₁ with q₂ = π puts the tip at the origin. Reporting one point as “the” answer is the closed form’s silent failure at a degenerate boundary. The learner who takes it as the answer has a valid pose, no information about the family it belongs to, and no way to choose among them by any criterion the solver supplied.

(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 — Numerical IK: The Jacobian, the Pseudoinverse, and the Iteration

§L.3 — Objects: the Newton–Raphson iteration, the pose error, and the pseudoinverse

The numerical IK starts from an initial guess q₀ and iterates:

qk+1=qk+J(qk)+ ekq_{k+1} = q_k + J(q_k)^+ \, e_k

where:

  • e_k = pose_error(T_{target}, T(q_k)) is the pose error — a 6-vector, typically the twist that takes T(q_k) to T_target: e_k = log(T(q_k)^{-1} · T_target) ∈ ℝ⁶ (using 0.2.3’s log on SE(3)), or the position + orientation error [p_target − p(q); log(R(q)ᵀ R_target)].
  • J(q_k) ∈ ℝ^{6×n} is the geometric Jacobian from 1.1.1.
  • J⁺ is the Moore–Penrose pseudoinverse, which for a square invertible J is J⁻¹, for a wide J (n > 6, more joints than task dimensions) is Jᵀ(JJᵀ)⁻¹ (the right inverse, and the minimum-norm solution), and for a tall J (n < 6) is (JᵀJ)⁻¹Jᵀ (the left inverse, the least-squares solution). The label follows the shape of J, which is 6×n because the task is six-dimensional: n > 6 is wide, n < 6 is tall. Getting this backwards is not a cosmetic error — for n > 6 the product JᵀJ is n×n of rank 6 and (JᵀJ)⁻¹ does not exist, so the formula fails on exactly the case it was chosen for. The rule of thumb: more joints than tasks → the minimum-norm solution → the JJᵀ form, because JJᵀ is the 6×6 matrix that is invertible.

The convergence criterion: ‖e_k‖ < ε (the pose error’s norm below a tolerance, e.g., 1e-4 m and 1e-3 rad), or a maximum iteration count (e.g., 100), whichever comes first.

The three failure modes:

  1. Far initial guess — the error’s basin of convergence is local (the linearization is valid only in a neighborhood of q_k), and a far q₀ may not converge (the error increases or oscillates).
  2. Singularity — the Jacobian’s smallest singular value σ_min is zero (or near zero), and the J⁺’s component 1/σ_min blows up — the step J⁺e is large, and the iteration jumps. The behavior at exactly σ_min = 0 depends on which J⁺ you computed, and the two spellings give opposite symptoms: the mathematical Moore–Penrose inverse is defined at a singular J (the zero singular value is truncated, 1/0 → 0), so the step along the missing direction is zero and the iteration stalls — silently. The closed form (JᵀJ)⁻¹Jᵀ raises instead. See §L.3.1 for which of the two the lesson’s near-miss actually produces.
  3. Local minimum — the iteration reaches a stationary point of ½‖e‖² that is not a solution: Jᵀe = 0 while ‖e‖ > 0. The test is ‖Jᵀe‖, not ‖e‖ — a residual that stops shrinking means nothing on its own, because a singular configuration produces exactly the same symptom. The two are told apart by σ_min(J): at a singularity the residual lies in the null space of a rank-deficient J, and the fix is a different starting configuration; at a true local minimum J is full rank, so Jᵀe = 0 forces e = 0, and a non-zero stationary residual means the target is outside the reachable set or a joint limit is active. §L.6.6’s break-it #3 measures both quantities on the same stalled configuration and finds σ_min = 5·10⁻¹¹, rank 5/6, ‖Jᵀe‖ = 1.5·10⁻¹¹ — the singularity case.

§L.3.1 — Result: the Newton–Raphson iteration converges quadratically near a solution and fails at a singularity; which failure it fails with depends on which pseudoinverse you wrote

Claim. If q_k is sufficiently close to a solution q*, the iteration q_{k+1} = q_k + J⁺ e_k converges quadratically — the error after k+1 steps is O(‖e_k‖²). If J is near-singular, the J⁺’s norm is 1/σ_min, which is large, and the step overshoots; the iteration can fail to converge from a seed that would converge from a well-conditioned one.

Proof sketch. The iteration is the Newton–Raphson method applied to f(q) = T(q), with the “derivative” being the Jacobian. The convergence is quadratic if J is full rank and the initial guess is close — the standard Newton–Raphson theorem for nonlinear systems. The near-singular case: the J⁺’s norm is ‖J⁺‖ = 1/σ_min, and the error’s component along the near-singular direction is amplified by 1/σ_min. The step is large, the linearization that justified it is invalid over a large step, and the next iterate can be further from the solution than the last.

Proof sketch, the exact-singular case. The claim above is about near-singularity, and it must not be extended to σ_min = 0, where it is false. The Moore–Penrose inverse is continuous in J only on the set of fixed rank; at a rank drop it is not. Concretely: J⁺ = V Σ⁺ Uᵀ with the reciprocal of each zero singular value set to zero, so ‖J⁺‖ = 1/σ_r where σ_r is the smallest non-zero singular value — finite. The step is then the minimum-norm solution of J Δq = e, which moves the end-effector only in the directions J can reach. If e lies entirely in the unreachable subspace, the step is exactly zero and the iteration stalls at the seed, printing no error and making no progress. That is the silent version of the failure, and it is the one a lab using pinv will see. The closed form (JᵀJ)⁻¹Jᵀ is the one that raises LinAlgError, because JᵀJ is genuinely singular. Neither behavior is “divergence”.

Corollary. The convergence basin is the neighborhood of q* where the linearization is valid, which is ‖q − q*‖ = O(1/L) where L is the FK’s second derivative’s bound. The basin’s size depends on the arm’s configuration — it is larger near the mid-workspace (the well-conditioned region) and smaller near the singularities (where L is large).

Corollary. The iteration q_{k+1} = q_k + J⁺ e_k with the full J⁺ is the Newton–Raphson step, and it is not the same as q = J⁺ e (a single step) — the single step is the linear approximation, and the iteration is the nonlinear correction. A learner who calls q = J⁺ e once and stops has done one Newton step, which is not the solution.

§L.3.2 — Worked-example ladder

Input. 2-link arm (a₁ = a₂ = 0.5), current q = (0.0, 0.0) (fully extended along +x, end-effector at (1.0, 0.0)), target (0.6, 0.3).

Trace.

  1. T(q) · [0;0;0;1] = (1.0, 0.0, 0.0) — the current end-effector.
  2. e = T_target · [0;0;0;1] − T(q) · [0;0;0;1] = (0.6, 0.3, 0.0) − (1.0, 0.0, 0.0) = (-0.4, 0.3, 0.0).
  3. The Jacobian at q = (0, 0): from 1.1.1’s J = [ω_1 × (p_ee − q_1); ω_1; ω_2 × (p_ee − q_2); ω_2] (with ω_1 = ω_2 = (0,0,1), q_1 = (0,0,0), q_2 = (0.5, 0, 0), p_ee = (1.0, 0, 0)):
    • J_1 = [ω_1 × (p_ee − q_1); ω_1] = [(0,0,1) × (1,0,0); (0,0,1)] = [(0, 1, 0); (0,0,1)] (linear = (0, 1, 0), angular = (0,0,1)).
    • J_2 = [ω_2 × (p_ee − q_2); ω_2] = [(0,0,1) × (0.5, 0, 0); (0,0,1)] = [(0, 0.5, 0); (0,0,1)].
    • The full 6×2 Jacobian is [[0, 0], [1, 0.5], [0, 0], [0, 0], [0, 0], [1, 1]] (in [v; ω] convention).
    • For the planar arm, the position Jacobian J_v = [[0, 0], [1, 0.5], [0, 0]] and the angular J_ω = [[0, 0], [0, 0], [1, 1]].
  4. The position residual e_v = (-0.4, 0.3) (the z and angular parts are zero, since the arm is planar and the target has no orientation change).
  5. The pseudoinverse J_v⁺ = (J_vᵀ J_v)⁻¹ J_vᵀ:
    • J_vᵀ J_v = [[0, 1, 0], [0, 0.5, 0]] · [[0, 0], [1, 0.5], [0, 0]] = [[1, 0.5], [0.5, 0.25]].
    • (J_vᵀ J_v)⁻¹ = (1/(1·0.25 − 0.25)) [[0.25, -0.5], [-0.5, 1]] — det = 0, singular!
  6. The q = (0, 0) is a singularity (the arm is fully extended, and the Jacobian’s determinant is zero). The J_v⁺ does not exist (the inverse is undefined).
  7. The fix: use the DLS J_vᵀ(J_v J_vᵀ + λ²I)⁻¹, or use a non-singular starting q (e.g., q = (π/4, π/4)).

Output and interpretation. The q = (0, 0) (fully extended) is a singularity — the J_v’s determinant is zero, and the pseudoinverse does not exist. The numerical IK fails at this configuration, and the fix is to start from a non-singular q, or to use the DLS (Part D) that handles the singularity gracefully. The example is deliberately chosen at the singularity, to show the failure mode clearly.

Self-explanation 7. Before reading on: in step 5, the det(J_vᵀ J_v) = 0 — the singularity is exactly the fully extended arm (q₂ = 0). What is the physical meaning, and why does the IK fail there?

Answer. At the fully extended arm, the two joints’ contributions to the end-effector’s radial velocity are parallel — both push the end-effector outward along the x-axis, with no way to move it tangentially without both joints moving. The arm cannot move radially inward (it is at the outer boundary of the workspace), and a target inside the workspace requires a radial inward motion — which the fully extended arm cannot produce with a finite joint velocity. The det = 0 is the geometric fact, and the J_v⁺’s blow-up is the algebraic symptom.

Self-explanation 8. Before reading on: in step 7, q = (π/4, π/4) is a non-singular starting point. What is the det(J_v) there, and why does the iteration converge?

Answer. q = (π/4, π/4) gives q₂ = π/4, and det(J_v) = a₁ a₂ sin(q₂) = 0.5·0.5·sin(π/4) ≈ 0.177 — non-zero. The J_v is invertible, and the Newton step J_v⁺ e is finite. The iteration converges (quadratically, once it is near a solution) to the solution in its basin — and the basin is not the joint-space neighborhood. From q = (π/4, π/4) the iteration reaches q⁻ = (1.299, -1.671), the − branch (q₂ < 0), even though that seed is closer to the + branch: ‖q₀ − q⁺‖ = 1.457 against ‖q₀ − q⁻‖ = 2.510. The nearest branch in joint space is not the one the iteration finds.

WE-L.3.b — Completion problem

Input. Same arm, start at q = (π/4, π/4), target (0.6, 0.3). Compute two Newton steps.

Trace (final 25% as prompts).

  1. T(q₀) at q₀ = (π/4, π/4): p_ee = Rz(π/4) · (0.5, 0) + Rz(π/2) · (0.5, 0) = (0.354, 0.354) + (0, 0.5) = (0.354, 0.854). ⟨your turn: verify⟩
  2. e₀ = (0.6, 0.3) − (0.354, 0.854) = (0.246, −0.554). ⟨your turn⟩
  3. J_v at q₀: J_v = [ω × (p_ee − q_1); ω × (p_ee − q_2)]. q_1 = (0,0), q_2 = (0.354, 0.354), p_ee = (0.354, 0.854). J_1 = (0,0,1) × (0.354, 0.854, 0) = (-0.854, 0.354, 0) (the z-component zero), J_2 = (0,0,1) × (0, 0.5, 0) = (-0.5, 0, 0). So J_v = [[-0.854, -0.5], [0.354, 0]]. ⟨your turn: verify⟩
  4. J_v⁺ e₀: the 2×2 J_v’s inverse times e₀. det(J_v) = (-0.854)(0) − (-0.5)(0.354) = 0.177. J_v⁻¹ = (1/0.177) [[0, 0.5], [-0.354, -0.854]] = [[0, 2.825], [-2.0, -4.825]]. Δq = J_v⁻¹ · (0.246, -0.554) = (2.825·(-0.554), -2.0·0.246 + (-4.825)·(-0.554)) = (-1.565, -0.492 + 2.673) = (-1.565, 2.181). ⟨your turn⟩
  5. q₁ = q₀ + Δq = (0.785 − 1.565, 0.785 + 2.181) = (-0.780, 2.966). ⟨your turn⟩
  6. T(q₁) — is it closer to (0.6, 0.3) than q₀’s? ⟨your turn: compute p_ee at q₁⟩

Answers.

  1. p_ee ≈ (0.354, 0.854). ✓
  2. e₀ ≈ (0.246, −0.554). ✓
  3. J_v ≈ [[-0.854, -0.5], [0.354, 0]]. ✓
  4. Δq ≈ (-1.565, 2.181). ✓
  5. q₁ ≈ (-0.780, 2.966). ✓
  6. p_ee at q₁: Rz(-0.780)·(0.5, 0) = (0.355, -0.352). Rz(-0.780 + 2.966)·(0.5, 0) = Rz(2.186)·(0.5, 0) = (-0.290, 0.407). p_ee ≈ (0.067, 0.057). The new error is (0.6 − 0.067, 0.3 − 0.057) = (0.533, 0.243), which has norm 0.586 vs. the old 0.606 — slightly smaller, but not much smaller.

The lesson. The first Newton step overshoots (the Δq is (-1.565, 2.181), which is large), and the new error is smaller but not by the quadratic factor (the linearization is poor for a large e₀, and the step overshoots the linearization’s validity). The second step would be smaller (the linearization is better near the solution), and the convergence is eventual (after several steps) but not in one step. The e₀’s large magnitude is the far initial guess — the iteration converges but slowly (the basin is large here, but the linearization’s error dominates the first steps).

WE-L.3.c — Faded: which branch the iteration finds

Setup. 2-link arm, target (0.6, 0.3), whose two analytical solutions are q⁺ = (-0.372, 1.671) (elbow-up) and q⁻ = (1.299, -1.671) (elbow-down). The iteration starts at q₀ = (2.5, -1.0).

Question. Which branch does the iteration converge to, and is it the branch closest to q₀?

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

Answer. The iteration converges to q⁺ = (-0.372, 1.671) — the farther branch. In joint space q₀ sits 1.376 from q⁻ and 3.922 from q⁺, so a “nearest branch” rule picks the wrong one. The basin is not a joint-space ball: the Newton step is J_v⁻¹ e, and J_v⁻¹ is large and badly conditioned away from the solution, so the early iterates are thrown around the joint space (q₀ = (2.5, -1.0) → (3.518, -4.565) → (2.739, -1.572) → …, with the error going up before it comes down: 1.086 → 1.226 → 0.934 → 0.791 → 1.219 → …) before settling at step 9 with ‖e‖ = 1.7·10⁻¹⁴.

The seed-level result generalizes, and the numbers below are what four independent generators give for 400 seeds uniform in [-3, 3]² with pinv and angles wrapped to (-π, π] before the distance is taken:

generator seedconvergedcloser to q⁺, got q⁻closer to q⁻, got q⁺rule fails
0294 / 400414128%
1310 / 400614334%
7306 / 400574032%
42304 / 400674437%

Between a quarter and a third of converging seeds land on the branch that is not the nearest one, in both directions. The exact pair of counts moves with the generator; the fraction does not. Quote the fraction, or quote a table like this one — a single pair of integers from an unstated generator is not a reproducible claim, and it is exactly the kind of number a reader will try to reproduce and fail to.

The lesson. A basin is a region of error space defined by where the linearization points, not a ball in joint space. Seeding with “the current configuration” makes the answer look close to the seed, but nothing in the iteration enforces that, and ‖q* − q_seed‖ cannot be used to decide which branch you got. The planner must check the returned branch against joint limits and collisions; it cannot infer it from the seed.

WE-L.3.d — Contrastive near-miss: the single-step “solution”

Correct solution. Iterate q_{k+1} = q_k + J⁺ e_k until ‖e_k‖ < ε, with multiple steps.

Near-miss. q* = q₀ + J⁺(q₀) · e₀ — one Newton step, called the “solution”.

Why the near-miss produces a plausible but incorrect number. The single step reduces the error by O(‖e₀‖²) (the Newton step’s quadratic convergence is for the full iteration, and the first step’s reduction is first-order in ‖e₀‖ if the linearization is good, and worse if it is poor). For a small e₀, the single step is a good approximation (the quadratic reduction is small for small e₀), and the learner who tests with a small error sees a plausible result. For a large e₀, the single step overshoots, and the new error is larger than the old (the linearization is invalid for the large step). The error is silent in the small-error test and visible in the large-error test, and the symptom is the overshoot — the arm jumps past the target.

The discrepancy, stated. q* = q₀ + J⁺ e₀ is the linear solution, and q* = q₀ + J⁺ e₀ + O(‖e₀‖²) is the nonlinear solution. The two differ by the second-order term, which is small for a small e₀ and large for a large e₀. The iteration q_{k+1} = q_k + J⁺ e_k reduces the error at each step, and the convergence is to the true solution — the single step is not.

Self-explanation 9. Before reading on: in the near-miss, if e₀ is exactly zero (the seed is the solution), what does the single step give?

Answer. q* = q₀ + J⁺ · 0 = q₀ — the seed is the solution, and the single step leaves it unchanged. The single step is correct at the solution and incorrect elsewhere, and the iteration is the same at the solution (no step needed). The e₀ = 0 case is degenerate (no iteration needed), and the single step coincides with the true solution only there.

§L.3.3 — Comparative table: the candidate step rules

Step ruleShape it is valid forWhat it returnsAt n > 6At a rank drop
(JᵀJ)⁻¹Jᵀtall J, n ≤ 6, full column rankthe least-squares solutiondoes not exist — JᵀJ is n×n of rank at most 6, so it is singular for exactly the case the rule was reached forraises LinAlgError, which is at least loud
Jᵀ(JJᵀ)⁻¹wide J, n ≥ 6, full row rankthe minimum-norm solutionexists and is the right one — JJᵀ is 6×6 and invertibleJJᵀ becomes singular and the rule fails
V Σ⁺ Uᵀ (Moore–Penrose, via SVD)any shape, any rankthe minimum-norm least-squares solutionexiststruncates 1/0 → 0: finite, and the step along the missing direction is zero
Jᵀ(JJᵀ + λ²I)⁻¹ (DLS)any shape, any ranka bounded approximation to the aboveexistsbounded at 1/(2λ), with a systematic bias O(λ²)
one application of any of the above—a single Newton step, O(‖e‖²) from the answer——
the same rule applied in a loop—the solution, when the iteration converges——

Design principle (P14.8). Read the shape of J before choosing the formula. J is 6×n because the task is six-dimensional, so n > 6 makes it wide and n < 6 makes it tall — the opposite of the intuition that more joints means a bigger square matrix. The consequence is not cosmetic: for a redundant arm JᵀJ is n×n of rank 6 and (JᵀJ)⁻¹ does not exist, so the tall formula fails on precisely the arms it is most often written for. More joints than tasks means the minimum-norm solution, which is the JJᵀ form, because JJᵀ is the 6×6 matrix that stays invertible. Assert the branch on n in the code rather than in a comment.

§L.3.4 — Concept check

Concept check (displaces M2). A learner says: “IK is q = J⁺(T_target − T(q)) — I invert the Jacobian, apply it to the error, and that’s the joint configuration.” What has the learner failed to model, and what bug class follows?

Answer. The learner has failed to model that J⁺ is a differential step, not a solution. The Jacobian is the FK’s derivative at one configuration, so J⁺e is exact only for the linearization it came from; the FK is nonlinear, and the error after one step is O(‖e‖²), which is negligible only when e is already small. The bug class is a solution that is accurate at the scale it was tested and wrong at the scale it is used: a unit test seeded close to the target passes with a single step, and the same code seeded at a realistic distance overshoots, because the linearization that justified the step is invalid over the step’s own length. The fix is the loop — q_{k+1} = q_k + J⁺(q_k)e_k until ‖e_k‖ < ε — and the loop is the object; the step is a line of it.

§L.3.5 — Consolidation checkpoint 3

(a) You should now be able to: (i) write the pose error and say which of its two forms the iteration consumes; (ii) pick the pseudoinverse’s spelling from the shape of J and say why the tall form fails for n > 6; (iii) state the three failure modes and the test that separates a stationary residual from a genuine local minimum.

(b) Transfer probe. The 2-link arm sits at q = (0, 0), fully extended, and the target is (0.6, 0.3). A colleague runs an iteration built on torch.linalg.pinv, sees q unchanged after 100 iterations, and reports “converged — the pose error must be below tolerance.” What actually happened, and what single number settles it? Answer sketch: the configuration is exactly singular — both linear columns are (0, 1, 0) and (0, 0.5, 0), so the rank is 1 and the residual’s inward component lies in the unreachable subspace. The Moore–Penrose inverse is finite there, with the zero singular value truncated to zero, so the step along that direction is exactly zero: the iteration stalls at the seed, prints no error, and makes no progress. It did not diverge and it did not converge. The number that settles it is σ_min(J) — zero here, which identifies the stall as a singularity rather than a local minimum — together with ‖e‖, which is still 0.5 and was never checked against the tolerance the colleague claimed.

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


Part D — Singularities, Damping, and the Null Space

§L.4 — Objects: the singular values, manipulability, DLS, and the null-space projection

The Jacobian’s singular value decomposition (SVD) is J = U Σ Vᵀ, with Σ a diagonal matrix of singular values σ₁ ≥ σ₂ ≥ … ≥ σ_min ≥ 0. The rank is the number of non-zero σ_i, and the smallest σ_min is the measure of proximity to a singularity:

  • σ_min = 0 — a singularity (the rank is less than n or 6).
  • σ_min small — near-singular (the J⁺’s norm 1/σ_min is large).
  • σ_min = O(1) — well-conditioned (the J⁺’s norm is O(1)).

The manipulability is w(q) = √(det(JJᵀ)) = ∏ σ_i — the product of the singular values, which is zero at a singularity and maximal at the most dexterous configuration. For the 2-link arm, w(q) = a₁ a₂ |sin q₂|, maximized at q₂ = ±π/2 (the right angle).

The damped least-squares (DLS) update is:

qk+1=qk+J⊤(JJ⊤+λ2I)−1 ekq_{k+1} = q_k + J^{\top} (J J^{\top} + \lambda^2 I)^{-1} \, e_k

The damping λ bounds the J⁺’s amplification — the effective 1/σ_i is replaced by σ_i/(σ_i² + λ²), which is bounded by 1/(2λ) and tends to 1/σ_i for $σ_i$ ≫ λ. The trade-off:

  • λ → 0: the DLS becomes the pseudoinverse (the unbounded case).
  • λ → ∞: the DLS becomes the gradient descent $q_{k+1}$ = $q_k$ + (1/λ²) Jᵀ $e_k$ (the slow but stable case).
  • Intermediate λ: the DLS balances convergence speed and stability, and the systematic error (the DLS’s step’s deviation from the pseudoinverse’s step) is O(λ²).

The null-space projection is the redundancy exploitation: for a redundant arm (n > 6), the Jacobian’s n − 6 null directions correspond to joint motions that do not move the end-effector, and the null-space joint velocity (I − J⁺J) q̇_0 can be used for a secondary objective (collision avoidance, joint-limit avoidance) without affecting the primary pose. The projection I − J⁺J is the orthogonal complement of the row space of J, and the secondary velocity is invisible to the pose.

§L.4.1 — Result: the pseudoinverse’s norm is 1/σ_min, and the DLS’s damping bounds it at 1/(2λ); the systematic error is O(λ²), and the choice of λ is the trade-off

Claim. The pseudoinverse’s norm is ‖J⁺‖ = 1/σ_min, which is unbounded as $σ_min$ → 0. The DLS’s effective inverse has the effective singular values σ_i/(σ_i² + λ²), bounded by 1/(2λ), and the DLS’s step deviates from the pseudoinverse’s by a bounded amount that is systematic — the DLS biases the solution toward the null direction, and the bias shrinks as O(λ²).

Proof sketch. The SVD gives J = U Σ Vᵀ, and J⁺ = V Σ⁺ Uᵀ with Σ⁺ the reciprocals of the singular values (zero for $σ_i$ = 0). The norm ‖J⁺‖ = 1/σ_min. The DLS’s effective $Σ_DLS$ has entries σ_i/(σ_i² + λ²), which is bounded by 1/(2λ) (the maximum of x/(x² + λ²) is 1/(2λ) at x = λ). The difference is exact, not asymptotic:

1/σ − σ/(σ² + λ²) = λ²/(σ(σ² + λ²)).

For σ ≫ λ this is O(λ²/σ³), and for σ ≪ λ it tends to 1/σ — the pseudoinverse’s own value, because the DLS’s contribution has gone to zero. So the deviation is largest exactly where the pseudoinverse is largest; the damping does not add error there, it removes the whole term.

Corollary. The damping choice is the trade-off: a small λ converges fast but is unstable near singularities; a large λ is stable but slow and biased. The adaptive choice λ(q) = $λ_0$ · (1 − σ_min/σ_threshold) (or λ = 0 when $σ_min$ > $σ_threshold$) is a common fix, which damps only near singularities.

Corollary. The null-space projection I − J⁺J is PSD (a projection), and it is exactly zero for a non-redundant arm (n = 6, full-rank J). For a redundant arm (n > 6), the null-space dimension is n − 6, and the secondary velocity is non-zero in the null directions.

§L.4.2 — Worked-example ladder

Input. 2-link arm (a₁ = a₂ = 0.5), at q = (0, 0), (π/4, π/2), and (0, π/2).

Trace.

  1. q = (0, 0): q₂ = 0, w = 0.5·0.5·|sin 0| = 0 — singular (fully extended).
  2. q = (π/4, π/2): q₂ = π/2, w = 0.5·0.5·|sin(π/2)| = 0.25 — maximal manipulability (the right angle).
  3. q = (0, π/2): q₂ = π/2 (same as above), w = 0.25 — the same manipulability (the shoulder doesn’t affect w for the 2-link planar arm).

Output and interpretation. The manipulability is zero at the fully extended arm and maximal at the right angle. The shoulder angle q₁ does not affect w (the arm’s shape is the same, only its global orientation changes), and the elbow angle q₂ determines w. The w = 0 at q₂ = 0 and q₂ = π is the workspace boundary, and the w = 0.25 at q₂ = π/2 is the most dexterous configuration.

Self-explanation 10. Before reading on: in step 1, q = (0, 0) gives w = 0 — the manipulability is zero. What is the physical meaning, and why is it a singularity?

Answer. The manipulability is the “ease of motion” — the volume of the ellipsoid that the end-effector can sweep with a unit joint-velocity ball. At q = (0, 0) (fully extended), the arm’s two joints both push the end-effector radially (along +x), and the tangential direction has zero velocity — the end-effector can only move outward (or not at all), and any tangential motion requires infinite joint velocity. The w = 0 is the degenerate ellipsoid (a line, not a volume), and the w = 0 is the singularity — the arm’s velocity constraints make some task-space directions unreachable.

WE-L.4.b — Completion problem

Input. Same arm, target at q₂ = π/2 (the right angle), and the DLS with λ = 0.1.

Trace (final 25% as prompts).

  1. The singular values of $J_v$ at q = (π/4, π/2): the 2×2 $J_v$ = [[−a₁ sin q₁ − a₂ sin(q₁+q₂), −a₂ sin(q₁+q₂)], [a₁ cos q₁ + a₂ cos(q₁+q₂), a₂ cos(q₁+q₂)]]. At q₁ = π/4, q₂ = π/2: −a₁ sin(π/4) − a₂ sin(3π/4) = −0.354 − 0.354 = −0.707; −a₂ sin(3π/4) = −0.354; a₁ cos(π/4) + a₂ cos(3π/4) = 0.354 − 0.354 = 0; a₂ cos(3π/4) = −0.354. So $J_v$ = [[−0.707, −0.354], [0, −0.354]]. ⟨your turn: verify⟩
  2. Singular values: $J_v$ᵀ $J_v$ = [[0.5, 0.25], [0.25, 0.25]]. Its eigenvalues are 0.6545 and 0.0955, so the singular values are their square roots: σ₁ ≈ 0.809, σ₂ ≈ 0.309. (Do not read the eigenvalues as singular values — that is a factor-of-√σ error, and it is the usual one here.) ⟨your turn: verify⟩
  3. J⁺’s norm: 1/σ_min ≈ 3.236. The arm is moderately conditioned — the condition number σ₁/σ₂ = 2.618. ⟨your turn⟩
  4. DLS’s effective 1/σ_min: σ_min/(σ_min² + λ²) = 0.309/(0.0955 + 0.01) = 0.309/0.1055 ≈ 2.929, against the pseudoinverse’s 3.236 — the damping shrinks the inverse by 9.5%. ⟨your turn⟩
  5. At a near-singular q = (0, 0.1): $σ_min$ ≈ 0.0224. Pseudoinverse 1/σ_min = 44.74, DLS 0.0224/(0.0005 + 0.01) ≈ 2.129 — the DLS is 21× smaller than the pseudoinverse. ⟨your turn⟩

Answers.

  1. $J_v$ ≈ [[−0.707, −0.354], [0, −0.354]]. ✓
  2. Singular values ≈ (0.809, 0.309). ✓
  3. ‖J⁺‖ ≈ 3.236. ✓
  4. DLS effective ≈ 2.929 — the damping’s effect is small here (the condition number is moderate).
  5. At q = (0, 0.1), the DLS caps the amplification at 2.13 vs. the pseudoinverse’s 44.7 — a 21× reduction, which is the stability the damping provides.

The lesson. The damping’s effect is proportional to the condition number — at a well-conditioned configuration, the DLS is almost the pseudoinverse; at a near-singular configuration, the DLS is much smaller. The choice of λ is the trade-off: λ = 0.1 is a good default, but the adaptive λ (large near singularities) is better for a general arm.

WE-L.4.c — Faded: the null-space projection for a redundant arm

Setup. A 7-DOF redundant arm (n = 7), with the Jacobian J ∈ ℝ^{6×7} and a secondary objective g(q) = ‖q − $q_nominal$‖² (a preference for the nominal configuration).

Question. Write the null-space update that moves the arm toward $q_nominal$ without changing the end-effector’s pose.

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

Answer. The primary step is J⁺ e, and the secondary step is (I − J⁺J) q̇_secondary where q̇_secondary = −∇g = −2(q − $q_nominal$). The combined step is $q_{k+1}$ = $q_k$ + J⁺ e + α (I − J⁺J) q̇_secondary where α is the secondary gain. The projection I − J⁺J removes the primary-space component of q̇_secondary, so the secondary motion is null (the end-effector does not move), and the arm drifts toward $q_nominal$ without affecting the pose.

The lesson. The null-space projection is the redundant arm’s feature — the n − 6 extra DOF can be used for a secondary objective without sacrificing the primary pose. The α is the secondary gain, and its choice is the trade-off between the primary convergence and the secondary progress.

WE-L.4.d — Contrastive near-miss: the undamped pseudoinverse at the singularity

Correct solution. J⁺ with damping Jᵀ(JJᵀ + λ²I)⁻¹ — the bounded step.

Near-miss. J⁺ = (JᵀJ)⁻¹Jᵀ — the undamped pseudoinverse, which at a singularity has infinite norm.

Why the near-miss produces a plausible but incorrect number. At a well-conditioned q (the normal case), the undamped J⁺ is finite and correct, and the iteration converges to the solution — the bug is invisible. At a near-singular q, the J⁺’s norm 1/σ_min is large (e.g., 40 for $σ_min$ = 0.025), and the step J⁺ e is huge — the arm jumps to a far configuration, which is not the solution (the linearization is invalid for the large step). The error is silent at well-conditioned configurations and catastrophic near singularities, and the symptom is the arm’s jump (a large, discontinuous motion).

The discrepancy, stated. J⁺ and the DLS’s Jᵀ(JJᵀ + λ²I)⁻¹ have effective singular values 1/σ and σ/(σ² + λ²), which differ by λ²/(σ(σ² + λ²)). For σ ≫ λ that is O(λ²/σ³) — negligible. For σ ≪ λ the DLS’s value tends to σ/λ² → 0 while the pseudoinverse’s 1/σ → ∞: the damping does not perturb a large number, it replaces it. The near-singular case is therefore the failure — the pseudoinverse’s huge step overshoots, and the iteration fails to converge. The DLS’s damping bounds the step at 1/(2λ), and the iteration converges (slowly) but does not fail. Measured on the 2-link arm at q = (π/4, π/2) with e = (0.1, 0.2): the steps differ by 0.0527 at λ = 0.1, 0.0142 at λ = 0.05, 0.0036 at λ = 0.025. The successive ratios are 3.72 and 3.92, and the ratio gap/λ² is 5.27, 5.68, 5.79 — drifting upward toward its σ ≪ λ limit rather than sitting at a constant. Quoting “a factor of 3.7 for each halving” is right for the first halving and wrong for the second; the honest statement is that the gap is O(λ²) over this window with a coefficient that has not yet converged, and the coefficient approaches 1/σ_min from below as λ → 0.

Self-explanation 11. Before reading on: in the near-miss, if the q is exactly singular ($σ_min$ = 0), what does the undamped J⁺ give?

Answer. It depends on how you compute it, and the difference is the whole point. The mathematical Moore–Penrose pseudoinverse is defined at a singular J — it is V Σ⁺ Uᵀ with 1/0 replaced by 0 — so the answer is not NaN; it is a finite matrix that simply cannot move the end-effector along the missing direction. (JᵀJ)⁻¹Jᵀ, the formula the lesson uses, is the one that crashes: torch.linalg.inv raises _LinAlgError and numpy.linalg.inv raises LinAlgError. But torch.linalg.pinv does not raise — it truncates the zero singular value and returns [[0.0, 0.8], [0.0, 0.4]] for the 2-link arm at q = (0,0). So “the singularity gives a loud error” is true only for the (JᵀJ)⁻¹ spelling, and a lab that uses pinv sees the silent version: a finite step in the wrong direction, or no step at all. The near-singular case ($σ_min$ small but non-zero) is the same failure without the crash: J⁺ is finite but huge, and the iteration jumps.

§L.4.3 — Comparative table: the update rules near a singularity

RuleEffective gain on $σ_i$BoundBehavior at $σ_min$ = 0Behavior at $σ_min$ = 10⁻³ with λ = 0.1Cost
1/σ_i (undamped pseudoinverse)1/σ_inone — ‖J⁺‖ = 1/σ_min1/0 → 0 under the SVD definition; (JᵀJ)⁻¹Jᵀ raises1000 — the step jumpsone SVD or one solve
σ_i/(σ_i² + λ²) (DLS)bounded1/(2λ)0 — same as the pseudoinverse0.1 — 10,000× smallerone solve, λ² added to the diagonal
σ_i/(σ_i² + λ²) with λ → ∞σ_i/λ²—0gradient descent: slow, stable—
adaptive λ(q)bounded, small away from singularity1/(2λ(q))0pseudoinverse-like where $σ_min$ is healthya check per step
(I − J⁺J) q̇_0 (null-space term)n/a — acts only in the null space—the projection survives, the primary step does notthe secondary motion is invisible to the poseone projection

Design principle (P14.9). Damping trades a bounded error for an unbounded one. The pseudoinverse is exact wherever it is finite and unbounded wherever it matters; the DLS is bounded everywhere and exact nowhere. The trade is not a compromise between two estimates of the same quantity — the damping replaces the term rather than perturbing it, since 1/σ − σ/(σ²+λ²) = λ²/(σ(σ²+λ²)) tends to 1/σ as σ → 0, which is to say the DLS’s own contribution vanishes and the whole of the pseudoinverse’s is gone. So state the bias as part of the answer: a DLS-driven arm converges to a pose that is wrong by O(λ²), and a controller that needs the last millimeter must remove the damping near convergence rather than reduce the tolerance.

§L.4.4 — Concept check

Concept check (displaces M3). A learner says: “I’ll use the pseudoinverse at singularities because it’s the minimum-norm solution — minimum norm is the numerically stable choice.” What has the learner failed to model, and what bug class follows?

Answer. The learner has failed to model what the norm is the minimum of: J⁺e is the smallest Δq that achieves the step e, and “smallest” is a statement about the step, not about the gain that produces it. The gain is 1/σ_min, so the norm is minimal precisely by being the limit of an unbounded amplification — the two facts are the same fact. The bug class is a step that is minimal in joint space and enormous in joint space: near a singularity $σ_min$ is small, 1/σ_min is large, and the minimum-norm step is still a huge one, so the arm jumps to a far configuration whose linearization is invalid, and the next error is larger than the last. At an exact singularity the behavior changes character rather than severity — the Moore–Penrose step is finite and zero along the missing direction, so the iteration stalls silently instead of blowing up, and the two symptoms are easy to mistake for each other. The fix is damping, which bounds the gain at 1/(2λ), and the diagnosis is $σ_min$, which is the only number that tells the two cases apart.

§L.4.5 — Consolidation checkpoint 4

(a) You should now be able to: (i) read $σ_min$ as the proximity to a singularity and 1/σ_min as the pseudoinverse’s gain; (ii) compute the DLS’s effective gain and its bound 1/(2λ) at a given λ; (iii) distinguish the exact-singularity behavior (finite, zero, silent stall) from the near-singular one (finite but huge) and from the (JᵀJ)⁻¹ spelling (raises).

(b) Transfer probe. A 2-link arm is at q = (π/4, π/2), where $σ_min$ ≈ 0.309, and the DLS runs with λ = 0.1. A colleague halves λ three times to speed convergence and reports that each halving roughly doubles the gain. Is the claim right, and what does it miss? Answer sketch: at $σ_min$ = 0.309 ≫ λ, the DLS gain is already within 9.5% of 1/σ_min — 2.929 against 3.236 — so halving λ changes almost nothing; the reported doubling would require the arm to be in the regime σ ≪ λ, which it is not. The claim is a statement about the wrong regime, and it is the usual one: the damping’s effect scales with the condition number, so at a well-conditioned configuration the DLS is the pseudoinverse and the dial does nothing. The number to check before tuning is σ_min/λ; only when it is small does the gain move, and then it moves as σ/(σ²+λ²) rather than as a constant factor.

(c) Remediation rule. If you cannot do (a)(iii) 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
The IK returns a different branch than expected(1) The seed is in a different basin; (2) the target is near a branch boundary; (3) the arm’s geometry has more branches than expectedNot ‖q* − $q_seed$‖ — the basin is not a joint-space ball, and the nearer branch is the wrong one on 106 of 400 random seeds (§WE-L.3.c). Check the sign of q₂ and the branch’s joint-limit validityValidate the branch against the limits; use the current q as the seed
The IK diverges near a target(1) Undamped J⁺ at a singularity; (2) the target is unreachable; (3) the step is too largeCheck $σ_min(J$); if small, it is (1)Use the DLS
The IK stalls at a non-zero error(1) A singular configuration reached from this seed; (2) a genuine local minimum (J full rank); (3) the target is outside the reachable set‖Jᵀe‖ separates “stationary” from “still moving”; $σ_min(J$) then separates (1) from (2) — rank-deficient J means the residual is unreachable from here. Measured: $σ_min$ = 5·10⁻¹¹, rank 5/6 (§L.6.6)Multiple restarts from a different seed; DLS alone does not escape
The IK crashes with a NaN(1) arccos of an out-of-range value; (2) (JᵀJ)⁻¹ of a singular matrix; (3) division by $σ_min$ = 0Check the operation; the arccos is (1), the inverse is (2)Saturate the target; DLS
The IK returns a pose far from the target(1) The kinematic model is wrong (the DH table or the PoE); (2) the Jacobian is wrong (missing moment arm); (3) the convergence criterion is too looseCompare the returned T(q*) to $T_target$; if the norm is large, the model is wrongCheck the DH/PoE cross-check
The IK returns a solution outside the joint limits(1) The IK does not filter the limits; (2) the target is only reachable outside the limits; (3) the branch choice ignores the limitsCheck q* against the limits; if outside, it is (1)Filter the solutions; multi-restart with limit-checked seeds

§L.6 — The lab

§L.6.1 — Task

Implement the analytical IK for the 2-link arm, the numerical IK (Newton–Raphson with DLS) for the 6-DOF arm, and verify the convergence, the branch behavior, and the singularity handling; then break the pipeline three ways.

§L.6.2 — Conditions

Python 3.10+, PyTorch 2.2+, CPU. Seed 42, dtype = float64. Runtime under 3 minutes. The 2-link arm’s targets: (0.6, 0.3) and (0.0, 0.5). The 6-DOF arm’s target: a reachable pose from the running case’s q = [0.3, 0.5, −0.2, 0.1, 0.4, −0.3] (the target is $T(q_target$) for a known $q_target$, so the solution is known).

§L.6.3 — Controls held fixed

The seed, dtype, the arm’s parameters, the target poses, and the iteration’s parameters (max_iter = 100, tol = 1e-6, λ = 0.1) are fixed.

§L.6.4 — Metrics table

MetricDefinitionHow measured
ik_2link_error‖$p_ee(q*$) − $p_target$‖ for the analytical 2-linkThe closed-form’s output
ik_nr_convergedWhether the NR iteration convergesThe error’s final value vs. the tolerance
ik_nr_iterationsThe number of iterations to convergeThe iteration counter
ik_dls_errorThe DLS’s final pose errorThe error’s final value
branch_jump‖q⁺ − q⁻‖ — the joint-space distance between the two analytical branches at the same targetThe difference between the two branches
singularity_ratio‖J⁺e‖ / ‖Jᵀ(JJᵀ + λ²I)⁻¹e‖ just off the singularityThe q₂ sweep

§L.6.5 — Pre-registered hypotheses

  • H1. The 2-link analytical IK’s ik_2link_error < 1e-10 (the closed form is exact). The prediction is the bound.
  • H2. The DLS IK for the 6-DOF arm converges from a near seed (‖$q_seed$ − $q_target$‖ < 0.5), with converged = True and ‖$q_seed$ − $q_target$‖ < 0.5 — the seed’s premise and the convergence together. The iteration count is reported, not gated: at λ = 0.1 the damped step is small enough that convergence takes ~56 iterations, so a < 20 bound would be a threshold chosen to fail. The prediction is the pair.
  • H3. The damping’s systematic error shrinks with λ: at λ = 0.1 the converged position error is < 1e-4, and lowering λ to 0.01 lowers it by roughly two orders of magnitude. The prediction is the pair (both < 1e-4, the smaller λ strictly smaller).
  • H4. The branch_jump — ‖q⁺ − q⁻‖ between the two analytical branches at (0.5, 0.5) — is > 1.0 (a full branch switch). The prediction is the bound.
  • H5. Near the singularity, the undamped step’s norm exceeds the damped step’s by more than 100×: at q = (0, 1e-3) the pseudoinverse step is 4472 while the damped step is 0.022. Exactly at the singularity both are zero — the requested direction is in the null space and neither method can move there — so the prediction is stated for the near-singular regime, which is where the trade-off lives. The prediction is the ratio.

§L.6.6 — The break-it sequence

  1. Undamped pseudoinverse at a singularity (silent). Use the undamped J⁺ at q = (0, 0). Predicted symptom: no exception. torch.linalg.pinv truncates the zero singular value and returns a finite matrix, so the bug is not the crash but the enormous step it produces just off the singularity — 4472 at q₂ = 1e-3 against the damped 0.022. Actual: silent.

  2. Single-branch assumption (silent). Return only the + branch for the 2-link arm, ignoring the −. Predicted symptom: the + branch is valid for some targets (the ones where the − is outside the joint limits), and invalid for others (where the + is outside). Actual: as predicted — silent (the returned branch is a valid solution for that target, just not the closest one).

  3. Far seed (silent). Seed the DLS with q = (2.5, -1.0, 0.5, -0.5, 1.0, -1.0), far from $q_target$. Predicted symptom: no exception and no error message — the loop returns whatever it has when max_iter runs out, and the caller sees a pose that is close but wrong, or a joint vector that differs from $q_target$. Actual: silent, and worse than the prediction, in a way that changes what the test must measure. The fixed seed converges to the same configuration: ‖$q_ik$ − $q_target$‖ = 6.2832, which is exactly 2π, so the raw joint-space distance reports a jump where the configuration is identical. Measured over 60 far seeds ($q_target$ + 1.2·randn): 16 converge to the same configuration (wrapped distance 0.0000), 16 converge to a different one (0.4813), and 28 stall at max_iter = 300 with a position error of 4.16e-02 — 4 cm on a 1.2 m arm, which passes a 1e-1 check and fails a 1e-6 one. So the joint-space distance must be measured modulo 2π, and the stall is the failure mode a pose test alone will miss.

    The stall is a singularity, not a local minimum — and the test that separates them is $σ_min$, not the residual. Every stalled seed above lands on the same configuration, with $σ_min(J$) = 5·10⁻¹¹, rank 5/6, and ‖Jᵀe‖ = 1.5·10⁻¹¹. That last number is the discriminator: at a true local minimum of the pose error, Jᵀe ≠ 0 and the residual points somewhere the arm can still move. Here Jᵀe = 0 to eleven digits while ‖e‖ = 0.0475, so the residual lies entirely in the Jacobian’s null space — the arm is at a singular configuration and the remaining error is in a direction no joint can produce. Iterating longer does not help: 2000 iterations leave the error at 4.16e-02. The lesson’s three failure modes are not three independent mechanisms; this one is mode 2 (singularity) wearing mode 3’s clothes, and ‖Jᵀe‖ is what tells you which you are looking at.

§L.6.7 — Acceptance criteria

CriterionThresholdArtefact it is tested on
H1’s ik_2link_error< 1e-10The analytical IK’s print
H2’s convergenceTrue with ‖$q_seed$ − $q_target$‖ < 0.5The DLS’s print
H3’s damping errorboth < 1e-4; λ = 0.01 strictly below λ = 0.1The λ sweep
H4’s branch jump> 1.0The branch comparison
H5’s undamped vs. damped stepundamped / damped > 100 near the singularityThe q2 sweep
Break-it #3’s stall diagnosis‖Jᵀe‖ < 1e-8 with $σ_min$ < 1e-8 on the stalled configThe stalled-config print
Three break-its predicted3/3 timestampedThe prediction file
The atan2 is used, not arctanThe code uses atan2The code review

§L.6.8 — What good looks like

A strong submission has five properties:

  1. The analytical 2-link IK is implemented with atan2, and the two branches are both returned (not just one).
  2. The NR iteration is implemented with the DLS, and the λ’s effect is demonstrated (the DLS’s systematic error vs. the NR’s exactness).
  3. The branch choice is validated against the joint limits, and the near-boundary case’s jump is shown.
  4. The near-singular blow-up is shown for the undamped step and the bounded damped step beside it, with the numerical values and the q₂ sweep that locates the regime.
  5. The break-its are predicted with the symptom (silent pseudoinverse truncation, silent branch switch, silent far-seed stall) stated before running, and the stall is diagnosed rather than labeled — ‖Jᵀe‖ for stationarity, $σ_min$ for which kind.

§L.6.9 — Runnable lab code

# lab_1_1_2.py — Analytical and numerical IK, branches, singularities, DLS
# Requires: Python 3.10+, PyTorch 2.2+

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

# --- Step 1: the 2-link analytical IK --------------------------------
def ik_2link(x, y, a1=0.5, a2=0.5):
    """Return both branches (q1, q2) for the 2-link planar arm."""
    r2 = x*x + y*y
    cos_q2 = (r2 - a1*a1 - a2*a2) / (2*a1*a2)
    if abs(cos_q2) > 1:
        return None  # unreachable
    q2_plus = math.acos(cos_q2)
    q2_minus = -q2_plus
    def q1_for(q2):
        return math.atan2(y, x) - math.atan2(a2*math.sin(q2), a1 + a2*math.cos(q2))
    return [(q1_for(q2_plus), q2_plus), (q1_for(q2_minus), q2_minus)]

def fk_2link(q, a1=0.5, a2=0.5):
    x = a1*math.cos(q[0]) + a2*math.cos(q[0]+q[1])
    y = a1*math.sin(q[0]) + a2*math.sin(q[0]+q[1])
    return x, y

# Test on the WE-L.2.a target (0.6, 0.3)
target = (0.6, 0.3)
branches = ik_2link(*target)
ik_2link_err = 0.0
for i, q in enumerate(branches):
    x, y = fk_2link(q)
    err = math.sqrt((x - target[0])**2 + (y - target[1])**2)
    ik_2link_err = max(ik_2link_err, err)
    print(f"branch {i}: q = ({q[0]:.4f}, {q[1]:.4f}), fk = ({x:.4f}, {y:.4f}), err = {err:.2e}")

# Test on the boundary (0.0, 0.0) — fully folded
print$f"\ntarget (0,0): {ik_2link(0, 0)}"$
# Test on unreachable (1.5, 0.0)
print(f"target (1.5, 0): {ik_2link(1.5, 0)}")

# --- Step 2: numerical IK with DLS for the 6-DOF arm ----------------
def fk_6(q):
    """PoE FK (from 0.2.3) for the running case."""
    a = 0.2
    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),
    ]
    T = torch.eye(4, dtype=dtype, device=device)
    for xi, qi in zip(twists, q):
        w, v = xi[:3], xi[3:]
        th = qi
        W = torch.tensor([[0, -w[2], w[1]], [w[2], 0, -w[0]], [-w[1], w[0], 0]], dtype=dtype, device=device)
        R = torch.eye(3, dtype=dtype, device=device) + math.sin(th)*W + (1-math.cos(th))*(W@W)
        J = th*torch.eye(3, dtype=dtype, device=device) + (1-math.cos(th))*W + (th-math.sin(th))*(W@W)
        $T_i$ = torch.eye(4, dtype=dtype, device=device)
        $T_i$[:3, :3] = R; $T_i$[:3, 3] = J @ v
        T = T @ $T_i$
    $T_end$ = torch.eye(4, dtype=dtype, device=device)
    $T_end$[:3, 3] = torch.tensor([6*a, 0., 0.], dtype=dtype, device=device)
    return T @ $T_end$

def jacobian_6(q):
    """Numerical 6x6 Jacobian via finite differences (for the lab)."""
    J = torch.zeros(6, 6, dtype=dtype, device=device)
    delta = 1e-6
    T0 = fk_6(q)
    for i in range(6):
        $q_p$ = q.clone(); $q_p[i$] += delta
        $T_p$ = $fk_6(q_p$)
        J[:3, i] = ($T_p$[:3, 3] - T0[:3, 3]) / delta
        dR = ($T_p$[:3, :3] - T0[:3, :3]) @ T0[:3, :3].T / delta
        J[3:, i] = torch.tensor([dR[2, 1], dR[0, 2], dR[1, 0]], dtype=dtype, device=device)
    return J

def numerical_ik(target_T, $q_seed$, lam=0.1, max_iter=100, tol=1e-6):
    q = $q_seed$.clone()
    for k in range(max_iter):
        T = fk_6(q)
        # Position error
        $e_p$ = target_T[:3, 3] - T[:3, 3]
        # Orientation error (log of $R_err$)
        $R_err$ = T[:3, :3].T @ target_T[:3, :3]
        # Simplified: use the skew-symmetric part
        $e_o$ = torch.$tensor([R_err[2$, 1] - $R_err[1$, 2], $R_err[0$, 2] - $R_err[2$, 0], $R_err[1$, 0] - $R_err[0$, 1]], dtype=dtype) / 2
        e = torch.$cat([e_p$, $e_o$])
        if e.norm() < tol:
            return q, k, True
        J = jacobian_6(q)
        # DLS update
        JJT = J @ J.T
        delta_q = J.T @ torch.linalg.solve(JJT + lam**2 * torch.eye(6, dtype=dtype), e)
        q = q + delta_q
    return q, max_iter, False

# Target from a known q
$q_target$ = torch.tensor([0.3, 0.5, -0.2, 0.1, 0.4, -0.3], dtype=dtype, device=device)
$T_target$ = $fk_6(q_target$)
# H2's premise is $||q_seed$ - $q_target||$ < 0.5, so the perturbation must be
# small enough to satisfy it; 0.3*randn gives ~0.98 and tests a different claim.
$q_seed$ = $q_target$ + 0.1 * torch.randn(6, dtype=dtype)
$q_ik$, iters, converged = $numerical_ik(T_target$, $q_seed$)
err = ($fk_6(q_ik$)[:3, 3] - $T_target$[:3, 3]).norm().item()
print$f"\nDLS IK: converged={converged}, iters={iters}, pos_error={err:.2e}"$
print(f"  $q_ik$ = {q_ik.tolist()}")
print(f"  $q_target$ = {q_target.tolist()}")
print(f"  ‖$q_ik$ − $q_target$‖ = {(q_ik - $q_target$).norm().item():.4f}")

# H3: the damping's error is O(lambda^2), so a smaller lambda converges tighter.
err_by_lam = {}
for lam in (0.1, 0.01):
    $q_l$, it_l, $c_l$ = $numerical_ik(T_target$, $q_seed$.clone(), lam=lam)
    err_by_lam[lam] = ($fk_6(q_l$)[:3, 3] - $T_target$[:3, 3]).norm().item()
    print(f"  lambda={lam}: $converged={c_l}$ in {it_l} iters, pos_error={err_by_lam[lam]:.2e}")

# --- Step 3: undamped pseudoinverse at singularity (SILENT) ----------
# torch.linalg.pinv does not raise at a singular J: it truncates the zero
# singular value and returns a finite matrix. The failure is the huge step
# just OFF the singularity, not a crash.
def jacobian_2link(q, a1=0.5, a2=0.5):
    J = torch.zeros(2, 2, dtype=dtype, device=device)
    J[0, 0] = -a1*math.sin(q[0]) - a2*math.sin(q[0]+q[1])
    J[0, 1] = -a2*math.sin(q[0]+q[1])
    J[1, 0] = a1*math.cos(q[0]) + a2*math.cos(q[0]+q[1])
    J[1, 1] = a2*math.cos(q[0]+q[1])
    return J

q0 = torch.tensor([0., 0.], dtype=dtype, device=device)
J0 = jacobian_2link(q0)
det_J0 = torch.linalg.det(J0).item()
print(f"\n[break-it #1] det(J) at q=(0,0): {det_J0:.2e} (singular, but pinv will NOT raise)")

# --- Step 4: branch jump, 2-link (silent) ---------------------------
# Not a far seed: this is the single-branch trap. Both branches are valid
# solutions for the same target, so a solver that returns one is not wrong —
# it just may not be the one the joint limits allow.
target_boundary = (0.5, 0.5)
branches_b = ik_2link(*target_boundary)
print(f"\n[break-it #2] branches at (0.5, 0.5): {branches_b}")
if branches_b:
    jump = math.sqrt((branches_b[0][0] - branches_b[1][0])**2 + (branches_b[0][1] - branches_b[1][1])**2)
    print(f"  branch jump: {jump:.4f} (predicted > 1.0 for a full branch switch)")

# --- Step 5: undamped vs. damped step NEAR the singularity ---------
# Exactly at q=(0,0) the requested direction lies in the null space, and
# NEITHER step moves: pinv returns zero and the damped solve returns zero
# too. The damping's benefit appears just OFF the singularity, where the
# undamped step blows up and the damped one stays bounded. That is the
# regime the lesson is about, so measure there.
$e_sing$ = torch.tensor([1.0, 0.0], dtype=dtype, device=device)   # +x is the weak direction
print(f"\n[break-it #1] det(J) at q=(0,0) = {det_J0:.2e}; pinv = {torch.linalg.pinv(J0).tolist()}")
print("q2        sigma_min   |dq_undamped|   |dq_damped|")
for q2 in (1e-4, 1e-3, 1e-2, 1e-1):
    Jq = jacobian_2link(torch.tensor([0., q2], dtype=dtype, device=device))
    sv = torch.linalg.svdvals(Jq).min().item()
    du = (torch.linalg.pinv(Jq) @ $e_sing$).norm().item()
    dd = (Jq.T @ torch.linalg.solve(Jq @ Jq.T + 0.1**2 * torch.eye(2, dtype=dtype, device=device),
                                    $e_sing$)).norm().item()
    print(f"{q2:8.4f}  {sv:.3e}   {du:12.4f}   {dd:10.4f}")
$J_near$ = jacobian_2link(torch.tensor([0., 1e-3], dtype=dtype, device=device))
dq_undamped = torch.linalg.$pinv(J_near$) @ $e_sing$
dq_damped = $J_near$.T @ torch.linalg.solve(
    $J_near$ @ $J_near$.T + 0.1**2 * torch.eye(2, dtype=dtype, device=device), $e_sing$)

# --- Step 6: far seed (silent) --------------------------------------
# A far seed does NOT raise: the loop simply runs to max_iter and returns
# whatever it has. Two distinct silent failures come out of it, and they need
# different tests.
#   (a) a 2*pi-wrapped joint vector, where the CONFIGURATION is identical and
#       only the raw distance looks like a branch jump;
#   (b) a stall, where the pose is close but not converged.
# So measure the distance modulo 2*pi, and check `converged` as well.
def wrap_dist(dq):
    """Joint-space distance modulo 2*pi — the physically meaningful one."""
    return (torch.remainder(dq + math.pi, 2 * math.pi) - math.pi).norm().item()

$q_far$ = torch.tensor([2.5, -1.0, 0.5, -0.5, 1.0, -1.0], dtype=dtype, device=device)
$q_ik_far$, it_far, conv_far = $numerical_ik(T_target$, $q_far$, max_iter=300)
far_pos_err = ($fk_6(q_ik_far$)[:3, 3] - $T_target$[:3, 3]).norm().item()
print(f"\n[break-it #3] fixed far seed: converged={conv_far} in {it_far} iters, "
      f"pos_error={far_pos_err:.2e}")
print(f"[break-it #3] raw $||q_ik$ - $q_target||$ = {(q_ik_far - $q_target$).norm().item():.4f}"
      f"  wrapped = {wrap_dist(q_ik_far - $q_target$):.4f}"
      f"   (near-seed run: raw {(q_ik - $q_target$).norm().item():.4f})")

# Sweep the seed to see all three outcomes. The counts below are the finding:
# a far seed splits into same-configuration, different-configuration, and stall.
same = other = stalled = 0
for s in range(60):
    torch.manual_seed(s)
    $q_s$ = $q_target$ + 1.2 * torch.randn(6, dtype=dtype)
    $q_s_ik$, it_s, conv_s = $numerical_ik(T_target$, $q_s$, max_iter=300)
    if not conv_s:
        stalled += 1
    elif $wrap_dist(q_s_ik$ - $q_target$) < 1e-3:
        same += 1
    else:
        other += 1
print(f"[break-it #3] 60 far seeds: same-config={same}  other-config={other}  "
      f"stalled={stalled}  (predicted: all converge to a different branch)")

# The stalled seeds all land on the same configuration. Is that a local minimum
# of the pose error, or a singularity? The residual alone cannot tell you; the
# discriminator is ||J^T e||, which is zero at BOTH. What separates them is
# sigma_min: a rank-deficient J means the residual is unreachable.
torch.manual_seed(0)
$q_stall$, _, conv_stall = $numerical_ik(T_target$, $q_target$ + 1.2*torch.randn(6, dtype=dtype),
                                      max_iter=300)
$T_s$ = $fk_6(q_stall$)
$e_p_s$ = $T_target$[:3, 3] - $T_s$[:3, 3]
$R_s$ = $T_s$[:3, :3].T @ $T_target$[:3, :3]
$e_o_s$ = torch.$tensor([R_s[2$, 1] - $R_s[1$, 2], $R_s[0$, 2] - $R_s[2$, 0],
                      $R_s[1$, 0] - $R_s[0$, 1]], dtype=dtype) / 2
$e_s$ = torch.$cat([e_p_s$, $e_o_s$])
$J_s$ = $jacobian_6(q_stall$)
sv_s = torch.linalg.$svdvals(J_s$)
rank_s = int((sv_s > 1e-8 * sv_s.max()).sum())
print(f"[break-it #3] stalled config: ||e||={e_s.norm().item():.4f}  "
      f"||J^T e||={(J_s.T @ $e_s$).norm().item():.2e}  "
      f"sigma_min={sv_s.min().item():.2e}  rank={rank_s}/6")

# --- Step 7: summary ------------------------------------------------
print("\n--- lab 1.1.2 summary ---")
print(f"H1: 2-link analytical error = {ik_2link_err:.2e} (predicted < 1e-10)")
print(f"H2: DLS IK converged = {converged}, iters = {iters}, "
      f"$||q_seed-q_target||$ = {(q_seed - $q_target$).norm().item():.3f} (predicted converged, < 0.5)")
print(f"H3: DLS pos error at lambda=0.1 = {err_by_lam[0.1]:.2e}, "
      f"at lambda=0.01 = {err_by_lam[0.01]:.2e} (predicted both < 1e-4, the smaller strictly smaller)")
print(f"H4: branch jump at (0.5, 0.5) = {jump if branches_b else 'N/A'}")
print(f"H5: near the singularity (q2 = 1e-3) the undamped step is "
      f"{dq_undamped.norm().item():.1f} and the damped step is {dq_damped.norm().item():.3f} "
      f"(predicted: undamped >> damped, i.e. a ratio > 100)")

§L.7 — The cheat sheet

Term or resultStatement or definitionRole
Inverse kinematicsq* = f⁻¹($T_target$); solve for joint angles from poseIntroduced here; consumed by 1.1.3 (control), 2.3.2 (planning)
WorkspaceThe set of reachable poses; the annulus `[a₁−a₂
BranchOne of the multiple solutions; up to 8 for 6-DOF spherical, up to 16 for generalIntroduced here; consumed by the planner’s selection
Pieper’s condition3 consecutive axes intersect (spherical wrist) or parallel → closed formIntroduced here; consumed by 2.3.2 (arm selection)
2-link closed formcos q₂ = (r²−a₁²−a₂²)/(2a₁a₂), q₁ = atan2(y,x) − atan2(a₂ sin q₂, a₁+a₂ cos q₂)Introduced here; consumed by 2.3.2
atan2(y, x)The polar angle in (−π, π] with the correct quadrantIntroduced here; consumed by every IK
Newton–Raphson IK$q_{k+1}$ = $q_k$ + J⁺ $e_k$, iterate until ‖e‖ < εIntroduced here; consumed by 1.1.3
Pose errorlog(T(q)^{-1} $T_target$) or [$p_target$ − p; log(R(q)ᵀ $R_target$)]Introduced here; consumed by 1.1.3
Pseudoinverse J⁺J⁻¹ (square), (JᵀJ)⁻¹Jᵀ (tall), Jᵀ(JJᵀ)⁻¹ (wide)Introduced here; consumed by 1.1.3
Singularity$σ_min(J$) = 0 (rank drop); the pseudoinverse blows upIntroduced in 1.1.1 as rank(J) < n; restated here in singular-value form, which is what the DLS damping needs
Manipulabilityw(q) = √det(JJᵀ) = ∏ $σ_i$; zero at singularityIntroduced here; consumed by 2.3.2 (path planning)
DLSJᵀ(JJᵀ + λ²I)⁻¹; bounds the inverse at 1/(2λ)Introduced here; consumed by 1.1.3 (robust control)
Damping trade-offλ small → fast/unstable; λ large → slow/stable; error O(λ²)Introduced here; consumed by 1.1.3
Null-space projectionI − J⁺J; secondary objectives for redundant armsIntroduced here; consumed by 2.3.2 (redundant planning)
Local minimumThe error stalls at a non-zero value; the iteration doesn’t convergeIntroduced here; consumed by 2.3.2 (multi-restart). Caution: a stall is more often a singularity than a local minimum — check $σ_min$ before believing this label
Design principle P14.1IK is a relation, not a function; the branch is the planner’s decisionIntroduced here; consumed by 2.3.2
Design principle P14.2atan2, not arctanIntroduced here; consumed by every IK
Design principle P14.3The pseudoinverse is a step, not a solution; iterateIntroduced here; consumed by 1.1.3
Design principle P14.4The DLS is the bounded pseudoinverse; λ’s choice is the trade-offIntroduced here; consumed by 1.1.3
Design principle P14.5Validate the branch against the joint limits and the continuityIntroduced here; consumed by 2.3.2
Design principle P14.6The method is chosen by the arm and the deadlineIntroduced here; consumed by 2.3.2 (arm selection)
Design principle P14.7A closed form is only as correct as its domainIntroduced here; consumed by every closed-form IK
Design principle P14.8Read the shape of J before choosing the formulaIntroduced here; consumed by 1.1.3
Design principle P14.9Damping trades a bounded error for an unbounded oneIntroduced here; consumed by 1.1.3 (robust control)

§L.8 — Exercises with solutions

E 1.1.2.1 — The 2-link IK (numerical). a₁ = a₂ = 0.4, target (0.5, 0.2). Compute both branches.

S 1.1.2.1. r² = 0.25 + 0.04 = 0.29. cos q₂ = (0.29 − 0.16 − 0.16)/(2·0.4·0.4) = (-0.03)/0.32 = -0.094. q₂ = ± arccos(-0.094) ≈ ± 1.665 rad ≈ ± 95.4°. For q₂ = 1.665: a₂ sin q₂ ≈ 0.4·0.996 ≈ 0.398; a₁ + a₂ cos q₂ ≈ 0.4 − 0.0375 ≈ 0.362; atan2(0.398, 0.362) ≈ 0.833; atan2(0.2, 0.5) ≈ 0.381; q₁ ≈ -0.452. For q₂ = -1.665: q₁ ≈ 0.381 − (−0.833) = 1.214. Solutions: q ≈ (-0.452, 1.665) and (1.214, -1.665). The lesson: the closed form gives the two branches, and the choice is the planner’s.

E 1.1.2.2 — The workspace test (diagnostic). Target (1.0, 0) for a₁ = a₂ = 0.4. Is it reachable?

S 1.1.2.2. r = 1.0 > a₁ + a₂ = 0.8. Unreachable — the arm’s max reach is 0.8, and 1.0 is beyond. The arccos((1.0 − 0.16 − 0.16)/0.32) = arccos(2.125) is undefined (NaN). The controller must saturate to (0.8, 0). The lesson: the arccos’s domain [-1, 1] is the reachability check.

E 1.1.2.3 — The Jacobian step (numerical). 2-link (a₁ = a₂ = 0.5) at q = (π/4, π/2), e = (0.1, 0.2). Compute $J_v$ and $J_v$⁺e.

S 1.1.2.3. $J_v[0$, 0] = -0.5 sin(π/4) − 0.5 sin(3π/4) = -0.354 − 0.354 = -0.707. $J_v[0$, 1] = -0.5 sin(3π/4) = -0.354. $J_v[1$, 0] = 0.5 cos(π/4) + 0.5 cos(3π/4) = 0.354 − 0.354 = 0. $J_v[1$, 1] = 0.5 cos(3π/4) = -0.354. det = (-0.707)(-0.354) − (-0.354)(0) = 0.25. $J_v$⁻¹ = (1/0.25) [[-0.354, 0.354], [0, -0.707]] = [[-1.414, 1.414], [0, -2.828]]. $J_v$⁺ e = $J_v$⁻¹ · (0.1, 0.2) = (-0.141 + 0.283, 0 - 0.566) = (0.141, -0.566). The lesson: the step is finite at the well-conditioned configuration, and the direction is the descent direction of the error.

E 1.1.2.4 — The manipulability (numerical). 2-link (a₁ = a₂ = 0.5) at q₂ = π/3. Compute the manipulability.

S 1.1.2.4. w = a₁ a₂ |sin q₂| = 0.5·0.5·sin(π/3) = 0.25·0.866 = 0.2165. The lesson: the manipulability is proportional to |sin q₂|, and it is maximal at π/2 (w = 0.25) and zero at 0 and π (the boundaries).

E 1.1.2.5 — The DLS’s bound (numerical). At $σ_min$ = 0.01, λ = 0.1, what is the effective inverse σ_min/(σ_min² + λ²)?

S 1.1.2.5. 0.01/(0.0001 + 0.01) = 0.01/0.0101 ≈ 0.990. The effective inverse is 0.99 vs. the pseudoinverse’s 1/0.01 = 100 — a 100× reduction. The lesson: the DLS’s bound 1/(2λ) = 5 caps the amplification, and at $σ_min$ ≪ λ the effective inverse is ≈ σ_min/λ² ≈ 0.01/0.01 = 1, which is the bounded value.

E 1.1.2.6 — The branch jump (diagnostic). A learner’s IK returns q = (0.5, 0.3) for target A, and q = (2.1, −1.4) for a target B very close to A. What is the likely cause?

S 1.1.2.6. The IK jumped branches — the two targets are close in the task space (they are near a branch boundary), and the IK’s single-branch assumption (or the iteration’s basin) returned the + branch for A and the − branch for B. ‖$q_A$ − $q_B$‖ = √(1.6² + 1.7²) = 2.33, even though the task-space distance is small — the motion the IK commands is huge, and the arm jumps across the workspace. The fix is to seed the IK with the previous q (continuity), and to validate the returned q against the joint limits and the motion constraints. The lesson: the IK’s pose test passes for both, but the trajectory test fails — the branch choice is the planner’s.

E 1.1.2.7 — The analytical vs. numerical (diagnostic). A 6-DOF arm with a spherical wrist has a closed-form IK, but a learner uses the numerical IK and gets slower convergence and occasional local minima. What is the cause?

S 1.1.2.7. The spherical wrist satisfies Pieper’s condition, so the closed form exists and is exact and fast. The numerical IK ignores this structure — it treats the arm as general, uses the iterative method, and pays the cost (slower, local minima). The fix is to use the closed form (the analytical IK) for the spherical-wrist arm — the structure is the advantage, and the numerical method discards it. The lesson: the choice of method depends on the arm’s structure — the closed form for Pieper, the numerical for general.

E 1.1.2.8 — The null-space (constructive, transfer probe). A 7-DOF redundant arm has the primary task (end-effector pose) and a secondary task (joint-limit avoidance). Write the combined update.

S 1.1.2.8.

delta_q = J.T @ inv(J @ J.T + lambda^2 * I) @ e    # primary
$q_secondary$ = -grad_g(q)                           # e.g., -2(q - $q_center$)
null_proj = I - pinv(J) @ J
delta_q_secondary = null_proj @ $q_secondary$
$q_next$ = q + delta_q + alpha * delta_q_secondary

The primary step is the DLS’s Jᵀ(JJᵀ + λ²I)⁻¹ e, and the secondary step is the null-space projection (I − J⁺J) q̇_secondary, which does not affect the end-effector’s pose. The α is the secondary gain. The lesson: the redundancy is used for the secondary objective without sacrificing the primary, and the null-space projection is the mechanism.


§L.9 — Exit test

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

  1. State the IK problem and the three questions (existence, uniqueness, reachability).
  2. State the 2-link arm’s closed-form IK and both branches.
  3. State the workspace annulus and the arccos’s domain.
  4. State why atan2 is required and arctan is insufficient.
  5. State the Newton–Raphson iteration and the convergence criterion.
  6. State the three failure modes (far seed, singularity, stationary residual) and the test that separates the last two.
  7. State the pseudoinverse’s behavior at singularities (1/σ_min unbounded).
  8. State the DLS update and the trade-off (speed vs. stability, O(λ²) error).
  9. State the manipulability w = ∏ $σ_i$ and the 2-link arm’s w = a₁a₂|sin q₂|.
  10. State the null-space projection and its use for redundant arms.
  11. State Pieper’s condition and why the running case lacks the closed form.
  12. Diagnose the branch jump, the singularity divergence, the stationary residual, and the arctan quadrant bug from their symptoms.

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


§L.10 — Provenance and evidence tiers

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

ClaimTierStatusWhere it is reproduced
The 2-link closed-form IK is exact: error 1.24e-16ARH1
Damped least squares converges from a near seed: True in 56 iterationsARH2, with the iteration count reported, not gated
Lowering λ from 0.1 to 0.01 lowers the residual from 9.02e-7 to 5.15e-9ARH3, both residuals printed
The analytical branch switch at (0.5, 0.5) is 3.512 radARH4
Near the singularity (q₂ = 1e-3) the undamped step is 4472.1 against the damped 0.022, a ratio over 10⁵ — the damping bounds the step where the pseudoinverse does notARH5’s paired print
From 60 far seeds, 16 reach the same configuration, 16 a different one, and 28 stallARBreak-it #3’s three-way print
A stalled solve is a rank deficiency, not a local minimum: $σ_min$ = 4.89e-11, rank 5/6ARBreak-it #3’s diagnostic line

What this lesson does not reproduce. The claim that exactly at the singularity both the damped and undamped steps are zero — because the requested direction lies in the null space — is stated in H5 and is not printed by the lab, which evaluates near the singularity (q₂ = 1e-3). It is tier B, status S: a limiting argument the reader can check by setting q₂ = 0 and observing that the column collapses, but not a number the shipped run produces. The distinction matters, because “near” and “at” differ by a factor of 10⁵ in the step norm, which is the whole point of the hypothesis.

The 28/60 stall result is a finding about the solver, not about the arm. A stalled run has ‖Jᵀe‖ = 1.49e-11 and $σ_min$ = 4.89e-11, so the gradient is zero because the Jacobian has lost a rank direction — not because the pose is unreachable. A reader who diagnoses a stall as “bad seed” will retry the same seed forever; the diagnostic that separates the two is the singular value, which the lab prints.

No external sources are cited in this lesson. The closed form is derived from the law of cosines in §L.1; damped least squares is derived in §L.3 from the regularized normal equations. No paper is named, and no citation is implied by the names “Levenberg” or “Marquardt,” which this lesson does not use.