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

ClosedFormIK

spherical wrist) or are parallel

neither, for example the Kinova Jaco

general 6R method of Raghavan and Roth [Raghavan93]

General6RIK

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

math.h and string.h only

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_ik raises NotImplementedError

  • the 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_ik

Analytic inverse kinematics solver for this robot

ikine_analytic

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: ClosedFormIK for an arm with a spherical wrist or three parallel axes, and General6RIK for 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, see generate().

Two methods solve a problem, and return an IKSolution as the numerical solvers do:

  • solve() gives all the solutions for one pose, as the rows of q

  • ikine() gives one solution for each pose, the nearest to q0, for a pose or a trajectory, as ikine_LM() does

Added in version 1.5.0.

solve(T, joint_limits=True)[source]

All the solutions for one pose

Parameters:
  • T (SE3 | ndarray) – end-effector pose, an SE3 or a 4x4 array

  • joint_limits (bool) – discard solutions that violate the joint limits. Revolute angles are first shifted by whole turns to fit the limits, as the numerical solvers do.

Return type:

IKSolution

Returns:

the solutions as the rows of q, shape (m, n), with success true if there is at least one. Otherwise q is empty and reason says why.

Raises:

ValueError – if T is more than one pose, use ikine()

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 limits

  • weights (ndarray | Sequence[float] | None) – weight for each joint in the distance from q0, default 1. A weight of zero ignores that joint.

Return type:

IKSolution

Returns:

q of shape (n,) for a pose, or (N, n) for a trajectory. If any pose has no solution success is false, reason says why and the rows for those poses are NaN.

Raises:

ValueError – if q0 has 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. iterations and searches are zero, as there is no iteration, and residual is the largest error of the forward kinematics of the solutions, as the norm of the 6-vector of the error, see tr2delta().

abstractmethod generate(language='python', name='ikine')[source]

Source code for the solution for this robot

Parameters:
  • language (str) – the language of the code

  • name (str) – name of the generated function

Return type:

str

Returns:

source code, which starts with a header saying where it came from

Raises:

NotImplementedError – for a language that is not supported

validate(trials=100, seed=1)[source]

Fraction of random poses for which the original configuration is found

Parameters:
  • trials (int) – number of random configurations to try

  • seed (int) – seed for the random configurations

Return type:

float

Returns:

a value between 0 and 1. A method that works gives 1, or very nearly, and one that does not apply to this robot gives a low value.

class ClosedFormIK(robot, max_time=120.0, verify=True)[source]

Bases: AnalyticIK

Inverse kinematics from the closed form solution of symbolic_ik

Parameters:
  • robot (DHRobot) – robot with standard DH links and six joints

  • max_time (float | None) – give up looking for the symbolic solution after this many seconds

  • verify (bool) – check each solution against the forward kinematics

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: AnalyticIK

Inverse kinematics of a general 6R arm

Parameters:
  • robot (DHRobot) – robot with six revolute joints and standard DH links

  • k (int | None) – which split of the loop closure to use, 0, 1 or 2, see the module documentation. By default the best of the three is chosen numerically.

  • seed (int) – seed for the random poses used to choose k

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, and ikine(T, q0) one for each pose, as an IKSolution.

Note

The method degenerates for some geometries, for example a spherical wrist. Check with validate, or use classify.

[Raghavan93]
  1. 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.