Skip to content

Commit e575d77

Browse files
authored
Add clamping for SO3 and SE3 (#96)
* PoseConstraintTask implementation * make clamping a function for SO3 and SE3, remove PoseConstraintTask * docstring format fix, typo
1 parent e396cb0 commit e575d77

File tree

6 files changed

+160
-4
lines changed

6 files changed

+160
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
## Unreleased
66

7+
### Added
8+
9+
- `clamp` method for SO3 and SE3.
10+
711
### Changed
812

913
- `solve_ik` now raises a new exception, `NoSolutionFound`, when the QP solver fails to find a solution.

examples/arm_hand_xarm_leap.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ def construct_model() -> mujoco.MjModel:
9999
configuration.update(data.qpos)
100100
posture_task.set_target_from_configuration(configuration)
101101

102+
# Define a rectangular region centered about the origin of the initial
103+
# end-effector pose. This can be used to clamp the end_effector_task target
104+
# to ensure that the end-effector stays within this region.
105+
# Roll and pitch limits are also defined to keep the hand pointed downward.
106+
T_eef_init = configuration.get_transform_frame_to_world(
107+
"attachment_site", "site"
108+
)
109+
translation_init = T_eef_init.translation()
110+
x_limits = (translation_init[0] - 0.1, translation_init[0] + 0.1)
111+
y_limits = (translation_init[1] - 0.1, translation_init[1] + 0.1)
112+
z_limits = (translation_init[2] - 0.1, translation_init[2] + 0.1)
113+
rpy_init = T_eef_init.rotation().as_rpy_radians()
114+
roll_limits = (rpy_init.roll - 0.2, rpy_init.roll + 0.2)
115+
pitch_limits = (rpy_init.pitch - 0.2, rpy_init.pitch + 0.2)
116+
102117
# Initialize the mocap target at the end-effector site.
103118
mink.move_mocap_to_frame(model, data, "target", "attachment_site", "site")
104119
for finger in fingers:
@@ -112,9 +127,16 @@ def construct_model() -> mujoco.MjModel:
112127

113128
rate = RateLimiter(frequency=200.0, warn=False)
114129
while viewer.is_running():
115-
# Update kuka end-effector task.
130+
# Clamp the target within the limits and then update the end-effector task.
116131
T_wt = mink.SE3.from_mocap_name(model, data, "target")
117-
end_effector_task.set_target(T_wt)
132+
T_wt_clamped = T_wt.clamp(
133+
x_translation=x_limits,
134+
y_translation=y_limits,
135+
z_translation=z_limits,
136+
roll_radians=roll_limits,
137+
pitch_radians=pitch_limits,
138+
)
139+
end_effector_task.set_target(T_wt_clamped)
118140

119141
# Update finger tasks.
120142
for finger, task in zip(fingers, finger_tasks):

mink/lie/se3.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from typing import Tuple
45

56
import mujoco
67
import numpy as np
@@ -221,6 +222,36 @@ def ljacinv(cls, other: np.ndarray) -> np.ndarray:
221222
ljacinv_se3[3:, 3:] = ljacinv_so3
222223
return ljacinv_se3
223224

225+
def clamp(
226+
self,
227+
x_translation: Tuple[float, float] = (-np.inf, np.inf),
228+
y_translation: Tuple[float, float] = (-np.inf, np.inf),
229+
z_translation: Tuple[float, float] = (-np.inf, np.inf),
230+
roll_radians: Tuple[float, float] = (-np.inf, np.inf),
231+
pitch_radians: Tuple[float, float] = (-np.inf, np.inf),
232+
yaw_radians: Tuple[float, float] = (-np.inf, np.inf),
233+
) -> SE3:
234+
"""Clamp a SE3 within translation and RPY limits.
235+
236+
Args:
237+
x_translation: The (lower, upper) limits for translation along the x axis.
238+
y_translation: The (lower, upper) limits for translation along the y axis.
239+
z_translation: The (lower, upper) limits for translation along the z axis.
240+
roll_radians: The (lower, upper) limits for the roll.
241+
pitch_radians: The (lower, upper) limits for the pitch.
242+
yaw_radians: The (lower, upper) limits for the yaw.
243+
244+
Returns:
245+
A SE3 within the translation and RPY limits.
246+
"""
247+
translation_limits = np.array([x_translation, y_translation, z_translation])
248+
return SE3.from_rotation_and_translation(
249+
rotation=self.rotation().clamp(roll_radians, pitch_radians, yaw_radians),
250+
translation=np.clip(
251+
self.translation(), translation_limits[:, 0], translation_limits[:, 1]
252+
),
253+
)
254+
224255

225256
# Eqn 180.
226257
def _getQ(c) -> np.ndarray:

mink/lie/so3.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4-
from typing import NamedTuple
4+
from typing import NamedTuple, Tuple
55

66
import mujoco
77
import numpy as np
@@ -260,3 +260,25 @@ def ljacinv(cls, other: np.ndarray) -> np.ndarray:
260260
ljacinv[1, 1] += 1.0
261261
ljacinv[2, 2] += 1.0
262262
return ljacinv
263+
264+
def clamp(
265+
self,
266+
roll_radians: Tuple[float, float] = (-np.inf, np.inf),
267+
pitch_radians: Tuple[float, float] = (-np.inf, np.inf),
268+
yaw_radians: Tuple[float, float] = (-np.inf, np.inf),
269+
) -> SO3:
270+
"""Clamp a SO3 within RPY limits.
271+
272+
Args:
273+
roll_radians: The (lower, upper) limits for the roll.
274+
pitch_radians: The (lower, upper) limits for the pitch.
275+
yaw_radians: The (lower, upper) limits for the yaw.
276+
277+
Returns:
278+
A SO3 within the RPY limits.
279+
"""
280+
return SO3.from_rpy_radians(
281+
roll=np.clip(self.compute_roll_radians(), *roll_radians),
282+
pitch=np.clip(self.compute_pitch_radians(), *pitch_radians),
283+
yaw=np.clip(self.compute_yaw_radians(), *yaw_radians),
284+
)

mink/tasks/frame_task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class FrameTask(Task):
2020
frame_name: Name of the frame to regulate, typically the name of body, geom
2121
or site in the robot model.
2222
frame_type: The frame type: `body`, `geom` or `site`.
23-
transform_frame_to_world: Target pose of the frame in the world frame.
23+
transform_target_to_world: Target pose of the frame in the world frame.
2424
2525
Example:
2626

tests/test_lie_operations.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,34 @@ def test_so3_apply_throws_assertion_error_if_wrong_shape(self):
148148
with self.assertRaises(AssertionError):
149149
rot.apply(vec)
150150

151+
def test_so3_clamp(self):
152+
# Clamping with the default RPY limits (+- infinity) means the SO3 should remain unchanged.
153+
rot = SO3.from_rpy_radians(roll=np.pi, pitch=0, yaw=-np.pi)
154+
self.assertEqual(rot, rot.clamp())
155+
156+
original_rpy = rot.as_rpy_radians()
157+
158+
# Test clamping the roll.
159+
clamped_rot = rot.clamp(roll_radians=(0, 1))
160+
clamped_rpy = clamped_rot.as_rpy_radians()
161+
self.assertAlmostEqual(clamped_rpy.roll, 1)
162+
self.assertAlmostEqual(clamped_rpy.pitch, original_rpy.pitch)
163+
self.assertAlmostEqual(clamped_rpy.yaw, original_rpy.yaw)
164+
165+
# Test clamping the pitch.
166+
clamped_rot = rot.clamp(pitch_radians=(1, 2))
167+
clamped_rpy = clamped_rot.as_rpy_radians()
168+
self.assertAlmostEqual(clamped_rpy.roll, original_rpy.roll)
169+
self.assertAlmostEqual(clamped_rpy.pitch, 1)
170+
self.assertAlmostEqual(clamped_rpy.yaw, original_rpy.yaw)
171+
172+
# Test clamping the yaw.
173+
clamped_rot = rot.clamp(yaw_radians=(np.pi, 2 * np.pi))
174+
clamped_rpy = clamped_rot.as_rpy_radians()
175+
self.assertAlmostEqual(clamped_rpy.roll, original_rpy.roll)
176+
self.assertAlmostEqual(clamped_rpy.pitch, original_rpy.pitch)
177+
self.assertAlmostEqual(clamped_rpy.yaw, np.pi)
178+
151179
# SE3.
152180

153181
def test_se3_equality(self):
@@ -257,6 +285,55 @@ def test_se3_interpolate(self):
257285
start.interpolate(end, alpha=2.0)
258286
self.assertIn(expected_error_message, str(cm.exception))
259287

288+
def test_se3_clamp(self):
289+
T = SE3.from_rotation_and_translation(
290+
rotation=SO3.from_rpy_radians(roll=np.pi, pitch=0, yaw=-np.pi),
291+
translation=np.array([0, 1, 2]),
292+
)
293+
original_rpy = T.rotation().as_rpy_radians()
294+
295+
# Clamping with the default limits (+- infinity) means the SE3 should remain unchanged.
296+
self.assertEqual(T, T.clamp())
297+
298+
# Test clamping the x translation.
299+
clamped_T = T.clamp(x_translation=(1, 2))
300+
np.testing.assert_allclose(clamped_T.translation(), np.array([1, 1, 2]))
301+
self.assertEqual(clamped_T.rotation(), T.rotation())
302+
303+
# Test clamping the y translation.
304+
clamped_T = T.clamp(y_translation=(-1, 0))
305+
np.testing.assert_allclose(clamped_T.translation(), np.array([0, 0, 2]))
306+
self.assertEqual(clamped_T.rotation(), T.rotation())
307+
308+
# Test clamping the z translation.
309+
clamped_T = T.clamp(z_translation=(5, 10))
310+
np.testing.assert_allclose(clamped_T.translation(), np.array([0, 1, 5]))
311+
self.assertEqual(clamped_T.rotation(), T.rotation())
312+
313+
# Test clamping the roll.
314+
clamped_T = T.clamp(roll_radians=(0, 1))
315+
clamped_rpy = clamped_T.rotation().as_rpy_radians()
316+
np.testing.assert_equal(clamped_T.translation(), T.translation())
317+
self.assertAlmostEqual(clamped_rpy.roll, 1)
318+
self.assertAlmostEqual(clamped_rpy.pitch, original_rpy.pitch)
319+
self.assertAlmostEqual(clamped_rpy.yaw, original_rpy.yaw)
320+
321+
# Test clamping the pitch.
322+
clamped_T = T.clamp(pitch_radians=(1, 2))
323+
clamped_rpy = clamped_T.rotation().as_rpy_radians()
324+
np.testing.assert_equal(clamped_T.translation(), T.translation())
325+
self.assertAlmostEqual(clamped_rpy.roll, original_rpy.roll)
326+
self.assertAlmostEqual(clamped_rpy.pitch, 1)
327+
self.assertAlmostEqual(clamped_rpy.yaw, original_rpy.yaw)
328+
329+
# Test clamping the yaw.
330+
clamped_T = T.clamp(yaw_radians=(np.pi, 2 * np.pi))
331+
clamped_rpy = clamped_T.rotation().as_rpy_radians()
332+
np.testing.assert_equal(clamped_T.translation(), T.translation())
333+
self.assertAlmostEqual(clamped_rpy.roll, original_rpy.roll)
334+
self.assertAlmostEqual(clamped_rpy.pitch, original_rpy.pitch)
335+
self.assertAlmostEqual(clamped_rpy.yaw, np.pi)
336+
260337

261338
if __name__ == "__main__":
262339
absltest.main()

0 commit comments

Comments
 (0)