Weekly Robotics logo
Weekly Robotics Beginner-friendly tutorials, every week
Inverse Kinematics with MoveIt 2
Control Systems

Inverse Kinematics with MoveIt 2

You know where you want a robot arm's fingertip to end up — but the motors only understand joint angles. Learn how MoveIt 2 solves that gap for you, and how to call it from your own ROS 2 code.

Last week’s gripper can open, close, and tell you when it’s holding something — but it still has to get to the object first. Back in Week 19 you saw why that’s hard: forward kinematics (joint angles → fingertip position) always has exactly one answer, but inverse kinematics (fingertip position → joint angles) can have many answers, or none. That post worked the equations for a simple two-link arm by hand. A real arm — five, six, or seven joints, with limits, offsets, and a gripper hanging off the end — is not something you want to solve with algebra every time. That’s what MoveIt 2 is for: it already knows your robot’s geometry from its URDF, and it will hand you a set of joint angles for a target pose on request. This week is about how to ask it.

What You’ll Learn

  • Why MoveIt doesn’t need you to write kinematics equations by hand
  • Which kinematics solver MoveIt uses by default, and when to swap in a better one
  • How to call the /compute_ik service from a Python node
  • Why the same target pose can return different joint angles depending on where the arm currently is
  • The pitfalls that make IK calls fail, return a useless solution, or hide a collision

MoveIt Already Solved the Equations For You

MoveIt 2 loads your robot’s kinematic chain from its URDF and SRDF (the files a MoveIt Setup Assistant run produces) and uses that model to answer exactly the two questions from Week 19, as ROS 2 services provided by the move_group node:

  • Forward kinematics — given joint angles, where’s the end effector? (/compute_fk)
  • Inverse kinematics — given a target pose, what joint angles reach it? (/compute_ik)

You never touch the Jacobian or the trigonometry yourself. Instead, you send a target pose over a service call, and a kinematics plugin — a swappable piece of MoveIt configured per robot in a file called kinematics.yaml — does the solving.

Picking a Kinematics Solver

The plugin matters more than it looks like it should, because not all solvers handle the same situations well:

SolverPlugin nameGood atWatch out for
KDL (default)kdl_kinematics_plugin/KDLKinematicsPluginSimple setup, works out of the box from Setup AssistantNewton’s-method convergence struggles near joint limits and can fail to find a solution that exists
TRAC-IKtrac_ik_kinematics_plugin/TRAC_IKKinematicsPluginRuns KDL’s approach and an SQP optimizer in parallel, races themExtra dependency to install; still a local (single-solution) solver
pick_ikpick_ik/PickIkPluginA global (evolutionary) search plus local refinement; more likely to find a solution when one existsMore parameters to tune (mode, position_scale, rotation_scale)

Swapping solvers is a one-line change in kinematics.yaml — no code changes, since every solver answers through the same /compute_ik service. If you’re using the default KDL plugin and seeing IK calls fail on poses that should be reachable, trying TRAC-IK or pick_ik before assuming your target is actually out of range is a cheap, common fix.

Calling /compute_ik from Python

Here’s a minimal client that asks MoveIt for a joint solution reaching a target position:

import rclpy
from rclpy.node import Node
from moveit_msgs.srv import GetPositionIK
from moveit_msgs.msg import PositionIKRequest
from geometry_msgs.msg import PoseStamped

class IKClient(Node):
    def __init__(self):
        super().__init__('ik_client')
        self.client = self.create_client(GetPositionIK, '/compute_ik')
        self.client.wait_for_service()

    def solve(self, x: float, y: float, z: float):
        request = GetPositionIK.Request()
        request.ik_request = PositionIKRequest()
        request.ik_request.group_name = 'arm'          # your SRDF planning group
        request.ik_request.avoid_collisions = True
        request.ik_request.timeout.sec = 1

        pose = PoseStamped()
        pose.header.frame_id = 'base_link'              # your robot's base frame
        pose.pose.position.x = x
        pose.pose.position.y = y
        pose.pose.position.z = z
        pose.pose.orientation.w = 1.0                    # no rotation constraint
        request.ik_request.pose_stamped = pose

        return self.client.call_async(request)

