Robotics From Zero
All Posts
Deep Dive

Inverse Kinematics Explained: Making Robot Arms Reach Their Target

A thorough explanation of inverse kinematics for robot arms. Covers forward vs inverse kinematics, geometric solutions for 2-link arms, the Jacobian method, iterative solvers, and includes complete Python implementations.

Robotics From Zero Team13 min readUpdated

Inverse Kinematics Explained: Making Robot Arms Reach Their Target

You tell a robot arm "move your end-effector to position (0.3, 0.5, 0.2) meters." The arm has six joints. What angle should each joint be at to place the end-effector at that position?

This is the inverse kinematics (IK) problem, and it is one of the most fundamental challenges in robotics. Every robot arm, from a 2-DOF educational kit to a 7-DOF industrial manipulator, needs an IK solver. Without one, you can describe what the joints are doing (forward kinematics) but you cannot command the arm to reach a specific point in space.

This post explains the IK problem from the ground up: what forward kinematics gives you, why inverting it is hard, how to solve it for simple cases analytically, and how iterative methods handle the general case. Code examples are in Python throughout.

Forward Kinematics: The Easy Direction

Before tackling inverse kinematics, you need to understand the forward problem. Forward kinematics (FK) computes the position and orientation of the end-effector given all joint angles. It is a direct computation with exactly one answer.

For a planar 2-link arm (two revolute joints, two rigid links), forward kinematics is straightforward trigonometry:

import math
 
 
def forward_kinematics_2link(
    theta1: float, theta2: float, l1: float, l2: float
) -> tuple[float, float]:
    """
    Compute end-effector position for a 2-link planar arm.
 
    Args:
        theta1: Angle of joint 1 from the positive x-axis (radians).
        theta2: Angle of joint 2 relative to link 1 (radians).
        l1: Length of link 1 (meters).
        l2: Length of link 2 (meters).
 
    Returns:
        (x, y): End-effector position in world coordinates.
    """
    x = l1 * math.cos(theta1) + l2 * math.cos(theta1 + theta2)
    y = l1 * math.sin(theta1) + l2 * math.sin(theta1 + theta2)
    return x, y

Given theta1 = 0.5 rad and theta2 = 0.8 rad, the function returns a unique (x, y). No ambiguity, no iteration, no special cases.

For a general 3D arm with N joints, forward kinematics is computed using a chain of homogeneous transformation matrices, one per joint, following the Denavit-Hartenberg (DH) convention or the product-of-exponentials formulation. The result is always a single 4x4 matrix encoding the end-effector's position and orientation.

The Inverse Problem: Why It Is Hard

Inverse kinematics flips the question. Given a desired end-effector position (and possibly orientation), find the joint angles that achieve it.

This is hard for several reasons:

Multiple solutions. A 2-link planar arm reaching a point within its workspace typically has two valid configurations: "elbow up" and "elbow down." A 6-DOF arm can have up to 16 distinct solutions for a given end-effector pose. Your solver must either return all solutions and let you pick, or have a strategy for choosing one (e.g., closest to the current configuration).

No solution. If the target is outside the arm's workspace, no joint configuration can reach it. Your solver must detect and report this case.

Infinite solutions (redundancy). A 7-DOF arm reaching a 6-DOF target has one extra degree of freedom. There are infinitely many joint configurations that place the end-effector at the same pose. Redundancy resolution strategies (minimize joint velocities, avoid joint limits, maximize manipulability) are needed to select one.

Nonlinear equations. The forward kinematics equations involve sines and cosines of joint angles. Inverting them requires solving systems of transcendental equations, which generally have no closed-form solution.

Geometric Solution for a 2-Link Planar Arm

For simple arms, you can derive a closed-form (analytical) solution using geometry. The 2-link planar arm is the canonical example.

Given a desired end-effector position (x, y) and link lengths l1 and l2, we want to find theta1 and theta2.

Step 1: Find theta2 using the law of cosines.

The distance from the base to the target is:

d = sqrt(x^2 + y^2)

By the law of cosines applied to the triangle formed by the two links and the line from base to target:

d^2 = l1^2 + l2^2 - 2 * l1 * l2 * cos(pi - theta2)

Since cos(pi - theta2) = -cos(theta2):

