Analytic inverse kinematics
For a six-axis arm described by standard Denavit-Hartenberg parameters the inverse kinematics can be solved without iteration. The equations are derived symbolically, once, for the particular robot. After that a pose is solved by evaluating them, which gives every joint configuration that reaches it, or tells you that the pose cannot be reached.
The same solution can be written out as source code in Python, C, C++ or Rust, for use on a robot controller or in another program. The generated code is released under the MIT licence, like the Toolbox, and needs neither the Toolbox nor SymPy.
Note
Deriving the equations needs SymPy, which is not installed with the Toolbox by default:
pip install roboticstoolbox-python[ik]
The ik extra also installs python-flint, which makes the derivation much faster. Without it a warning is issued and SymPy is used instead, which gives the same results. It is not part of the all extra.
Added in version 1.5.0.
The notebook ik-symbolic.ipynb works through the examples on this page, and more.
Setting up
The solver is made by the robot. The first call for a robot derives the equations, which takes a few seconds, and the solver is kept so that later calls are immediate.
>>> import numpy as np
>>> import roboticstoolbox as rtb
>>> puma = rtb.models.DH.Puma560()
>>> ik = puma.analytic_ik()
>>> print(ik)
ClosedFormIK for Puma 560: closed form
joints meeting at a point: 4-6
joints with parallel axes: none
The method is chosen from the structure of the arm, and each method suits only its own kind of arm:
Arm |
Method |
Solver class |
|---|---|---|
three consecutive joint axes meet at a point (a |
closed form, derived symbolically |
|
spherical wrist) or are parallel |
||
neither, for example the Kinova Jaco |
general 6R method of Raghavan and Roth [Raghavan93] |
|
Solving
To pose a problem we need a pose that can be reached, so we take the forward kinematics of a joint configuration that is within the joint limits.
>>> import numpy as np
>>> import roboticstoolbox as rtb
>>> puma = rtb.models.DH.Puma560()
>>> q = np.array([0.2, 0.9, -0.8, 0.3, -0.5, 0.4])
>>> T = puma.fkine(q)
>>> sol = puma.analytic_ik().solve(T)
>>> len(sol)
7
>>> np.set_printoptions(precision=3, suppress=True)
>>> sol.q
array([[ 0.2 , 1.624, -2.248, -0.538, 0.28 , 1.185],
[ 0.2 , 1.624, -2.248, 2.604, -0.28 , -1.956],
[ 0.2 , 0.9 , -0.8 , 0.3 , -0.5 , 0.4 ],
[ 2.244, 2.242, -2.248, 1.451, 0.406, -2.801],
[ 2.244, 2.242, -2.248, -1.691, -0.406, 0.34 ],
[ 2.244, 1.517, -0.8 , 2.541, 0.767, 2.239],
[ 2.244, 1.517, -0.8 , -0.601, -0.767, -0.903]])
solve() gives all the solutions as the rows of q in an IKSolution, the same result type as the numerical solvers. Each row is a different arm configuration, and every one reaches the pose.
Usually you want one solution. The ikine_analytic method of the robot is the counterpart of ikine_LM and the other numerical methods. It returns the solution nearest to q0, and weights changes what nearest means: a weight of zero ignores a joint.
>>> import numpy as np
>>> import roboticstoolbox as rtb
>>> puma = rtb.models.DH.Puma560()
>>> q = np.array([0.2, 0.9, -0.8, 0.3, -0.5, 0.4])
>>> T = puma.fkine(q)
>>> sol = puma.ikine_analytic(T, q0=q + 0.3)
>>> sol
IKSolution: q=[0.2, 0.9, -0.8, 0.3, -0.5, 0.4], success=True, residual=3.91e-16
iterations and searches are zero, since nothing is searched, and residual is the error of the forward kinematics of the solution.
A pose that can’t be reached is not an exception. The result is a failed IKSolution that says why, and q is NaN.
>>> import roboticstoolbox as rtb
>>> from spatialmath import SE3
>>> puma = rtb.models.DH.Puma560()
>>> puma.ikine_analytic(SE3(5, 5, 5))
IKSolution: q=[nan, nan, nan, nan, nan, nan], success=False, reason=the pose is outside the workspace
Joint limits
Solutions that violate the joint limits are discarded, as the numerical solvers do with joint_limits=True. Revolute angles are first shifted by whole turns, since \(q\) and \(q \pm 2\pi\) are the same configuration, so a solution is rejected only if no equivalent angle is within the limits. Use joint_limits=False to get everything that the equations allow.
Trajectories
A trajectory is an SE3 with N poses, or an array of shape (N, 4, 4), and the solution has one row of q for each pose. The first pose is solved nearest to q0 and each later pose nearest to the solution of the one before, so the path stays on one branch for as long as the arm can. This differs from the numerical solvers, which solve every pose from the same q0. If a pose can’t be solved success is false, the row for that pose is NaN, and reason names the first such pose.
>>> import numpy as np
>>> import roboticstoolbox as rtb
>>> puma = rtb.models.DH.Puma560()
>>> qa = np.array([0.2, 0.9, -0.8, 0.3, -0.5, 0.4])
>>> qb = qa + [0.5, -0.3, 0.4, 0.6, -0.2, 0.5]
>>> Ts = rtb.ctraj(puma.fkine(qa), puma.fkine(qb), 50)
>>> sol = puma.ikine_analytic(Ts, q0=qa)
>>> sol.success, sol.q.shape
(True, (50, 6))
Generating code
generate writes the solution as source code. The first argument is the language and the second is the name of the function. Every file starts with a header that says where it came from, and records the parameters of the robot.
Language |
Closed form and general 6R |
Needs |
|---|---|---|
Python |
both |
NumPy for the general method, and SciPy if installed |
C |
both |
|
C++ |
both |
the C++17 standard library only |
Rust |
closed form only |
the standard library only |
>>> import roboticstoolbox as rtb
>>> puma = rtb.models.DH.Puma560()
>>> source = puma.analytic_ik().generate("c", "puma_ik")
>>> lines = source.splitlines()
>>> len(lines)
259
>>> print("\n".join(lines[:9]))
/*
* Generated by Robotics Toolbox for Python by Peter Corke
* https://github.com/petercorke/robotics-toolbox-python
* Robotics Toolbox for Python 1.4.4, MIT licence
*
* Generated 2026-10-23 02:31:33 UTC (UTC+0000)
* Method: closed form, derived symbolically (symbolic_ik)
*
* The robot, as printed by Robotics Toolbox. Standard DH parameters, with the
The generated C is written with embedded use in mind: no dynamic memory allocation, no input or output, no recursion, no variable-length arrays, and working arrays of fixed size, with the solutions written to an array that the caller provides. It compiles without warnings as C99 with -pedantic -Wall -Wextra -Wvla. It has not been checked against a coding standard such as MISRA C, see #839. The C++ and Rust code return their solutions in a Vec or std::vector, which allocates.
The general 6R method needs to find the roots of a polynomial matrix, so its C and C++ code includes a small linear algebra kernel of about 300 lines: LU, Householder QR, a one-sided Jacobi SVD and an eigenvalue solver, in plain C99. It was written for this project from the standard algorithms, with the references in the source, and is tested against NumPy, but it has had far less use than LAPACK.
Limitations
six joints and standard DH parameters, not modified DH
an arm that needs the general method and has a prismatic joint is not supported, and
analytic_ikraisesNotImplementedErrorthe general method is less well conditioned than the closed form, it is slower, and near a singularity its accuracy falls; every solution is checked against the forward kinematics before it is returned
Rust code is generated for the closed form only
For a robot that this can’t handle, the numerical methods ikine_LM() and its relatives still work.
The methods
Analytic inverse kinematics solver for this robot |
|
Analytic inverse kinematics |
The solver classes
- class AnalyticIK(robot)[source]
The interface of an analytic inverse kinematics solver for a robot
- Parameters:
robot (
DHRobot) – the robot, with standard DH links and six joints
Made by
analytic_ik(), which chooses the method that suits the arm:ClosedFormIKfor an arm with a spherical wrist or three parallel axes, andGeneral6RIKfor the others. The equations are derived symbolically when the solver is made, which takes seconds, and the solver can then be used for any number of poses, or written out as source code that has no dependency on the Toolbox, seegenerate().Two methods solve a problem, and return an
IKSolutionas the numerical solvers do:solve()gives all the solutions for one pose, as the rows ofqikine()gives one solution for each pose, the nearest toq0, for a pose or a trajectory, asikine_LM()does
Added in version 1.5.0.
- solve(T, joint_limits=True)[source]
All the solutions for one pose
- Parameters:
- Return type:
- Returns:
the solutions as the rows of
q, shape (m, n), withsuccesstrue if there is at least one. Otherwiseqis empty andreasonsays why.- Raises:
ValueError – if
Tis more than one pose, useikine()
There can be several solutions for a pose, up to 8 for an arm with a spherical wrist and 16 for the general case, and all are returned.
>>> import roboticstoolbox as rtb >>> puma = rtb.models.DH.Puma560() >>> ik = puma.analytic_ik() >>> sol = ik.solve(puma.fkine(puma.qn)) >>> len(sol) 3
- ikine(T, q0=None, joint_limits=True, weights=None)[source]
One solution for each pose, the nearest to a configuration
- Parameters:
T (
SE3|ndarray) – end-effector pose, an SE3 or a 4x4 array, or a trajectory of poses, an SE3 with N poses or an array (N, 4, 4)q0 (
ndarray|Sequence[float] |None) – configuration to be near, default all zeros. For a trajectory it is used for the first pose, and each later pose is solved nearest to the solution of the one before, so the path stays on one branch where it can.joint_limits (
bool) – reject solutions that violate the joint limitsweights (
ndarray|Sequence[float] |None) – weight for each joint in the distance fromq0, default 1. A weight of zero ignores that joint.
- Return type:
- Returns:
qof shape (n,) for a pose, or (N, n) for a trajectory. If any pose has no solutionsuccessis false,reasonsays why and the rows for those poses are NaN.- Raises:
ValueError – if
q0has the wrong length
The distance is the weighted norm of the joint differences, with revolute differences wrapped to \((-\pi, \pi]\), and the angles returned are the equivalent ones nearest to
q0, if they are within the limits.iterationsandsearchesare zero, as there is no iteration, andresidualis the largest error of the forward kinematics of the solutions, as the norm of the 6-vector of the error, seetr2delta().
- abstractmethod generate(language='python', name='ikine')[source]
Source code for the solution for this robot
- Parameters:
- Return type:
- Returns:
source code, which starts with a header saying where it came from
- Raises:
NotImplementedError – for a language that is not supported
- class ClosedFormIK(robot, max_time=120.0, verify=True)[source]
Bases:
AnalyticIKInverse kinematics from the closed form solution of
symbolic_ik- Parameters:
- Raises:
ValueError – if no closed form solution is found
The symbolic solution is found once, when the solver is made. The solution is then generated as Python code, which is run to solve, and the same plan can be written in other languages by
generate().
- class General6RIK(robot, k=None, seed=0)[source]
Bases:
AnalyticIKInverse kinematics of a general 6R arm
- Parameters:
- Raises:
ValueError – if the robot does not have six revolute joints
The first time a robot is used the equations are derived symbolically, which takes a few seconds per split. The solver is Python code, which is generated and then run, see
generate().solve(T)returns all the solutions for a pose, andikine(T, q0)one for each pose, as anIKSolution.Note
The method degenerates for some geometries, for example a spherical wrist. Check with
validate, or useclassify.
Raghavan and B. Roth, “Inverse kinematics of the general 6R manipulator and related linkages,” Journal of Mechanical Design, vol. 115, no. 3, pp. 502–508, September 1993.