def main():
    rclpy.init()
    node = IKClient()
    future = node.solve(0.4, 0.0, 0.3)
    rclpy.spin_until_future_complete(node, future)
    result = future.result()

    if result.error_code.val == 1:  # moveit_msgs/MoveItErrorCodes.SUCCESS
        for name, position in zip(result.solution.joint_state.name,
                                   result.solution.joint_state.position):
            node.get_logger().info(f'{name}: {position:.3f} rad')
    else:
        node.get_logger().warn(f'IK failed, error code {result.error_code.val}')

    rclpy.shutdown()

if __name__ == '__main__':
    main()

group_name and the base frame_id come from your robot’s SRDF and URDF — check the Setup Assistant output or your MoveIt config package before running this against real hardware. avoid_collisions=True asks the solver to reject a joint solution that collides with the robot itself or known obstacles, which is worth leaving on even at this stage.

One Target, Many (or Zero) Solutions

A 6-DOF arm can usually reach a given position with its elbow up or down — both are valid, equally correct answers to “what joint angles put the fingertip here”:

target elbow up elbow down
Two different joint configurations (elbow up, elbow down) both put the fingertip at the same target — IK solvers return one valid answer, not the only one.

Which solution you get back depends on the seed state — usually the arm’s current joint position, which the solver searches outward from. That’s a feature, not a bug: it’s what keeps the arm from suddenly flipping to a wildly different pose for a small pose change, but it also means calling /compute_ik twice from two different starting states for the same target can legitimately return two different answers.

At the other extreme, a target can have zero solutions: it might be geometrically out of reach, or reachable only with an end-effector orientation your request doesn’t allow, or sitting in a singularity — a configuration (often full arm extension) where the arm briefly loses a degree of freedom and tiny position changes demand huge, sometimes undefined, joint velocity changes.

Common Pitfalls

Treating “IK succeeded” as “safe to move there.” /compute_ik (with avoid_collisions on) checks that the final pose is collision-free, but it says nothing about the path from where the arm is now to that pose. Getting a valid joint solution is necessary, not sufficient — actually moving there safely is next week’s topic, motion planning.

Over-constraining the orientation. Fully specifying a target orientation (not just orientation.w = 1.0 as a placeholder, but a real, specific rotation) shrinks the solution space a lot. If IK keeps failing on a pose that looks reachable, try loosening the orientation requirement before assuming the position itself is out of range.

Blaming the robot when it’s the solver. A KDL failure on a pose near a joint limit doesn’t always mean no solution exists — it often means Newton’s method got stuck. Try TRAC-IK or pick_ik before redesigning your robot’s reach.

Ignoring the timeout. A short timeout (the example above uses 1 second, generous for testing) can cut off a solver before it finds a solution that’s actually there, especially a global solver like pick_ik mid-search. Give it more time during development, then tighten it once you know realistic solve times for your robot.

Forgetting the seed state matters. If your arm keeps unexpectedly flipping to an awkward elbow configuration between two nearby targets, it’s often because the “current state” used as the seed changed more than you expected between calls — not because the solver is unstable.

Recap

What you wantHow to do it
Get joint angles for a target poseCall MoveIt’s /compute_ik service (moveit_msgs/srv/GetPositionIK)
Choose a kinematics solverSet kinematics_solver in kinematics.yaml — KDL (default), TRAC-IK, or pick_ik
Fix “IK failed” on a reachable-looking poseLoosen the orientation constraint, increase the timeout, or try a different solver
Understand a different answer for the “same” targetThe seed (current) joint state shapes which of several valid solutions comes back
Know when zero solutions existOut of reach, orientation too constrained, or sitting in a singularity
Know what IK does not guaranteeA collision-free path to get there — that’s motion planning, not IK

A joint solution that reaches the target is only half the job — the arm still has to travel there without hitting itself, the table, or the object it’s about to pick up. Next week picks up exactly that thread: Motion Planning with MoveIt 2.