cos(theta2) = (x^2 + y^2 - l1^2 - l2^2) / (2 * l1 * l2)

If the right side is outside [-1, 1], the target is unreachable (too far or too close).

There are two solutions for theta2: one positive (elbow down) and one negative (elbow up), obtained using atan2 and the sine:

sin(theta2) = +/- sqrt(1 - cos(theta2)^2)
theta2 = atan2(sin(theta2), cos(theta2))

Step 2: Find theta1.

theta1 = atan2(y, x) - atan2(l2 * sin(theta2), l1 + l2 * cos(theta2))

Here is the complete implementation:

def inverse_kinematics_2link(
    x: float, y: float, l1: float, l2: float, elbow_up: bool = True
) -> tuple[float, float] | None:
    """
    Compute joint angles for a 2-link planar arm to reach (x, y).
 
    Args:
        x: Target x position (meters).
        y: Target y position (meters).
        l1: Length of link 1 (meters).
        l2: Length of link 2 (meters).
        elbow_up: If True, return elbow-up solution. Otherwise elbow-down.
 
    Returns:
        (theta1, theta2) in radians, or None if the target is unreachable.
    """
    dist_sq = x * x + y * y
    cos_theta2 = (dist_sq - l1 * l1 - l2 * l2) / (2.0 * l1 * l2)
 
    # Check reachability
    if cos_theta2 < -1.0 or cos_theta2 > 1.0:
        return None
 
    sin_theta2 = math.sqrt(1.0 - cos_theta2 * cos_theta2)
    if elbow_up:
        sin_theta2 = -sin_theta2
 
    theta2 = math.atan2(sin_theta2, cos_theta2)
    theta1 = math.atan2(y, x) - math.atan2(
        l2 * sin_theta2, l1 + l2 * cos_theta2
    )
 
    return theta1, theta2
 
 
# Verification: forward(inverse(target)) should return target
if __name__ == "__main__":
    l1, l2 = 1.0, 0.8
    target_x, target_y = 1.2, 0.6
 
    for elbow_up in [True, False]:
        result = inverse_kinematics_2link(target_x, target_y, l1, l2, elbow_up)
        if result:
            t1, t2 = result
            fx, fy = forward_kinematics_2link(t1, t2, l1, l2)
            config = "elbow-up" if elbow_up else "elbow-down"
            print(f"{config}: theta1={math.degrees(t1):.1f} deg, "
                  f"theta2={math.degrees(t2):.1f} deg")
            print(f"  FK check: ({fx:.4f}, {fy:.4f}) vs target ({target_x}, {target_y})")

This geometric approach is exact and fast (no iteration), but it only works when you can derive the closed-form equations by hand. For a 6-DOF arm with arbitrary DH parameters, the algebra becomes impractical.

The Jacobian Method

For general robot arms, the standard approach to IK is iterative: start from the current joint configuration, compute how to adjust the joints to move the end-effector closer to the target, and repeat until convergence.

The key tool is the Jacobian matrix. The Jacobian relates small changes in joint angles to small changes in end-effector position:

delta_x = J * delta_theta

where delta_x is a vector of end-effector displacements (e.g., [dx, dy] in 2D or [dx, dy, dz, d_roll, d_pitch, d_yaw] in 3D), delta_theta is a vector of joint angle changes, and J is the Jacobian matrix.

To solve for delta_theta given a desired delta_x, we invert the Jacobian:

delta_theta = J^(-1) * delta_x

If J is not square (the arm has more joints than task-space dimensions), we use the pseudoinverse:

delta_theta = J^T * (J * J^T)^(-1) * delta_x

This is the Jacobian pseudoinverse method, and it is the foundation of most practical IK solvers.

Jacobian for the 2-Link Arm

For the 2-link planar arm, the Jacobian is a 2x2 matrix:

J = [[-l1*sin(t1) - l2*sin(t1+t2),  -l2*sin(t1+t2)],
     [ l1*cos(t1) + l2*cos(t1+t2),   l2*cos(t1+t2)]]

Each column represents how the end-effector moves when one joint changes. Each row represents one dimension of end-effector motion (x or y).

Iterative IK with the Jacobian

