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:
- Reach around obstacles
- Choose a comfortable posture
- Avoid awkward joint configurations
- Keep our elbow from bumping the table
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:
- J⁺Δx: Primary task (reach the target)
- (I − J⁺J): Null space projector — removes components that affect the primary task
- Δq_secondary: Gradient of a secondary objective function
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.
Why 7-DOF Arms Are Popular
This explains why many modern industrial arms (KUKA LBR, Franka Emika Panda) have 7 DOF instead of 6:
- 6 DOF: exactly enough for 3D position + orientation — no redundancy
- 7 DOF: one extra DOF in the null space — can avoid singularities, obstacles, and joint limits
- 8+ DOF: even more freedom, but harder to control and more expensive
The 7-DOF design is the “sweet spot” of redundancy for industrial manipulation.
Key Takeaways
- Redundancy (more DOF than task constraints) is a feature, not a bug
- The null space provides “free” joint motion that doesn’t affect the task
- Null space projection lets you optimize secondary objectives while solving IK
- Common objectives: joint limit avoidance, singularity avoidance, obstacle avoidance
- 7-DOF arms are popular because one redundant DOF provides significant practical benefits
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.