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

Redundancy and the Null Space

With more joints than task dimensions there are infinitely many answers. Choose among them on purpose.

Concept  ·  about 35 minutes

A seven-joint arm positioning a hand in six-dimensional pose space has one degree of redundancy: a continuum of joint configurations produce the same hand pose. Your elbow can swing while your hand stays put — try it. That freedom is not a nuisance; it is capacity for a second objective.

qdot = J^+ v + (I - J^+ J) z ^ achieves the task ^ projects z into the null space: motion that changes joints without moving the hand

The projector `(I - J^+ J)` annihilates any component of `z` that would disturb the task. So you can pick `z` to be the gradient of any secondary objective and pursue it for free — stay away from joint limits, maximize manipulability so you do not drift toward a singularity, avoid an obstacle, or minimize energy.

Task plus posture, in one command
import numpy as np

def redundant_step(J, error, secondary_gradient):
    J_pinv = np.linalg.pinv(J)
    null_projector = np.eye(J.shape[1]) - J_pinv @ J
    return J_pinv @ error + null_projector @ secondary_gradient

def away_from_limits(q, q_min, q_max):
    """A secondary objective: push each joint toward the middle of its range."""
    middle = (q_min + q_max) / 2
    return -(q - middle) / (q_max - q_min) ** 2
The mirror worth sitting with

Human motor control faces exactly this problem, and Bernstein named it in the 1960s: the body has vastly more degrees of freedom than any task requires, yet movement is stereotyped and smooth rather than arbitrary. The nervous system is choosing within a null space too, and the criterion it uses — smoothness, effort, variance minimization — is still argued about. The engineering solution and the biological question are the same question.

There is also a machine-learning mirror. An overparameterized network has many parameter settings that fit the training data equally well; which one optimization lands on is determined by implicit biases in the algorithm rather than by the loss. Redundancy resolved by a secondary criterion is a pattern you have now seen in three places.


Hold on to

  • Redundancy means a continuum of solutions, not an ambiguity to eliminate
  • Null-space projection pursues a secondary objective without disturbing the task
  • Bernstein's degrees-of-freedom problem is the same problem in biology

Work through

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

  1. Build a four-link planar arm (two redundant degrees of freedom) and hold the hand fixed while the elbow moves through the null space.
    Hint

    With 4 joints and a 2-D task, the null space is 2-dimensional.

    Solution

    Project a random vector through `(I - J^+ J)` and step along it: the joint angles change visibly while the end-effector position stays fixed to numerical precision. This is self-motion — the arm reconfiguring without doing anything the task can see. It is exactly what your elbow does while your hand stays on a doorknob.

    import numpy as np
    
    lengths = np.array([1.0, 0.8, 0.6, 0.4])
    q = np.array([0.3, 0.5, -0.4, 0.2])
    start = forward_kinematics(q, lengths)
    
    rng = np.random.default_rng(0)
    for _ in range(50):
        J = jacobian_analytic(q, lengths)
        null = np.eye(4) - np.linalg.pinv(J) @ J
        q = q + 0.05 * (null @ rng.normal(size=4))
    
    print("joint change:", np.abs(q - np.array([0.3, 0.5, -0.4, 0.2])).max())
    print("hand moved by:", np.linalg.norm(forward_kinematics(q, lengths) - start))
    Check your work

    Paste this after your own code. If it runs without raising, you have it.

    import numpy as np
    assert np.linalg.norm(forward_kinematics(q, lengths) - start) < 1e-6
    print("ok")
  2. Add a joint-limit-avoidance secondary objective and show the arm reaches the same targets with a better posture.
    Hint

    Use the `away_from_limits` gradient as the secondary objective z.

    Solution

    The arm reaches the same targets — the primary task is untouched, by construction — but finishes with joints nearer the middle of their ranges. Measure it: the maximum normalized distance from range centre drops substantially. This is free capability, paid for entirely out of redundancy you already had.

    import numpy as np
    
    q_min = np.full(4, -np.pi / 2)
    q_max = np.full(4, np.pi / 2)
    
    def solve(target, q0, use_secondary):
        q = np.array(q0, dtype=float)
        for _ in range(300):
            error = target - forward_kinematics(q, lengths)
            if np.linalg.norm(error) < 1e-4:
                break
            J = jacobian_analytic(q, lengths)
            z = away_from_limits(q, q_min, q_max) if use_secondary else np.zeros(4)
            q = q + 0.3 * redundant_step(J, error, z)
        return q
    
    for flag in (False, True):
        q = solve(np.array([1.5, 0.5]), [0.1, 0.1, 0.1, 0.1], flag)
        margin = np.abs(q - (q_min + q_max) / 2) / (q_max - q_min)
        print(f"secondary={flag}: worst limit proximity {margin.max():.3f}")


Sign in to track your progress through the lab.