← Back to Blog
manipulationrobot-armpythonprogramming

Inverse Kinematics for 6-DOF Robot Arm: Theory + Python

Solve IK for 6-DOF robot arm — from DH parameters, forward kinematics to numerical IK solver in Python with NumPy.

Nguyen Anh Tuan26 tháng 1, 202610 min read
Inverse Kinematics for 6-DOF Robot Arm: Theory + Python

What is Inverse Kinematics and Why Does It Matter?

Inverse Kinematics (IK) for 6-DOF robot arms is a core robotics problem: given a desired end-effector position and orientation, find the corresponding joint angles. If you've ever programmed a robot to grasp an object, you know that humans think in Cartesian coordinates ("move the hand to point x, y, z"), but robots need individual joint angles. IK is the bridge between these two worlds.

This post covers the theory from Denavit-Hartenberg (DH) parameters, through forward kinematics, to a complete numerical IK solver in Python. The code is self-contained — requiring only NumPy and Matplotlib, no specialized robotics libraries.

6-DOF industrial robot arm in factory

Denavit-Hartenberg Convention

Why Do We Need DH Parameters?

Each robot arm has different joint and link configurations. The DH convention provides a standardized method to describe the geometric relationships between successive joints using exactly 4 parameters per joint:

Parameter Symbol Meaning
Link length a_i Distance between z_{i-1} and z_i axes along x_i axis
Link twist alpha_i Rotation angle from z_{i-1} to z_i around x_i axis
Link offset d_i Distance along z_{i-1} axis
Joint angle theta_i Rotation around z_{i-1} axis (variable for revolute joint)

DH Transformation Matrix

Each set of DH parameters produces a 4x4 homogeneous transformation matrix:

import numpy as np

def dh_matrix(theta, d, a, alpha):
    """
    Create DH transformation matrix 4x4.
    theta: joint angle (rad)
    d: link offset
    a: link length
    alpha: link twist (rad)
    """
    ct, st = np.cos(theta), np.sin(theta)
    ca, sa = np.cos(alpha), np.sin(alpha)

    return np.array([
        [ct, -st * ca,  st * sa, a * ct],
        [st,  ct * ca, -ct * sa, a * st],
        [0,   sa,       ca,      d     ],
        [0,   0,        0,       1     ]
    ])

This matrix encodes both rotation and translation. Multiplying 6 matrices together gives us forward kinematics — the end-effector position and orientation from joint angles.

Forward Kinematics for 6-DOF Robots

Example: Puma 560-Style Robot

Below is a sample DH parameters table for a 6-revolute-joint robot (units: meters, radians):

# DH Parameters for anthropomorphic 6-DOF robot
# Each row: [a, alpha, d, theta_offset]
# Actual theta = theta_variable + theta_offset
DH_PARAMS = [
    # a      alpha        d      theta_offset
    [0.0,    np.pi/2,     0.4,   0.0],   # Joint 1 (base rotation)
    [0.25,   0.0,         0.0,   0.0],   # Joint 2 (shoulder)
    [0.025,  np.pi/2,     0.0,   0.0],   # Joint 3 (elbow)
    [0.0,   -np.pi/2,     0.28,  0.0],   # Joint 4 (wrist roll)
    [0.0,    np.pi/2,     0.0,   0.0],   # Joint 5 (wrist pitch)
    [0.0,    0.0,         0.1,   0.0],   # Joint 6 (wrist yaw)
]

def forward_kinematics(joint_angles, dh_params=DH_PARAMS):
    """
    Compute end-effector position and orientation from joint angles.
    Returns 4x4 matrix (rotation + position).
    """
    T = np.eye(4)
    for i, (a, alpha, d, offset) in enumerate(dh_params):
        theta = joint_angles[i] + offset
        T = T @ dh_matrix(theta, d, a, alpha)
    return T

# Test: all joints at zero position
q_home = np.zeros(6)
T_home = forward_kinematics(q_home)
position = T_home[:3, 3]
print(f"Home position: x={position[0]:.3f}, y={position[1]:.3f}, z={position[2]:.3f}")

