Skip to content

Commit 3f43b32

Browse files
committed
add pybullet fk and ik
1 parent 5163c56 commit 3f43b32

File tree

11 files changed

+316
-118
lines changed

11 files changed

+316
-118
lines changed

docs/examples/05_backends_pybullet/01_pybullet_examples.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Adding and removing a collision mesh attached to the end effector link of the
5151
robot is similar. Again, the calls to ``sleep`` and ``step_simulation`` exist only
5252
to make the GUI rendering smoother.
5353

54-
.. literalinclude :: files/02_add_attached_collision_mesh.py
54+
.. literalinclude :: files/01_add_attached_collision_mesh.py
5555
:language: python
5656
5757
.. raw:: html
@@ -60,7 +60,7 @@ to make the GUI rendering smoother.
6060
<div class="card-body">
6161
<div class="card-title">Downloads</div>
6262

63-
* :download:`Add Attached Collision Mesh (.PY) <files/02_add_attached_collision_mesh.py>`
63+
* :download:`Add Attached Collision Mesh (.PY) <files/01_add_attached_collision_mesh.py>`
6464

6565
.. raw:: html
6666

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
*******************************************************************************
2+
Forward and inverse kinematics
3+
*******************************************************************************
4+
5+
Forward kinematics
6+
==================
7+
8+
As with ROS, the forward kinematics function calculates the pose of the robot's
9+
end-effector from joint states. This means the state of each joint in the
10+
articulated body of a robot needs to be defined.
11+
12+
Joint states are described in **COMPAS FAB** with the
13+
:class:`compas_fab.robots.Configuration` class.
14+
15+
Below we demonstrate calculating the forward kinematics for a UR5 robot.
16+
(Note: Since the PyBullet server has the capacity to load multiple robots,
17+
which robot the forward kinematics are being calculated for must be specified.)
18+
19+
.. literalinclude :: files/02_forward_kinematics.py
20+
:language: python
21+
22+
23+
Inverse kinematics
24+
==================
25+
26+
Inverse kinematics is the inverse function of forward kinematics. The
27+
inverse kinematics function calculates the joint states required for the
28+
end-effector to reach a certain target pose.
29+
30+
Here is an example of such a calculation using PyBullet:
31+
32+
.. literalinclude :: files/02_inverse_kinematics.py
33+
:language: python

docs/examples/05_backends_pybullet/files/02_add_attached_collision_mesh.py renamed to docs/examples/05_backends_pybullet/files/01_add_attached_collision_mesh.py

File renamed without changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import compas_fab
2+
3+
from compas_fab.backends import PyBulletClient
4+
from compas_fab.robots import Configuration
5+
6+
with PyBulletClient() as client:
7+
urdf_filename = compas_fab.get('universal_robot/ur_description/urdf/ur5.urdf')
8+
robot = client.load_robot(urdf_filename)
9+
10+
configuration = Configuration.from_revolute_values([-2.238, -1.153, -2.174, 0.185, 0.667, 0.])
11+
options = {'robot': robot}
12+
13+
frame_WCF = client.forward_kinematics(configuration, options=options)
14+
15+
print("Frame in the world coordinate system")
16+
print(frame_WCF)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import compas_fab
2+
from compas.geometry import Frame
3+
from compas_fab.backends import PyBulletClient
4+
5+
with PyBulletClient() as client:
6+
urdf_filename = compas_fab.get('universal_robot/ur_description/urdf/ur5.urdf')
7+
robot = client.load_robot(urdf_filename)
8+
9+
frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0])
10+
start_configuration = robot.zero_configuration()
11+
options = {'robot': robot}
12+
13+
configuration = client.inverse_kinematics(frame_WCF, start_configuration, options=options)
14+
15+
print("Found configuration", configuration)

