Skip to content

Commit 0d4f595

Browse files
committed
Revise Robot API to be unambigous between link frame and CoM
computations.
1 parent 6641da5 commit 0d4f595

File tree

2 files changed

+147
-40
lines changed

2 files changed

+147
-40
lines changed

pyb_utils/robots.py

Lines changed: 114 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import numpy as np
33
import pybullet as pyb
44

5-
from .named_tuples import getJointInfo
5+
from .named_tuples import getJointInfo, getJointStates, getLinkState
6+
from .math import quaternion_to_matrix
67

78

89
class Robot:
@@ -43,9 +44,9 @@ def get_joint_states(self):
4344
A tuple ``(q, v)`` where ``q`` is the robot's joint configuration
4445
and ``v`` is the joint velocity.
4546
"""
46-
states = pyb.getJointStates(self.uid, self._joint_indices)
47-
q = np.array([state[0] for state in states])
48-
v = np.array([state[1] for state in states])
47+
states = getJointStates(self.uid, self._joint_indices)
48+
q = np.array([state.jointPosition for state in states])
49+
v = np.array([state.jointVelocity for state in states])
4950
return q, v
5051

5152
def reset_joint_configuration(self, q):
@@ -79,11 +80,11 @@ def command_velocity(self, u):
7980
targetVelocities=list(u),
8081
)
8182

82-
def get_link_pose(self, link_idx=None):
83-
"""Get the pose of a link.
83+
def get_link_com_pose(self, link_idx=None):
84+
"""Get the pose of a link's center of mass.
8485
85-
The pose is computed about the link's CoM with respect to the world
86-
frame.
86+
The pose is computed about the link's center of mass with respect to
87+
the world frame.
8788
8889
Parameters
8990
----------
@@ -100,15 +101,42 @@ def get_link_pose(self, link_idx=None):
100101
"""
101102
if link_idx is None:
102103
link_idx = self.tool_idx
103-
state = pyb.getLinkState(self.uid, link_idx, computeForwardKinematics=True)
104-
pos, orn = state[4], state[5]
105-
return np.array(pos), np.array(orn)
104+
state = getLinkState(self.uid, link_idx, computeForwardKinematics=True)
105+
return np.array(state.linkWorldPosition), np.array(
106+
state.linkWorldOrientation
107+
)
106108

