Week 03 | SKILL-021

Redundancy and Optimization: Getting More from Your Robot

Published: April 11, 2026 | Author: Smartotics Learning Journey | Reading Time: 11 min

A robot is redundant when it has more degrees of freedom than needed to perform its task. A 3-DOF planar arm reaching a 2D point has one extra DOF. A 7-DOF industrial arm positioning in 3D has one extra DOF. Far from being wasteful, this redundancy is a superpower — it lets the robot optimize for secondary objectives.

Why Redundancy Matters

Consider a human arm: we have 7 major DOF from shoulder to wrist, but reaching for a coffee cup only requires 6 (3 position + 3 orientation). That extra DOF lets us:

Robot arms with redundancy can do the same thing — but we need the right math to exploit it.

The Null Space

For a robot with n joints and m task constraints (where n > m), the Jacobian J is an m×n matrix. The null space of J is the set of joint velocities that produce zero end-effector motion:

$$\mathbf{N}(\mathbf{J}) = {\Delta\mathbf{q} : \mathbf{J}\Delta\mathbf{q} = \mathbf{0}}$$

Any joint motion in the null space doesn’t affect the task. We can use this freedom for secondary optimization!

Null Space Projection

The key formula is the projected gradient:

$$\Delta\mathbf{q} = \mathbf{J}^+\Delta\mathbf{x} + (\mathbf{I} - \mathbf{J}^+\mathbf{J}) \Delta\mathbf{q}_{secondary}$$

Where:

This is incredibly powerful. The robot solves the primary IK task and optimizes a secondary objective simultaneously.

Common Secondary Objectives

1. Joint Limit Avoidance

Keep joints centered in their range:

$$H(\mathbf{q}) = \sum_{i=1}^{n} \left(\frac{q_i - \bar{q}i}{q{i,max} - q_{i,min}}\right)^2$$

def joint_limit_gradient(q, q_min, q_max):
    """Gradient pushing joints toward the center of their range."""
    q_center = (q_min + q_max) / 2
    q_range = q_max - q_min
    return 2 * (q - q_center) / (q_range ** 2)

2. Singularity Avoidance

Maximize the manipulability measure:

$$w(\mathbf{q}) = \sqrt{\det(\mathbf{J}\mathbf{J}^T)}$$

Gradient pushes the robot away from configurations where det(JJ^T) → 0.

3. Obstacle Avoidance

Use a repulsive potential field:

$$H_{obs} = \sum_k \frac{1}{2} \eta \left(\frac{1}{d_k} - \frac{1}{d_0}\right)^2 \text{ if } d_k < d_0$$

Where d_k is distance to obstacle k and d_0 is the influence distance.

Complete Redundancy-Aware IK Solver

import numpy as np

def forward_kinematics_nlink(q, lengths):
    x, y, angle_sum = 0.0, 0.0, 0.0
    for i in range(len(q)):
        angle_sum += q[i]
        x += lengths[i] * np.cos(angle_sum)
        y += lengths[i] * np.sin(angle_sum)
    return np.array([x, y])

def jacobian_nlink(q, lengths):
    n = len(q)
    J = np.zeros((2, n))
    for i in range(n):
        jx, jy, a = 0.0, 0.0, 0.0
        for j in range(i):
            a += q[j]
            jx += lengths[j] * np.cos(a)
            jy += lengths[j] * np.sin(a)
        ee = forward_kinematics_nlink(q, lengths)
        J[0, i] = -(ee[1] - jy)
        J[1, i] =  (ee[0] - jx)
    return J