Extracting Position and Orientation

def extract_pose(T):
    """Extract position (xyz) and rotation matrix from 4x4 matrix T."""
    position = T[:3, 3]
    rotation = T[:3, :3]
    return position, rotation

def rotation_to_euler(R):
    """Convert rotation matrix to Euler angles (ZYX convention)."""
    sy = np.sqrt(R[0, 0]**2 + R[1, 0]**2)
    singular = sy < 1e-6

    if not singular:
        roll  = np.arctan2(R[2, 1], R[2, 2])
        pitch = np.arctan2(-R[2, 0], sy)
        yaw   = np.arctan2(R[1, 0], R[0, 0])
    else:
        roll  = np.arctan2(-R[1, 2], R[1, 1])
        pitch = np.arctan2(-R[2, 0], sy)
        yaw   = 0.0

    return np.array([roll, pitch, yaw])

Jacobian Matrix — The Key to Numerical IK

What is the Jacobian?

The Jacobian matrix J describes the differential relationship between joint velocities and end-effector velocities:

v = J(q) * dq

Where v is a 6D vector (3 linear + 3 angular velocities) and q is the joint angle vector. For numerical IK, the Jacobian tells us: "if I want the end-effector to move delta_x, how much do I change the joint angles?"

Computing the Jacobian via Finite Differences

The simplest and most robust method is finite differences:

def compute_jacobian(joint_angles, dh_params=DH_PARAMS, delta=1e-6):
    """
    Compute numerical Jacobian (6x6) via finite differences.
    Rows 0-2: linear velocity (dx, dy, dz)
    Rows 3-5: angular velocity (drx, dry, drz)
    """
    n_joints = len(joint_angles)
    J = np.zeros((6, n_joints))

    T_current = forward_kinematics(joint_angles, dh_params)
    pos_current, R_current = extract_pose(T_current)
    euler_current = rotation_to_euler(R_current)

    for i in range(n_joints):
        q_perturbed = joint_angles.copy()
        q_perturbed[i] += delta

        T_perturbed = forward_kinematics(q_perturbed, dh_params)
        pos_perturbed, R_perturbed = extract_pose(T_perturbed)
        euler_perturbed = rotation_to_euler(R_perturbed)

        # Linear velocity columns
        J[:3, i] = (pos_perturbed - pos_current) / delta
        # Angular velocity columns
        J[3:, i] = (euler_perturbed - euler_current) / delta

    return J

Jacobian matrix and end-effector velocity illustration

Numerical IK Solver: Newton-Raphson Method

The Algorithm

Core idea: iteratively compute error between current and desired pose, then use pseudo-inverse Jacobian to update joint angles:

while |error| > tolerance:
    error = target_pose - current_pose
    dq = J_pinv * error
    q = q + dq

Full Implementation

def pose_error(T_current, T_target):
    """
    Compute 6D error between current and target pose.
    Returns: [dx, dy, dz, drx, dry, drz]
    """
    pos_err = T_target[:3, 3] - T_current[:3, 3]

    R_current = T_current[:3, :3]
    R_target = T_target[:3, :3]
    R_err = R_target @ R_current.T

    # Extract rotation error as axis-angle
    angle = np.arccos(np.clip((np.trace(R_err) - 1) / 2, -1, 1))
    if angle < 1e-6:
        rot_err = np.zeros(3)
    else:
        rot_err = angle / (2 * np.sin(angle)) * np.array([
            R_err[2, 1] - R_err[1, 2],
            R_err[0, 2] - R_err[2, 0],
            R_err[1, 0] - R_err[0, 1]
        ])

    return np.concatenate([pos_err, rot_err])