import numpy as np
 
 
def jacobian_2link(
    theta1: float, theta2: float, l1: float, l2: float
) -> np.ndarray:
    """Compute the 2x2 Jacobian for a 2-link planar arm."""
    s1 = math.sin(theta1)
    c1 = math.cos(theta1)
    s12 = math.sin(theta1 + theta2)
    c12 = math.cos(theta1 + theta2)
 
    return np.array([
        [-l1 * s1 - l2 * s12, -l2 * s12],
        [ l1 * c1 + l2 * c12,  l2 * c12],
    ])
 
 
def ik_jacobian(
    target_x: float,
    target_y: float,
    l1: float,
    l2: float,
    theta1_init: float = 0.1,
    theta2_init: float = 0.1,
    max_iterations: int = 200,
    tolerance: float = 1e-4,
    step_size: float = 0.5,
) -> tuple[float, float] | None:
    """
    Solve 2-link IK using the Jacobian pseudoinverse method.
 
    Args:
        target_x, target_y: Desired end-effector position.
        l1, l2: Link lengths.
        theta1_init, theta2_init: Initial joint angle guesses.
        max_iterations: Maximum solver iterations.
        tolerance: Position error threshold for convergence.
        step_size: Damping factor to prevent overshoot (0 < step_size <= 1).
 
    Returns:
        (theta1, theta2) or None if the solver did not converge.
    """
    theta1 = theta1_init
    theta2 = theta2_init
 
    for iteration in range(max_iterations):
        # Current end-effector position
        x, y = forward_kinematics_2link(theta1, theta2, l1, l2)
 
        # Error vector
        error = np.array([target_x - x, target_y - y])
        error_magnitude = np.linalg.norm(error)
 
        if error_magnitude < tolerance:
            return theta1, theta2
 
        # Compute Jacobian
        J = jacobian_2link(theta1, theta2, l1, l2)
 
        # Pseudoinverse (handles singular and non-square cases)
        J_pinv = np.linalg.pinv(J)
 
        # Joint angle update
        delta_theta = step_size * J_pinv @ error
 
        theta1 += delta_theta[0]
        theta2 += delta_theta[1]
 
    return None  # Did not converge
 
 
# Example usage
if __name__ == "__main__":
    l1, l2 = 1.0, 0.8
 
    targets = [(1.2, 0.6), (0.5, 1.0), (1.5, 0.0), (0.3, 0.3)]
 
    for tx, ty in targets:
        result = ik_jacobian(tx, ty, l1, l2)
        if result:
            t1, t2 = result
            fx, fy = forward_kinematics_2link(t1, t2, l1, l2)
            print(f"Target ({tx}, {ty}): theta1={math.degrees(t1):.1f}, "
                  f"theta2={math.degrees(t2):.1f}, "
                  f"achieved ({fx:.4f}, {fy:.4f})")
        else:
            print(f"Target ({tx}, {ty}): solver did not converge")

The step_size parameter (also called a damping factor or learning rate) prevents the solver from overshooting. Values between 0.1 and 1.0 are typical. Smaller values converge more reliably but take more iterations.

Other IK Methods

Damped Least Squares (Levenberg-Marquardt)

Near singularities (configurations where the Jacobian loses rank, meaning the arm cannot move in some direction), the pseudoinverse produces huge joint velocities. The damped least squares method adds a regularization term:

delta_theta = J^T * (J * J^T + lambda^2 * I)^(-1) * delta_x

The damping factor lambda prevents the inversion from blowing up near singularities. This is the most widely used method in practice because it balances accuracy with numerical stability.

CCD (Cyclic Coordinate Descent)

CCD is a popular method in animation and games. It works through the joints one at a time, from the end-effector back to the base. For each joint, it rotates the joint to minimize the distance between the end-effector and the target. One pass through all joints constitutes one iteration.

CCD is simple to implement, does not require computing a Jacobian, and handles joint limits naturally. However, it can converge slowly and sometimes produces unnatural-looking configurations.

FABRIK (Forward and Backward Reaching Inverse Kinematics)

FABRIK works by alternating forward and backward reaching passes along the kinematic chain. In the forward pass, it moves the end-effector to the target and adjusts joints backward. In the backward pass, it fixes the base position and adjusts forward. Each pass preserves link lengths.

