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_ikservice 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:
| Solver | Plugin name | Good at | Watch out for |
|---|---|---|---|
| KDL (default) | kdl_kinematics_plugin/KDLKinematicsPlugin | Simple setup, works out of the box from Setup Assistant | Newton’s-method convergence struggles near joint limits and can fail to find a solution that exists |
| TRAC-IK | trac_ik_kinematics_plugin/TRAC_IKKinematicsPlugin | Runs KDL’s approach and an SQP optimizer in parallel, races them | Extra dependency to install; still a local (single-solution) solver |
| pick_ik | pick_ik/PickIkPlugin | A global (evolutionary) search plus local refinement; more likely to find a solution when one exists | More 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”:
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 want | How to do it |
|---|---|
| Get joint angles for a target pose | Call MoveIt’s /compute_ik service (moveit_msgs/srv/GetPositionIK) |
| Choose a kinematics solver | Set kinematics_solver in kinematics.yaml — KDL (default), TRAC-IK, or pick_ik |
| Fix “IK failed” on a reachable-looking pose | Loosen the orientation constraint, increase the timeout, or try a different solver |
| Understand a different answer for the “same” target | The seed (current) joint state shapes which of several valid solutions comes back |
| Know when zero solutions exist | Out of reach, orientation too constrained, or sitting in a singularity |
| Know what IK does not guarantee | A 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.