def inverse_kinematics(T_target, q_init=None, dh_params=DH_PARAMS,
                        max_iter=200, pos_tol=1e-4, rot_tol=1e-3,
                        damping=0.1):
    """
    Numerical IK solver using Damped Least Squares (Levenberg-Marquardt).

    Args:
        T_target: 4x4 target matrix
        q_init: joint angle initialization (None = zeros)
        max_iter: maximum iterations
        pos_tol: position error tolerance (meters)
        rot_tol: orientation error tolerance (rad)
        damping: damping coefficient lambda (avoid singularity)

    Returns:
        q_solution: solution joint angles
        success: True if converged
        iterations: actual iterations taken
    """
    n_joints = len(dh_params)
    q = q_init.copy() if q_init is not None else np.zeros(n_joints)

    for iteration in range(max_iter):
        T_current = forward_kinematics(q, dh_params)
        error = pose_error(T_current, T_target)

        pos_err_norm = np.linalg.norm(error[:3])
        rot_err_norm = np.linalg.norm(error[3:])

        # Check convergence
        if pos_err_norm < pos_tol and rot_err_norm < rot_tol:
            return q, True, iteration

        # Compute Jacobian and damped pseudo-inverse
        J = compute_jacobian(q, dh_params)
        # Damped Least Squares: dq = J^T (J J^T + lambda^2 I)^(-1) * error
        JJT = J @ J.T
        dq = J.T @ np.linalg.solve(
            JJT + damping**2 * np.eye(6), error
        )

        q = q + dq

        # Clamp joint angles to [-pi, pi]
        q = np.mod(q + np.pi, 2 * np.pi) - np.pi

    return q, False, max_iter


# === EXAMPLE USAGE ===

# Step 1: Forward kinematics to create target pose
q_desired = np.array([0.3, -0.5, 0.8, 0.1, -0.3, 0.6])
T_target = forward_kinematics(q_desired)

print("Target position:", T_target[:3, 3])
print("Target angles (ground truth):", np.degrees(q_desired))

# Step 2: Solve IK from different initial guess
q_init = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
q_solution, success, iters = inverse_kinematics(T_target, q_init)

print(f"\nIK {'converged' if success else 'FAILED'} after {iters} iterations")
print(f"Solution angles: {np.degrees(q_solution).round(2)}")

# Verify
T_result = forward_kinematics(q_solution)
pos_error = np.linalg.norm(T_target[:3, 3] - T_result[:3, 3])
print(f"Position error: {pos_error*1000:.3f} mm")

Damped Least Squares vs Pure Pseudo-Inverse

The Singularity Problem

When a robot approaches a singularity (e.g., two wrist joints aligned), the Jacobian becomes nearly singular, and pseudo-inverse produces extremely large values causing uncontrolled robot motion.

Damped Least Squares (also called Levenberg-Marquardt) adds a damping coefficient lambda:

dq = J^T (J J^T + lambda^2 I)^(-1) * error

When lambda = 0, we have pure pseudo-inverse. When lambda is large, motion is slower but stable near singularities. In practice, lambda = 0.01 - 0.5 works well for most robots.

Adaptive Damping

An advanced technique is to adjust lambda based on distance to singularity:

def adaptive_damping(J, lambda_min=0.01, lambda_max=0.5):
    """Increase damping when near singularity."""
    # Manipulability measure
    w = np.sqrt(max(np.linalg.det(J @ J.T), 0))
    # Threshold
    w_threshold = 0.05
    if w < w_threshold:
        ratio = 1.0 - (w / w_threshold)**2
        return lambda_min + ratio * (lambda_max - lambda_min)
    return lambda_min

Practical Tips When Implementing IK

1. Multiple Solutions

A 6-DOF robot typically has up to 8 solutions for the same target pose. The initial guess determines which solution you get. In real applications, choose the solution closest to the current configuration to minimize joint motion:

def best_ik_solution(T_target, q_current, n_trials=10):
    """Try multiple initial guesses, select closest solution."""
    best_q = None
    best_dist = float('inf')

    for _ in range(n_trials):
        q_init = q_current + np.random.uniform(-0.5, 0.5, 6)
        q_sol, success, _ = inverse_kinematics(T_target, q_init)
        if success:
            dist = np.linalg.norm(q_sol - q_current)
            if dist < best_dist:
                best_dist = dist
                best_q = q_sol

    return best_q