107-
def get_link_velocity(self, link_idx=None):
108-
"""Get the velocity of a link.
109+
def get_link_frame_pose(self, link_idx=None):
110+
"""Get the pose of a link's URDF frame.
109111
110-
The velocity is computed about the link's CoM with respect to the world
111-
frame.
112+
The pose is computed about the link's parent joint position, which is
113+
its URDF frame.
114+
115+
Parameters
116+
----------
117+
link_idx :
118+
Index of the link to use. If not provided, defaults to the end
119+
effector ``self.tool_idx``.
120+
121+
Returns
122+
-------
123+
:
124+
A tuple containing the position and orientation quaternion of the
125+
link's frame with respect to the world. The quaternion is
126+
represented in xyzw order.
127+
"""
128+
if link_idx is None:
129+
link_idx = self.tool_idx
130+
state = getLinkState(self.uid, link_idx, computeForwardKinematics=True)
131+
return np.array(state.worldLinkFramePosition), np.array(
132+
state.worldLinkFrameOrientation
133+
)
134+
135+
def get_link_com_velocity(self, link_idx=None):
136+
"""Get the velocity of a link's center of mass.
137+
138+
The velocity is computed about the link's center of mass with respect
139+
to the world frame.
112140
113141
Parameters
114142
----------
@@ -124,25 +152,36 @@ def get_link_velocity(self, link_idx=None):
124152
"""
125153
if link_idx is None:
126154
link_idx = self.tool_idx
127-
state = pyb.getLinkState(
155+
state = getLinkState(
156+
self.uid,
157+
link_idx,
158+
computeLinkVelocity=True,
159+
)
160+
v_com = np.array(state.worldLinkLinearVelocity)
161+
ω_com = np.array(state.worldLinkAngularVelocity)
162+
return v_com, ω_com
163+
164+
def get_link_frame_velocity(self, link_idx=None):
165+
if link_idx is None:
166+
link_idx = self.tool_idx
167+
state = getLinkState(
128168
self.uid,
129169
link_idx,
130170
computeLinkVelocity=True,
171+
computeForwardKinematics=True,
131172
)
132-
return np.array(state[-2]), np.array(state[-1])
173+
C = quaternion_to_matrix(state.worldLinkFrameOrientation)
174+
v_com = np.array(state.worldLinkLinearVelocity)
175+
ω_com = np.array(state.worldLinkAngularVelocity)
176+
v = v_com - np.cross(ω_com, C @ state.localInertialFramePosition)
177+
ω = ω_com
178+
return v, ω
133179

134-
def jacobian(self, q=None, link_idx=None, offset=None):
180+
def compute_link_jacobian(self, q=None, link_idx=None, offset=None):
135181
"""Get the Jacobian of a point on a link at the given configuration.
136182
137-
Warning
138-
-------
139-
The Jacobian is computed around the link's parent joint position,
140-
whereas ``get_link_pose`` and ``get_link_velocity`` compute the pose
141-
and velocity around the link's center of mass (CoM). To compute the
142-
Jacobian around the CoM, pass in the ``offset`` of the CoM from the
143-
joint position in the local frame. Alternatively, it may be convenient
144-
to simply design your URDF so that the CoM of the link of interest
145-
coincides with the joint origin.
183+
When ``offset`` is ``None`` or zeros, the Jacobian is computed around
184+
the URDF link frame (i.e., the parent joint position).
146185
147186
See also
148187
https://github.com/bulletphysics/bullet3/issues/2429#issuecomment-538431246.
@@ -180,3 +219,51 @@ def jacobian(self, q=None, link_idx=None, offset=None):
180219
Jv, Jw = pyb.calculateJacobian(self.uid, link_idx, offset, q, z, z)
181220
J = np.vstack((Jv, Jw))
182221
return J
222+
223+
def compute_link_frame_jacobian(self, q=None, link_idx=None):
224+
"""Compute the Jacobian around the URDF link frame.
225+
226+
This is the same as calling ``compute_link_jacobian`` with
227+
``offset=None``.
228+
229+
Parameters
230+
----------
231+
q :
232+
The joint configuration at which to compute the Jacobian. If no
233+
configuration is given, then the current one is used.
234+
link_idx :
235+
The index of the link to compute the Jacobian for. The end effector
236+
link ``self.tool_idx`` is used if not given.
237+
238+
Returns
239+
-------
240+
:
241+
The :math:`6\\times n` Jacobian matrix, where :math:`n` is the
242+
number of joints.
243+
"""
244+
return self.compute_link_jacobian(q=q, link_idx=link_idx, offset=None)
245+
246+
def compute_link_com_jacobian(self, q=None, link_idx=None):
247+
"""Compute the Jacobian around the link's center of mass.
248+
249+
Parameters
250+
----------
251+
q :
252+
The joint configuration at which to compute the Jacobian. If no
253+
configuration is given, then the current one is used.
254+
link_idx :
255+
The index of the link to compute the Jacobian for. The end effector
256+
link ``self.tool_idx`` is used if not given.
257+
258+
Returns
259+
-------
260+
:
261+
The :math:`6\\times n` Jacobian matrix, where :math:`n` is the
262+
number of joints.
263+
"""
264+
if link_idx is None:
265+
link_idx = self.tool_idx
266+
state = getLinkState(self.uid, link_idx)
267+
return self.compute_link_jacobian(
268+
q=q, link_idx=link_idx, offset=state.localInertialFramePosition
269+
)