FABRIK converges faster than CCD for many configurations and produces more natural-looking motion. It is widely used in game engines and animation software.

Analytical Solutions for 6-DOF Arms

Some 6-DOF arm geometries (notably those with a spherical wrist, where the last three joint axes intersect at a point) allow closed-form analytical solutions. The Pieper criterion states that a 6-DOF arm with three consecutive intersecting axes or three consecutive parallel axes has an analytical IK solution.

Most industrial arms (UR5, KUKA iiwa, ABB IRB series) have spherical wrists specifically to enable analytical IK. The solution typically decouples position (solved by the first three joints) from orientation (solved by the last three joints using Euler angle decomposition).

Analytical solutions are faster and more reliable than iterative methods, but they require deriving the equations for each specific arm geometry.

Workspace and Reachability

Not every point in space is reachable by a given arm. The workspace is the set of all points the end-effector can reach. For a 2-link planar arm:

  • The reachable workspace is an annular region (donut shape) between radii |l1 - l2| and l1 + l2.
  • The dexterous workspace (where the end-effector can achieve any orientation) is smaller or may be empty for a 2-link arm.

Your IK solver should check reachability before attempting to solve. For the geometric solution, this is the cos(theta2) bounds check. For iterative solvers, failure to converge usually indicates an unreachable target, but you should also check the distance against the arm's maximum reach as a fast pre-filter.

def is_reachable_2link(x: float, y: float, l1: float, l2: float) -> bool:
    """Check if (x, y) is within the 2-link arm's workspace."""
    dist = math.sqrt(x * x + y * y)
    return abs(l1 - l2) <= dist <= l1 + l2

Practical Tips

1. Use a Good Initial Guess

Iterative IK solvers are sensitive to the initial joint configuration. Starting from the robot's current joint angles (rather than zeros) usually converges faster and finds the closest solution, avoiding large, unnecessary joint motions.

2. Respect Joint Limits

Real robot joints have angular limits. After each IK iteration, clamp joint angles to their valid ranges. Better yet, use a solver that incorporates joint limits as constraints (e.g., constrained optimization with scipy.optimize.minimize).

3. Handle Singularities

At singular configurations, the arm loses a degree of freedom in task space. The Jacobian becomes rank-deficient, and the pseudoinverse produces very large joint velocities. Use damped least squares to handle this gracefully, and avoid commanding the arm through singularities during trajectory planning.

4. Consider Using a Library

For production systems, use established IK libraries rather than rolling your own:

  • IKFast (OpenRAVE): Generates analytical IK solvers for specific arm geometries at compile time. Extremely fast at runtime.
  • KDL (Orocos): General-purpose kinematics library. Used in ROS MoveIt.
  • Pinocchio: Modern C++ rigid body dynamics library with fast FK/IK. Widely used in research.
  • ikpy: Pure Python IK library. Great for prototyping and education.

5. Validate with Forward Kinematics

Always verify your IK solution by passing the computed joint angles through forward kinematics and checking that the result matches the target within tolerance. This catches bugs and numerical issues early.

Summary

Inverse kinematics is the bridge between what you want the robot to do ("reach this point") and what the robot needs to execute ("set these joint angles"). For simple arms, geometric solutions are exact and fast. For general arms, the Jacobian pseudoinverse method with damping is the standard approach.

The key concepts to remember:

  • Forward kinematics is easy (unique solution, direct computation).
  • Inverse kinematics is hard (multiple solutions, singularities, nonlinear equations).
  • The Jacobian relates joint velocities to end-effector velocities.
  • Iterative solvers converge well with good initial guesses and damping.
  • Always validate IK solutions with FK.

The 2-link arm examples in this post give you the foundation. The same principles -- Jacobians, pseudoinverses, singularity handling, joint limits -- scale directly to 6-DOF and 7-DOF arms used in industry.


Want to try inverse kinematics hands-on? Our Robot Arm Kinematics lesson lets you drag an end-effector target in a 2D simulator and watch the arm solve IK in real time, with both geometric and Jacobian methods visualized side by side.

Related Articles

Related Lessons

Enjoyed this article?

Subscribe to get notified when we publish new content.

Subscribe