src/compas_fab/backends/pybullet/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
:toctree: generated/
1212
1313
PyBulletClient
14+
PyBulletError
15+
CollisionError
16+
InverseKinematicsError
1417
PyBulletPlanner
1518
1619
"""

src/compas_fab/backends/pybullet/backend_features/pybullet_forward_kinematics.py

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,43 @@ def __init__(self, client):
1111
self.client = client
1212

1313
def forward_kinematics(self, configuration, group=None, options=None):
14-
# """Calculate the robot's forward kinematic.
15-
#
16-
# Parameters
17-
# ----------
18-
# configuration : :class:`compas_fab.robots.Configuration`
19-
# The full configuration to calculate the forward kinematic for. If no
20-
# full configuration is passed, the zero-joint state for the other
21-
# configurable joints is assumed.
22-
# group : str, optional
23-
# Unused parameter.
24-
# options : dict, optional
25-
# Dictionary containing the following key-value pairs:
26-
#
27-
# - ``"base_link"``: (:obj:`str`) The name of the base link.
28-
# - ``"ee_link"``: (:obj:`str`, optional) The name of the link to
29-
# calculate the forward kinematics for. Defaults to the group's end
30-
# effector link.
31-
#
32-
# Returns
33-
# -------
34-
# :class:`Frame`
35-
# The frame in the world's coordinate system (WCF).
36-
# """
37-
raise NotImplementedError
38-
# """
39-
# """
40-
# ee_link_name = options['ee_link_name']
41-
# joints = self.client.joints_from_names(self.client.robot_uid, configuration.joint_names)
42-
# ee_link = self.client.link_from_name(self.client.robot_uid, ee_link_name)
43-
# self.client.set_joint_positions(self.client.robot_uid, joints, configuration.values)
44-
# pose = self.client.get_link_pose(self.client.robot_uid, ee_link)
45-
# if options.get('check_collision'):
46-
# collision, names = self.client.client.collision_check()
47-
# if collision:
48-
# raise CollisionError(*names)
49-
# return frame_from_pose(pose)
14+
"""Calculate the robot's forward kinematic.
15+
16+
Parameters
17+
----------
18+
configuration : :class:`compas_fab.robots.Configuration`
19+
The full configuration to calculate the forward kinematic for. If no
20+
full configuration is passed, the zero-joint state for the other
21+
configurable joints is assumed.
22+
group : str, optional
23+
The planning group used for calculation. Defaults to the robot's
24+
main planning group.
25+
options : dict, optional
26+
Dictionary containing the following key-value pairs:
27+
28+
- ``"robot"``: (:class:`compas_fab.robots.Robot`) The robot whose
29+
kinematics are to be calculated.
30+
- ``"link"``: (:obj:`str`, optional) The name of the link to
31+
calculate the forward kinematics for. Defaults to the end effector.
32+
- ``"check_collision"``: (:obj:`str`, optional) When ``True``,
33+
:meth:`compas.pybullet.PyBulletClient.check_collisions` will be called.
34+
Defaults to ``False``.
35+
36+
Returns
37+
-------
38+
:class:`Frame`
39+
The frame in the world's coordinate system (WCF).
40+
"""
41+
robot = options['robot']
42+
# Joint names, but not link names, are recoverable from the pybullet server.
43+
# My intention is that a user doesn't have to care about pybullet's internal id system,
44+
# except for maybe the robot's uid. I would put just the robot uid in the options
45+
# (rather than the full robot)
46+
# if it were possible to recover the links, but it's not.
47+
link_name = options.get('link') or robot.get_end_effector_link_name(group)
48+
link_id = self.client._get_link_id_by_name(link_name, robot)
49+
self.client.set_robot_configuration(robot, configuration, group)
50+
frame = self.client._get_link_frame(link_id, robot.pybullet_uid)
51+
if options.get('check_collision'):
52+
self.client.collision_check()
53+
return frame

src/compas_fab/backends/pybullet/backend_features/pybullet_inverse_kinematics.py

Lines changed: 115 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@
22
from __future__ import division
33
from __future__ import print_function
44