tests/test_robot.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,33 @@ def test_robot_link_pose():
7171
useFixedBase=True,
7272
)
7373
robot = pyb_utils.Robot(kuka_id)
74-
r0, Q0 = robot.get_link_pose()
74+
r0, Q0 = robot.get_link_frame_pose()
75+
C0 = pyb_utils.quaternion_to_matrix(Q0)
76+
77+
# compare frame and CoM positions
78+
offset = pyb_utils.getLinkState(
79+
robot.uid, robot.tool_idx
80+
).localInertialFramePosition
81+
r0_com = robot.get_link_com_pose()[0]
82+
assert np.allclose(r0_com, r0 + C0 @ offset)
7583

7684
Cz = rotz(np.pi / 2)
7785
Qz = pyb_utils.matrix_to_quaternion(Cz)
7886

7987
robot.reset_joint_configuration([np.pi / 2, 0, 0, 0, 0, 0, 0])
80-
r1, Q1 = robot.get_link_pose()
88+
r1, Q1 = robot.get_link_frame_pose()
89+
C1 = pyb_utils.quaternion_to_matrix(Q1)
8190
assert np.allclose(r0, r1)
8291
assert np.allclose(Q1, pyb_utils.quaternion_multiply(Qz, Q0))
8392

93+
r1_com = robot.get_link_com_pose()[0]
94+
assert np.allclose(r1_com, r1 + C1 @ offset)
95+
8496
robot.reset_joint_configuration([0, np.pi / 2, 0, 0, 0, 0, 0])
85-
r2 = robot.get_link_pose()[0]
97+
r2 = robot.get_link_frame_pose()[0]
8698

8799
robot.reset_joint_configuration([0, -np.pi / 2, 0, 0, 0, 0, 0])
88-
r3 = robot.get_link_pose()[0]
100+
r3 = robot.get_link_frame_pose()[0]
89101

90102
assert np.allclose(r2, [-1, 1, 1] * r3)
91103

@@ -103,14 +115,15 @@ def test_robot_jacobian():
103115
robot.command_velocity(vd)
104116
pyb.stepSimulation()
105117

106-
# get the offset to the CoM of the link
107-
# note that Jacobian is computed around the link's parent joint by default,
108-
# whereas velocity from get_link_velocity is computed around the link's CoM
109-
state = pyb.getLinkState(robot.uid, robot.tool_idx)
110-
offset = state[2]
118+
# URDF frame
119+
J = robot.compute_link_frame_jacobian()
120+
V = np.concatenate(robot.get_link_frame_velocity())
121+
v = robot.get_joint_states()[1]
122+
assert np.allclose(V, J @ v, rtol=0, atol=1e-2)
111123

112-
J = robot.jacobian(offset=offset)
113-
V = np.concatenate(robot.get_link_velocity())
124+
# inertial frame
125+
J = robot.compute_link_com_jacobian()
126+
V = np.concatenate(robot.get_link_com_velocity())
114127
v = robot.get_joint_states()[1]
115128
assert np.allclose(V, J @ v, rtol=0, atol=1e-2)
116129

@@ -121,7 +134,14 @@ def test_robot_jacobian():
121134
robot.command_velocity(vd)
122135
pyb.stepSimulation()
123136

124-
J = robot.jacobian(offset=offset)
125-
V = np.concatenate(robot.get_link_velocity())
137+
# URDF frame
138+
J = robot.compute_link_frame_jacobian()
139+
V = np.concatenate(robot.get_link_frame_velocity())
140+
v = robot.get_joint_states()[1]
141+
assert np.allclose(V, J @ v, rtol=0, atol=1e-2)
142+
143+
# inertial frame
144+
J = robot.compute_link_com_jacobian()
145+
V = np.concatenate(robot.get_link_com_velocity())
126146
v = robot.get_joint_states()[1]
127147
assert np.allclose(V, J @ v, rtol=0, atol=1e-2)

0 commit comments

Comments
 (0)