Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,16 +264,15 @@ Tep = SE3.Trans(0.6, -0.3, 0.1) * SE3.OA([0, 1, 0], [0, 0, -1])
sol = robot.ik_LM(Tep) # solve IK
print(sol)

(array([ 0.20592815, 0.86609481, -0.79473206, -1.68254794, 0.74872915,
2.21764746, -0.10255606]), 1, 114, 7, 2.890164057230228e-07)
IKSolution: q=[-2.709, -1.176, 1.961, -1.642, 1.173, 1.958, 2.876], success=True, iterations=6, searches=1, residual=1.06e-07

q_pickup = sol[0]
q_pickup = sol.q
print(robot.fkine(q_pickup)) # FK shows that desired end-effector pose was achieved

1 -8.913e-05 -0.0003334 0.5996
-8.929e-05 -1 -0.0004912 -0.2998
-0.0003334 0.0004912 -1 0.1001
0 0 0 1
-1 0.0002931 0.0003122 0.5999
0.0002931 1 7.601e-05 -0.2999
-0.0003121 7.611e-05 -1 0.1001
0 0 0 1
```

We can animate a path from the ready pose `qr` configuration to this pickup configuration
Expand Down
2 changes: 1 addition & 1 deletion docs/source/IK/ik.rst
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ The :py:class:`~roboticstoolbox.robot.IK.IKSolver` provides basic functionality

iksolution

The :py:class:`~roboticstoolbox.robot.IK.IKSolution` is a :py:class:`dataclasses.dataclass` instance with the following members.
The :py:class:`~roboticstoolbox.robot.IK.IKSolution` is a :py:class:`dataclasses.dataclass` instance with the following members. It is also a sequence of the rows of ``q``, one per pose: ``len(sol)`` is the number of poses, ``for q in sol`` and ``sol[i]`` give the joint coordinates of each pose, and ``bool(sol)`` is ``success``. Iterating over the six members was removed in 1.5.0, use the attributes or ``sol.astuple()``.

============== ========= =====================================================================================================
Element Type Description
Expand Down
165 changes: 125 additions & 40 deletions src/roboticstoolbox/robot/IK.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class IKSolution:
----------
q
The joint coordinates of the solution (ndarray). Note that these
will not be valid if failed to find a solution
will not be valid if failed to find a solution. The shape is (n,) for a
single pose, or (N, n) for a trajectory of N poses
success
True if a valid solution was found
iterations
Expand All @@ -52,6 +53,33 @@ class IKSolution:
``ik_GN`` (which now also return ``IKSolution``, see :meth:`ETS.ik_LM`)
remain compatible with code that indexed the old bare tuple return

.. versionchanged:: 1.5.0
An ``IKSolution`` is now a sequence of the rows of ``q``, one per pose:
``len(sol)`` is the number of poses (1 for a single pose), and
``for q in sol`` and ``sol[i]`` give the joint vector of each pose. This
replaces iterating over the fields, so ``q, success, ... = sol`` and
``sol[1]`` no longer work, use the attributes or :meth:`astuple`. An
``IKSolution`` is true if ``success`` is true. Printing a trajectory
shows the number of poses and abbreviates a long ``q``, and the
residual of a failed solution is no longer rounded to zero.

The solution is a sequence of the rows of ``q``, one per pose.

.. runblock:: pycon
>>> import numpy as np
>>> import roboticstoolbox as rtb
>>> panda = rtb.models.Panda()
>>> T = panda.fkine(panda.qr + np.array([[0], [0.05], [0.1]]))
>>> sol = panda.ikine_LM(T, q0=panda.qr, seed=0)
>>> len(sol)
>>> sol[2]
>>> sol[:2]
>>> bool(sol) == sol.success

A single pose is treated as one row, so ``len(sol)`` is 1 and ``sol[0]`` is
``q``. ``sol[i]`` is the same as ``np.atleast_2d(sol.q)[i]``, it does not
return an ``IKSolution`` since ``success``, ``residual`` and ``reason`` are
values for the whole solution, not for each pose.
"""

q: np.ndarray
Expand All @@ -61,56 +89,113 @@ class IKSolution:
residual: float = 0.0
reason: str = ""

def _rows(self) -> NDArray:
# the joint coordinates as an array with one row per pose
if self.q is None:
return np.empty((0, 0))
return np.atleast_2d(self.q)

def __len__(self) -> int:
"""
Number of poses, the number of rows of ``q``

:returns: 1 for a single pose, N for a trajectory of N poses, 0 if ``q`` is
``None``
"""
return 0 if self.q is None else self._rows().shape[0]

def __iter__(self):
return iter(
(
self.q,
self.success,
self.iterations,
self.searches,
self.residual,
self.reason,
)
)
"""
Iterate over the joint coordinates of each pose

:returns: an iterator over ``q``, one ndarray(n) per pose
"""
return iter(self._rows())

def __getitem__(self, i):
return tuple(self)[i]
"""
Joint coordinates of one or more poses

:param i: an integer, slice or index array, as for a NumPy array
:returns: ``np.atleast_2d(self.q)[i]``, an ndarray(n) for an integer index
:raises IndexError: if the index is out of range
"""
return self._rows()[i]

def __bool__(self) -> bool:
"""
True if the IK solution was successful

:returns: the value of ``success``
"""
return bool(self.success)

def astuple(self) -> tuple:
"""
The fields of the solution as a tuple

:returns: ``(q, success, iterations, searches, residual, reason)``

This is what iterating over an ``IKSolution`` gave before 1.5.0.
"""
return (
self.q,
self.success,
self.iterations,
self.searches,
self.residual,
self.reason,
)

def __repr__(self):
return str(self)

def _q_str(self) -> str | None:
# joint coordinates as text, a trajectory of many poses is abbreviated
if self.q is None:
return None

fmt = {"float": lambda x: "{:.4g}".format(0 if abs(x) < 1e-6 else x)}

def text(a):
return np.array2string(a, separator=", ", formatter=fmt)

if self.q.ndim == 1 or len(self) <= 6:
return text(self.q)

# first three poses, an ellipsis, then the last two
rows = self._rows()
lines = [text(r) for r in rows[:3]] + ["..."] + [text(r) for r in rows[-2:]]
return "[" + ",\n ".join(lines) + "]"

def __str__(self):
if self.q is not None:
q_str = np.array2string(
self.q,
separator=", ",
formatter={
"float": lambda x: "{:.4g}".format(0 if abs(x) < 1e-6 else x)
},
) # np.round(self.q, 4)
else:
q_str = None
analytic = self.iterations == 0 and self.searches == 0

if self.iterations == 0 and self.searches == 0:
# Check for analytic
if self.success:
return f"IKSolution: q={q_str}, success=True"
else:
return f"IKSolution: q={q_str}, success=False, reason={self.reason}"
# everything after q
if self.success:
status = "success=True"
else:
status = f"success=False, reason={self.reason}"
if analytic:
# not iterative, show the residual if one was computed
if self.residual != 0:
status += f", residual={self.residual:.3g}"
else:
# Otherwise it is a numeric solution
if self.success:
return (
f"IKSolution: q={q_str}, success=True,"
status += (
f", iterations={self.iterations}, searches={self.searches},"
f" residual={self.residual:.3g}"
)
if not self.success:
# reason comes before the counts for a failure
status = (
f"success=False, reason={self.reason},"
f" iterations={self.iterations}, searches={self.searches},"
f" residual={self.residual:.3g}"
)
else:
return (
f"IKSolution: q={q_str}, success=False, reason={self.reason},"
f" iterations={self.iterations}, searches={self.searches},"
f" residual={np.round(self.residual, 4):.3g}"
)

if self.q is not None and self.q.ndim == 2 and len(self) > 1:
return f"IKSolution: {len(self)} poses, {status}\nq={self._q_str()}"
return f"IKSolution: q={self._q_str()}, {status}"


class IKSolver(ABC):
Expand Down Expand Up @@ -1437,6 +1522,6 @@ def step(
np.array([1, 2, 3]), success=True, iterations=10, searches=100, residual=0.1
)

a, b, c, d, e = sol
a, b, c, d, e, f = sol.astuple()

print(a, b, c, d, e)
print(a, b, c, d, e, f)
Loading
Loading