5+
import math
6+
import sys
7+
8+
from compas.robots import Joint
59
from compas_fab.backends.interfaces import InverseKinematics
10+
from compas_fab.backends.pybullet.conversions import pose_from_frame
11+
from compas_fab.backends.pybullet.exceptions import InverseKinematicsError
12+
from compas_fab.robots import Configuration
13+
from compas_fab.utilities import LazyLoader
14+
15+
pybullet = LazyLoader('pybullet', globals(), 'pybullet')
16+
617

718
__all__ = [
819
'PyBulletInverseKinematics',
@@ -15,46 +26,107 @@ def __init__(self, client):
1526
self.client = client
1627

1728
def inverse_kinematics(self, frame_WCF, start_configuration=None, group=None, options=None):
18-
# """Calculate the robot's inverse kinematic for a given frame.
19-
#
20-
# Parameters
21-
# ----------
22-
# frame_WCF: :class:`compas.geometry.Frame`
23-
# The frame to calculate the inverse for.
24-
# start_configuration: :class:`compas_fab.robots.Configuration`, optional
25-
# If passed, the inverse will be calculated such that the calculated
26-
# joint positions differ the least from the start_configuration.
27-
# Defaults to the init configuration.
28-
# group: str, optional
29-
# The planning group used for calculation. Defaults to the robot's
30-
# main planning group.
31-
# options: dict, optional
32-
# Dictionary containing the following key-value pairs:
33-
#
34-
# - ``"base_link"``: (:obj:`str`) Name of the base link.
35-
# - ``"avoid_collisions"``: (:obj:`bool`, optional) Whether or not to avoid collisions.
36-
# Defaults to `True`.
37-
# - ``"constraints"``: (:obj:`list` of :class:`compas_fab.robots.Constraint`, optional)
38-
# A set of constraints that the request must obey. Defaults to `None`.
39-
# - ``"attempts"``: (:obj:`int`, optional) The maximum number of inverse kinematic attempts.
40-
# Defaults to `8`.
41-
# - ``"attached_collision_meshes"``: (:obj:`list` of :class:`compas_fab.robots.AttachedCollisionMesh`, optional)
42-
# Defaults to `None`.
43-
#
44-
# Returns
45-
# -------
46-
# :class:`compas_fab.robots.Configuration`
47-
# The planning group's configuration.
48-
# """
49-
raise NotImplementedError
50-
# """
51-
# """
52-
# options = options or {}
53-
# self.ensure_robot()
54-
# ee_link_name = options.get('ee_link_name')
55-
# pb_ee_link = self.client.link_from_name(self.client.robot_uid, ee_link_name)
56-
# joint_names = [name.decode('UTF-8') for name in self.client.get_joint_names(self.robot_uid, self.client.get_movable_joints(self.client.robot_uid))]
57-
# joint_positions = inverse_kinematics(self.client.robot_uid, pb_ee_link, self.client.pose_from_frame(frame), max_iterations=attempts)
58-
# if not joint_positions:
59-
# raise InverseKinematicsError()
60-
# return joint_positions, joint_names
29+
"""Calculate the robot's inverse kinematic for a given frame.
30+
31+
Parameters
32+
----------
33+
frame_WCF: :class:`compas.geometry.Frame`
34+
The frame to calculate the inverse for.
35+
start_configuration: :class:`compas_fab.robots.Configuration`, optional
36+
If passed, the inverse will be calculated such that the calculated
37+
joint positions differ the least from the start_configuration.
38+
Defaults to the init configuration.
39+
group: str, optional
40+
The planning group used for calculation. Defaults to the robot's
41+
main planning group.
42+
options: dict, optional
43+
Dictionary containing the following key-value pairs:
44+
45+
-``"robot"``: (:class:`compas_fab.robots.Robot) Robot for which to compute
46+
the inverse kinematics.
47+
- ``"link_name"``: (:obj:`str`, optional ) Name of the link for which
48+
to compute the inverse kinematics. Defaults to the given robot's end
49+
effector.
50+
- ``"semi-constrained"``: (:obj:`bool`, optional) When ``True``, only the
51+
position and not the orientation of ``frame_WCF`` will not be considered
52+
in the calculation. Defaults to ``False``.
53+
- ``"enforce_joint_limits"``: (:obj:`bool`, optional) When ``False``, the
54+
robot's joint limits will be ignored in the calculation. Defaults to
55+
``True``.
56+
57+
Returns
58+
-------
59+
:class:`compas_fab.robots.Configuration`
60+
The planning group's configuration.
61+
62+
Raises
63+
------
64+
:class:`compas_fab.backends.pybullet.InverseKinematicsError`
65+
"""
66+
robot = options['robot']
67+
link_name = options.get('link_name') or robot.get_end_effector_link_name(group)
68+
link_id = self.client._get_link_id_by_name(link_name, robot)
69+
point, orientation = pose_from_frame(frame_WCF)
70+
71+
joints = robot.get_configurable_joints()
72+
joints.sort(key=lambda j: j.attr['pybullet']['id'])
73+
joint_names = [joint.name for joint in joints]
74+
joint_types = [joint.type for joint in joints]
75+
76+
if start_configuration:
77+
start_configuration = self.client.set_robot_configuration(robot, start_configuration, group)
78+
79+
called_from_test = 'pytest' in sys.modules
80+
if options.get('enforce_joint_limits', True) and not called_from_test:
81+
82+
lower_limits = [joint.limit.lower if joint.type != Joint.CONTINUOUS else 0 for joint in joints]
83+
upper_limits = [joint.limit.upper if joint.type != Joint.CONTINUOUS else 2 * math.pi for joint in joints]
84+
# I don't know what jointRanges needs to be. Erwin Coumans knows, but he isn't telling.
85+
# https://stackoverflow.com/questions/49674179/understanding-inverse-kinematics-pybullet
86+
# https://docs.google.com/document/d/10sXEhzFRSnvFcl3XxNGhnD4N2SedqwdAvK3dsihxVUA/preview?pru=AAABc7276PI*zazLer2rlZ8tAUI8lF98Kw#heading=h.9i02ojf4k3ve
87+
joint_ranges = [u - l for u, l in zip(upper_limits, lower_limits)]
88+
rest_configuration = start_configuration or robot.zero_configuration()
89+
rest_poses = self._get_rest_poses(joint_names, rest_configuration)
90+
91+
if options.get('semi-constrained'):
92+
joint_positions = pybullet.calculateInverseKinematics(
93+
robot.pybullet_uid,
94+
link_id,
95+
point,
96+
lowerLimits=lower_limits,
97+
upperLimits=upper_limits,
98+
jointRanges=joint_ranges,
99+
restPoses=rest_poses,
100+
)
101+
else:
102+
joint_positions = pybullet.calculateInverseKinematics(
103+
robot.pybullet_uid,
104+
link_id,
105+
point,
106+
orientation,
107+
lowerLimits=lower_limits,
108+
upperLimits=upper_limits,
109+
jointRanges=joint_ranges,
110+
restPoses=rest_poses,
111+
)
112+
else:
113+
if options.get('semi-constrained'):
114+
joint_positions = pybullet.calculateInverseKinematics(
115+
robot.pybullet_uid,
116+
link_id,
117+
point,
118+
)
119+
else:
120+
joint_positions = pybullet.calculateInverseKinematics(
121+
robot.pybullet_uid,
122+
link_id,
123+
point,
124+
orientation,
125+
)
126+
if not joint_positions:
127+
raise InverseKinematicsError()
128+
return Configuration(joint_positions, joint_types, joint_names)
129+
130+
def _get_rest_poses(self, joint_names, configuration):
131+
name_value_map = {configuration.joint_names[i]: configuration.values[i] for i in range(len(configuration.joint_names))}
132+
return [name_value_map[name] for name in joint_names]

0 commit comments

Comments
 (0)