def ik_redundant(target, q_init, lengths,
                 q_min=None, q_max=None,
                 obstacle_pos=None, obstacle_radius=0.5,
                 max_iter=300, tol=1e-5, alpha_null=0.3):
    """
    Redundancy-aware IK solver with null space optimization.
    Optimizes for: joint limit avoidance + obstacle avoidance.
    """
    n = len(q_init)
    q = np.array(q_init, dtype=float)
    
    if q_min is None:
        q_min = np.full(n, -np.pi)
    if q_max is None:
        q_max = np.full(n, np.pi)
    
    for iteration in range(max_iter):
        ee = forward_kinematics_nlink(q, lengths)
        error = target - ee
        
        if np.linalg.norm(error) < tol:
            return q, True, iteration
        
        J = jacobian_nlink(q, lengths)
        J_pinv = np.linalg.pinv(J)
        
        # Primary task: reach the target
        dq_primary = J_pinv @ error
        
        # Null space projector
        N = np.eye(n) - J_pinv @ J
        
        # Secondary: joint limit avoidance
        dq_joints = joint_limit_gradient(q, q_min, q_max)
        
        # Secondary: obstacle avoidance (repulsive gradient)
        dq_obstacle = np.zeros(n)
        if obstacle_pos is not None:
            # Compute positions of each joint
            positions = []
            jx, jy, a = 0.0, 0.0, 0.0
            for i in range(n):
                a += q[i]
                jx += lengths[i] * np.cos(a)
                jy += lengths[i] * np.sin(a)
                positions.append(np.array([jx, jy]))
            
            # Compute gradient for obstacle avoidance
            influence_dist = obstacle_radius * 3
            for i in range(n):
                to_obs = np.array(obstacle_pos) - positions[i]
                dist = np.linalg.norm(to_obs)
                if 0.01 < dist < influence_dist:
                    # Repulsive force scaled by closeness
                    repulsion = 2.0 * (1/dist - 1/influence_dist) / (dist**2)
                    direction = -to_obs / dist
                    dq_obstacle[i] = repulsion * direction[0]  # Simplified
        
        # Combined secondary objective
        dq_secondary = dq_joints + 0.5 * dq_obstacle
        
        # Null space projection of secondary objective
        dq_null = N @ dq_secondary
        
        # Update with primary + null space contribution
        q = q + dq_primary + alpha_null * dq_null
        q = np.clip(q, q_min, q_max)
    
    return q, False, max_iter

# Example: 4-link arm (3 DOF redundant for 2D point reaching)
lengths = [1.5, 1.2, 1.0, 0.8]
q_init = [0.5, 0.3, -0.2, 0.1]
target = np.array([3.0, 2.0])
obstacle = [2.0, 1.5]  # Obstacle position

# Without obstacle awareness
q1, conv1, it1 = ik_redundant(target, q_init, lengths)
print(f"Without obstacle: {'Converged' if conv1 else 'Failed'} in {it1} iters")
print(f"  Angles: {[f'{np.degrees(a):.1f}°' for a in q1]}")

# With obstacle awareness
q2, conv2, it2 = ik_redundant(target, q_init, lengths, 
                                obstacle_pos=obstacle)
print(f"With obstacle: {'Converged' if conv2 else 'Failed'} in {it2} iters")
print(f"  Angles: {[f'{np.degrees(a):.1f}°' for a in q2]}")

Task Priority Framework

When you have multiple objectives (reach target AND avoid obstacle AND keep joints centered), you can stack them with priority levels:

$$\Delta\mathbf{q} = \mathbf{J}_1^+\Delta\mathbf{x}_1 + \mathbf{N}_1(\mathbf{J}_2^+\Delta\mathbf{x}_2 + \mathbf{N}_2(\mathbf{J}_3^+\Delta\mathbf{x}_3 + \cdots))$$

Higher-priority tasks are always satisfied first. Lower-priority tasks only use the remaining null space. This hierarchy is essential in practice — you always want the primary task (reaching the target) to take priority.

This explains why many modern industrial arms (KUKA LBR, Franka Emika Panda) have 7 DOF instead of 6:

The 7-DOF design is the “sweet spot” of redundancy for industrial manipulation.

Key Takeaways

Next Steps

Tomorrow we’ll look at real-world applications of inverse kinematics — from industrial pick-and-place to humanoid robot locomotion. We’ll see how the theory we’ve learned this week translates to actual robots working in factories, labs, and homes.