Kiru Lab  /  Embodied Control: Kinematics and the Inverse Jacobian  /  The Jacobian

Singularities and Conditioning

When the Jacobian loses rank, an entire direction of motion becomes unreachable and naive inversion explodes.

Concept  ·  about 30 minutes

A singularity is a configuration where the Jacobian loses rank — a direction of desired end-effector motion that no combination of joint velocities can produce. The classic case is a fully extended arm: at full stretch, no joint motion moves the hand further outward along the arm's axis.

Detecting a singularity with the SVD from track two
import numpy as np

def singularity_report(J, tol=1e-4):
    U, S, Vt = np.linalg.svd(J)
    manipulability = float(np.prod(S))          # volume of the achievable velocity ellipsoid
    condition = float(S[0] / S[-1]) if S[-1] > 0 else np.inf
    lost = [Vt[i] for i, s in enumerate(S) if s < tol]
    return {"singular_values": S, "manipulability": manipulability,
            "condition_number": condition, "degenerate_directions": lost}

Near a singularity the smallest singular value approaches zero, so the condition number blows up. Since the inverse scales by one over the singular values, the naive inverse demands enormous joint velocities to produce a tiny end-effector motion. On a physical robot that means violent, unsafe commands. This is precisely the ill-conditioning you saw in track two — same mathematics, physical consequences.

Structure dictates function

A singularity is a fact about the arm's geometry at that pose, not about the controller. No control algorithm can produce a motion the structure forbids. The structural fix is to design the workspace and the arm so that operations stay away from these configurations — which is a design decision, made long before any code is written.


Hold on to

  • A singularity is lost Jacobian rank: a direction that cannot be produced
  • Manipulability and condition number quantify how close you are
  • No controller can overcome a structural limitation of the geometry

Work through

Try each one before opening the solution. Getting it wrong first is most of where the learning happens.

  1. Plot the smallest singular value across the workspace of a two-link arm and locate the singular set.
    Hint

    Sweep a grid of joint angles, compute the SVD at each, and plot the smallest singular value.

    Solution

    For a two-link arm the singular set is where the arm is fully extended (q2 = 0) or fully folded (q2 = pi). At those configurations the two columns of the Jacobian become parallel, rank drops to 1, and the smallest singular value hits zero — a whole direction of hand motion becomes unreachable. Note that this depends only on q2: the singularity is a property of the arm's shape, not its orientation in the world.

    import numpy as np, matplotlib.pyplot as plt
    
    lengths = np.array([1.0, 1.0])
    q1 = np.linspace(-np.pi, np.pi, 200)
    q2 = np.linspace(-np.pi, np.pi, 200)
    smallest = np.zeros((200, 200))
    
    for i, a in enumerate(q1):
        for j, b in enumerate(q2):
            smallest[i, j] = np.linalg.svd(jacobian_analytic(np.array([a, b]), lengths),
                                           compute_uv=False)[-1]
    
    plt.imshow(smallest, extent=[-np.pi, np.pi, -np.pi, np.pi], origin="lower")
    plt.colorbar(label="smallest singular value"); plt.xlabel("q2"); plt.show()
  2. Command a straight-line motion through a singularity with a naive inverse and record the peak joint velocity.
    Hint

    Use the raw pseudoinverse with no damping and record max(abs(qdot)) at each step.

    Solution

    Peak joint velocity spikes by orders of magnitude as the path passes near the singular configuration — values of 1e3 to 1e6 are routine, depending on how close you pass. On real hardware that is a violent, potentially destructive command. This single measurement is why damped least squares is the production default rather than an optimization.

    import numpy as np
    
    lengths = np.array([1.0, 1.0])
    q = np.array([0.1, 0.2])
    peak = 0.0
    
    for target in np.linspace(1.5, 1.99, 200):          # march toward full extension
        error = np.array([target, 0.0]) - forward_kinematics(q, lengths)
        J = jacobian_analytic(q, lengths)
        qdot = np.linalg.pinv(J) @ error                # no damping
        peak = max(peak, float(np.abs(qdot).max()))
        q = q + 0.1 * qdot
    
    print("peak joint velocity:", peak)

Sign in to track your progress through the lab.