2. Joint Limits

Real robots have joint angle limits. Add clamping after each iteration:

JOINT_LIMITS = [
    (-np.pi, np.pi),       # Joint 1
    (-np.pi/2, np.pi/2),   # Joint 2
    (-np.pi*3/4, np.pi*3/4), # Joint 3
    (-np.pi, np.pi),       # Joint 4
    (-np.pi/2, np.pi/2),   # Joint 5
    (-np.pi, np.pi),       # Joint 6
]

def clamp_joints(q, limits=JOINT_LIMITS):
    return np.array([
        np.clip(q[i], limits[i][0], limits[i][1])
        for i in range(len(q))
    ])

3. Workspace Check

Before running IK, verify the target lies within workspace:

def in_workspace(T_target, dh_params=DH_PARAMS):
    """Quick check if target is reachable."""
    pos = T_target[:3, 3]
    dist = np.linalg.norm(pos)
    # Sum of all link lengths
    max_reach = sum(
        np.sqrt(p[0]**2 + p[2]**2) for p in dh_params
    )
    return dist <= max_reach * 0.95  # 95% safety margin

3D Visualization with Matplotlib

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def plot_robot(joint_angles, dh_params=DH_PARAMS, ax=None):
    """Plot 3D robot arm from joint angles."""
    if ax is None:
        fig = plt.figure(figsize=(10, 8))
        ax = fig.add_subplot(111, projection='3d')

    positions = [np.array([0, 0, 0])]
    T = np.eye(4)

    for i, (a, alpha, d, offset) in enumerate(dh_params):
        theta = joint_angles[i] + offset
        T = T @ dh_matrix(theta, d, a, alpha)
        positions.append(T[:3, 3].copy())

    positions = np.array(positions)
    ax.plot(positions[:, 0], positions[:, 1], positions[:, 2],
            'o-', linewidth=3, markersize=8, color='steelblue')
    ax.scatter(*positions[-1], color='red', s=100, zorder=5,
               label='End-effector')
    ax.scatter(*positions[0], color='green', s=100, zorder=5,
               label='Base')

    ax.set_xlabel('X (m)')
    ax.set_ylabel('Y (m)')
    ax.set_zlabel('Z (m)')
    ax.legend()
    ax.set_title('Robot Arm Configuration')
    plt.tight_layout()
    return ax

# Plot home configuration and IK solution
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7),
                                subplot_kw={'projection': '3d'})
plot_robot(np.zeros(6), ax=ax1)
ax1.set_title('Home Configuration')

plot_robot(q_solution, ax=ax2)
ax2.set_title('IK Solution')
plt.savefig('robot_ik_comparison.png', dpi=150)
plt.show()

3D robot arm visualization with Python Matplotlib

Next Steps

The code in this post is self-contained — requiring only NumPy and Matplotlib. You can extend it further:

If you're new to Python for robotics, read Robot Control Programming with Python to understand GPIO, PID, and computer vision fundamentals.

Related Articles

Related Posts

ISO 10218 thực hành: Risk Assessment cho robot hàn
safetyrobot-armstandards

ISO 10218 thực hành: Risk Assessment cho robot hàn

Hướng dẫn thực hiện risk assessment theo ISO 10218 cho cell robot hàn — từ hazard identification đến safety measures.

28/3/202613 min read
ROS 2 từ A đến Z (Phần 4): ros2_control và Hardware
ros2tutorialrobot-armPart 4

ROS 2 từ A đến Z (Phần 4): ros2_control và Hardware

Kết nối ROS 2 với phần cứng thực — viết hardware interface cho motor driver và đọc encoder với ros2_control framework.

26/3/202611 min read
Multi-robot Coordination: Thuật toán phân công task
fleetamrprogramming

Multi-robot Coordination: Thuật toán phân công task

Các thuật toán phân công nhiệm vụ cho đội robot — từ Hungarian algorithm, auction-based đến RL-based task allocation.

20/3/202612 min read