Skip to content

Commit 9e01180

Browse files
authored
Add translation_tolerance and rotation_tolerance to inverse_kinematics (#636)
- Add translation_tolerance parameter to allow per-axis position tolerance from target in target's local coordinate frame - Add rotation_tolerance parameter to allow per-axis rotation tolerance from target orientation - Support both inverse_kinematics and batch_inverse_kinematics methods - Add --translation-tolerance and --rotation-tolerance CLI options to batch_ik_demo.py and pr2_inverse_kinematics.py examples - Add unit tests for tolerance parameters - Fix rotation error calculation in batch IK convergence check
1 parent c558d81 commit 9e01180

File tree

4 files changed

+300
-26
lines changed

4 files changed

+300
-26
lines changed

examples/batch_ik_demo.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ def main():
4545
help='Position error threshold in meters. Default: 0.001')
4646
parser.add_argument('--rthre', type=float, default=1.0,
4747
help='Rotation error threshold in degrees. Default: 1.0')
48+
parser.add_argument('--translation-tolerance', '--translation_tolerance',
49+
type=float, nargs=3, default=None, metavar=('X', 'Y', 'Z'),
50+
help='Translation tolerance per axis in meters (e.g., 0.05 0.05 0.05)')
51+
parser.add_argument('--rotation-tolerance', '--rotation_tolerance',
52+
type=float, nargs=3, default=None, metavar=('R', 'P', 'Y'),
53+
help='Rotation tolerance per axis in degrees (e.g., 10 10 10)')
4854
parser.add_argument('--no-interactive', action='store_true',
4955
help='Disable interactive visualization')
5056
parser.add_argument('--viewer', type=str,
@@ -59,6 +65,12 @@ def main():
5965
rotation_axis = parse_axis_constraint(args.rotation_axis)
6066
translation_axis = parse_axis_constraint(args.translation_axis)
6167

68+
# Process tolerance arguments
69+
translation_tolerance = args.translation_tolerance
70+
rotation_tolerance = None
71+
if args.rotation_tolerance is not None:
72+
rotation_tolerance = [np.deg2rad(v) for v in args.rotation_tolerance]
73+
6274
print("ADVANCED BATCH IK - WITH AXIS CONSTRAINTS")
6375
print("=" * 55)
6476
print("Configuration:")
@@ -71,6 +83,10 @@ def main():
7183
print(f" Max iterations: {args.stop}")
7284
print(f" Position threshold: {args.thre}m")
7385
print(f" Rotation threshold: {args.rthre} degrees")
86+
if translation_tolerance is not None:
87+
print(f" Translation tolerance: {translation_tolerance}m")
88+
if args.rotation_tolerance is not None:
89+
print(f" Rotation tolerance: {args.rotation_tolerance} degrees")
7490

7591
# Initialize robot based on selection
7692
if args.robot == 'fetch':
@@ -152,6 +168,10 @@ def main():
152168
'rthre': np.deg2rad(args.rthre),
153169
'attempts_per_pose': args.attempts_per_pose,
154170
}
171+
if translation_tolerance is not None:
172+
ik_kwargs['translation_tolerance'] = translation_tolerance
173+
if rotation_tolerance is not None:
174+
ik_kwargs['rotation_tolerance'] = rotation_tolerance
155175

156176
# For robots with inverse_kinematics_defaults, use those settings
157177
if hasattr(arm, 'inverse_kinematics_defaults'):
@@ -362,9 +382,9 @@ def main():
362382
print(f"Visualization error: {e}")
363383

364384
else:
365-
print("No successful solutions to visualize")
385+
print("Interactive visualization skipped (--no-interactive)")
366386
else:
367-
print("Interactive visualization skipped (--no-interactive)")
387+
print("No successful solutions to verify")
368388

369389

370390
if __name__ == '__main__':

examples/pr2_inverse_kinematics.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,24 @@ def main():
165165
action='store_true',
166166
help="Skip the revert_if_fail demonstration"
167167
)
168+
parser.add_argument(
169+
'--translation-tolerance', '--translation_tolerance',
170+
type=float, nargs=3, default=None, metavar=('X', 'Y', 'Z'),
171+
help='Translation tolerance per axis in meters (e.g., 0.05 0.05 0.05)'
172+
)
173+
parser.add_argument(
174+
'--rotation-tolerance', '--rotation_tolerance',
175+
type=float, nargs=3, default=None, metavar=('R', 'P', 'Y'),
176+
help='Rotation tolerance per axis in degrees (e.g., 10 10 10)'
177+
)
168178
args = parser.parse_args()
169179

180+
# Process tolerance arguments
181+
translation_tolerance = args.translation_tolerance
182+
rotation_tolerance = None
183+
if args.rotation_tolerance is not None:
184+
rotation_tolerance = [np.deg2rad(v) for v in args.rotation_tolerance]
185+
170186
# Create robot model
171187
robot_model = skrobot.models.PR2()
172188
robot_model.reset_pose()
@@ -196,6 +212,10 @@ def main():
196212
# Define target position
197213
target_pos = [0.7, -0.2, 0.8]
198214
print("Solving inverse kinematics for target: {}".format(target_pos))
215+
if translation_tolerance is not None:
216+
print("Translation tolerance: {}m".format(translation_tolerance))
217+
if args.rotation_tolerance is not None:
218+
print("Rotation tolerance: {} degrees".format(args.rotation_tolerance))
199219

200220
# Create target coordinates
201221
target_coords = skrobot.coordinates.Coordinates(target_pos, [0, 0, 0])
@@ -211,22 +231,23 @@ def main():
211231
viewer.redraw()
212232
print("Target position visualized with coordinate frame")
213233

234+
# Build IK kwargs
235+
ik_kwargs = {
236+
'link_list': link_list,
237+
'move_target': robot_model.rarm_end_coords,
238+
'rotation_axis': True,
239+
}
240+
if translation_tolerance is not None:
241+
ik_kwargs['translation_tolerance'] = translation_tolerance
242+
if rotation_tolerance is not None:
243+
ik_kwargs['rotation_tolerance'] = rotation_tolerance
244+
214245
# Solve inverse kinematics with optional visualization
215246
if not args.no_ik_visualization:
216247
with ik_visualization(viewer, sleep_time=0.5):
217-
result = robot_model.inverse_kinematics(
218-
target_coords,
219-
link_list=link_list,
220-
move_target=robot_model.rarm_end_coords,
221-
rotation_axis=True
222-
)
248+
result = robot_model.inverse_kinematics(target_coords, **ik_kwargs)
223249
else:
224-
result = robot_model.inverse_kinematics(
225-
target_coords,
226-
link_list=link_list,
227-
move_target=robot_model.rarm_end_coords,
228-
rotation_axis=True
229-
)
250+
result = robot_model.inverse_kinematics(target_coords, **ik_kwargs)
230251

231252
# Check if result is successful (could be False or an array)
232253
success = result is not False and result is not None

skrobot/model/robot_model.py

Lines changed: 149 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,44 @@ def inverse_kinematics(
980980
check_collision=None,
981981
additional_jacobi=None,
982982
additional_vel=None,
983+
translation_tolerance=None,
984+
rotation_tolerance=None,
983985
**kwargs):
986+
"""Solve inverse kinematics to reach target coordinates.
987+
988+
Parameters
989+
----------
990+
target_coords : skrobot.coordinates.Coordinates or list
991+
Target coordinate(s) for the end-effector(s).
992+
stop : int
993+
Maximum number of iterations.
994+
link_list : list
995+
List of links to use for IK.
996+
move_target : skrobot.coordinates.Coordinates or list
997+
End-effector coordinate frame(s).
998+
revert_if_fail : bool
999+
If True, revert to initial joint angles on failure.
1000+
rotation_axis : bool, str, or list
1001+
Which rotation axes to constrain. True=all, False=none,
1002+
'x'/'y'/'z'=single axis, 'xy'/'yz'/'zx'=two axes.
1003+
translation_axis : bool, str, or list
1004+
Which translation axes to constrain. Same format as rotation_axis.
1005+
translation_tolerance : list or None
1006+
Per-axis position tolerance from target as [x_tol, y_tol, z_tol]
1007+
in meters. If error on an axis is within tolerance, it's treated
1008+
as reached. Use None for no tolerance on an axis.
1009+
rotation_tolerance : list or None
1010+
Per-axis rotation tolerance from target as
1011+
[roll_tol, pitch_tol, yaw_tol] in radians. If error on an axis
1012+
is within tolerance, it's treated as reached.
1013+
**kwargs
1014+
Additional keyword arguments passed to inverse_kinematics_loop.
1015+
1016+
Returns
1017+
-------
1018+
numpy.ndarray or False
1019+
Joint angle vector if successful, False otherwise.
1020+
"""
9841021
additional_jacobi = additional_jacobi or []
9851022
additional_vel = additional_vel or []
9861023
target_coords = listify(target_coords)
@@ -1079,11 +1116,37 @@ def inverse_kinematics(
10791116
dif_pos = list(map(lambda mv, tc, trans_axis:
10801117
mv.difference_position(
10811118
tc, translation_axis=trans_axis),
1082-
move_target, target_coords, translation_axis))
1119+
move_target, target_coords,
1120+
translation_axis))
10831121
dif_rot = list(map(lambda mv, tc, rot_axis:
10841122
mv.difference_rotation(
10851123
tc, rotation_axis=rot_axis),
1086-
move_target, target_coords, rotation_axis))
1124+
move_target, target_coords,
1125+
rotation_axis))
1126+
1127+
# Apply translation tolerance in target_coords' local frame
1128+
# Note: tol > 0 check ensures 0.0 means "no special tolerance"
1129+
if translation_tolerance is not None:
1130+
for i, (mt, tc) in enumerate(zip(move_target, target_coords)):
1131+
# Calculate error in target_coords' local frame
1132+
world_error = tc.worldpos() - mt.worldpos()
1133+
local_error = tc.worldrot().T @ world_error
1134+
for axis_idx, tol in enumerate(translation_tolerance):
1135+
if tol is not None and tol > 0:
1136+
if abs(local_error[axis_idx]) <= tol:
1137+
# Zero out the corresponding dif_pos component
1138+
# dif_pos is in move_target's frame, so transform
1139+
target_axis_in_mt = mt.worldrot().T @ tc.worldrot()[:, axis_idx]
1140+
dif_pos[i] -= (dif_pos[i] @ target_axis_in_mt) * target_axis_in_mt
1141+
1142+
# Apply rotation tolerance: if error is within tolerance,
1143+
# treat as reached (set dif to 0)
1144+
if rotation_tolerance is not None:
1145+
for i, dr in enumerate(dif_rot):
1146+
for axis_idx, tol in enumerate(rotation_tolerance):
1147+
if tol is not None and tol > 0 and abs(dr[axis_idx]) <= tol:
1148+
dif_rot[i][axis_idx] = 0.0
1149+
10871150
if loop == 1 and self.ik_convergence_check(
10881151
dif_pos, dif_rot, rotation_axis, translation_axis,
10891152
thre, rthre, centroid_thre, target_centroid_pos,
@@ -2434,6 +2497,8 @@ def batch_inverse_kinematics(
24342497
alpha=1.0,
24352498
attempts_per_pose=1,
24362499
random_initial_range=0.7,
2500+
translation_tolerance=None,
2501+
rotation_tolerance=None,
24372502
**kwargs):
24382503
"""Solve batch inverse kinematics for multiple target poses.
24392504
@@ -2474,6 +2539,14 @@ def batch_inverse_kinematics(
24742539
Number of attempts with different random initial poses per target (default: 1)
24752540
random_initial_range : float
24762541
Range for random initial poses as fraction of joint limits (0.0-1.0, default: 0.7)
2542+
translation_tolerance : list or None
2543+
Per-axis position tolerance from target as [x_tol, y_tol, z_tol]
2544+
in meters. If error on an axis is within tolerance, it's treated
2545+
as reached. Use None for no tolerance on an axis.
2546+
rotation_tolerance : list or None
2547+
Per-axis rotation tolerance from target as
2548+
[roll_tol, pitch_tol, yaw_tol] in radians. If error on an axis
2549+
is within tolerance, it's treated as reached.
24772550
**kwargs : dict
24782551
Additional keyword arguments
24792552
@@ -2506,12 +2579,14 @@ def batch_inverse_kinematics(
25062579
return self._batch_inverse_kinematics_impl(
25072580
target_coords, move_target, link_list,
25082581
rotation_axis, translation_axis, stop, thre, rthre,
2509-
initial_angles, alpha, attempts_per_pose, random_initial_range, **kwargs)
2582+
initial_angles, alpha, attempts_per_pose, random_initial_range,
2583+
translation_tolerance, rotation_tolerance, **kwargs)
25102584

25112585
def _batch_inverse_kinematics_impl(
25122586
self, target_coords, move_target, link_list,
25132587
rotation_axis, translation_axis, stop, thre, rthre,
2514-
initial_angles, alpha, attempts_per_pose, random_initial_range, **kwargs):
2588+
initial_angles, alpha, attempts_per_pose, random_initial_range,
2589+
translation_tolerance, rotation_tolerance, **kwargs):
25152590
"""Internal implementation of batch inverse kinematics."""
25162591
# Auto-adjust initial_angles based on attempts_per_pose
25172592
if isinstance(initial_angles, str) and initial_angles == "current" and attempts_per_pose > 1:
@@ -2635,7 +2710,8 @@ def _batch_inverse_kinematics_impl(
26352710
stop, thre, rthre, alpha, base_to_source,
26362711
rotation_axis, translation_axis, actual_joint_list,
26372712
attempts_per_pose, random_initial_range,
2638-
move_target_offset_pos, move_target_offset_rot
2713+
move_target_offset_pos, move_target_offset_rot,
2714+
translation_tolerance, rotation_tolerance
26392715
)
26402716

26412717
full_solutions = []
@@ -2655,7 +2731,8 @@ def _solve_batch_ik(
26552731
stop, thre, rthre, alpha, base_to_source,
26562732
rotation_axis, translation_axis, joint_list_without_fixed,
26572733
attempts_per_pose, random_initial_range,
2658-
move_target_offset_pos=None, move_target_offset_rot=None):
2734+
move_target_offset_pos=None, move_target_offset_rot=None,
2735+
translation_tolerance=None, rotation_tolerance=None):
26592736
"""Batch IK solver using batch expansion for multiple attempts."""
26602737

26612738
n_poses = target_poses_xyz_qwxyz.shape[0]
@@ -2704,7 +2781,8 @@ def _solve_batch_ik(
27042781
stop, thre, rthre, alpha, base_to_source,
27052782
expanded_rotation_axis, expanded_translation_axis,
27062783
joint_list_without_fixed, min_angles, max_angles,
2707-
move_target_offset_pos, move_target_offset_rot
2784+
move_target_offset_pos, move_target_offset_rot,
2785+
translation_tolerance, rotation_tolerance
27082786
)
27092787

27102788
solutions = []
@@ -2740,7 +2818,8 @@ def _solve_batch_ik_internal(
27402818
stop, thre, rthre, alpha, base_to_source,
27412819
rotation_axis, translation_axis,
27422820
joint_list_without_fixed, min_angles, max_angles,
2743-
move_target_offset_pos=None, move_target_offset_rot=None):
2821+
move_target_offset_pos=None, move_target_offset_rot=None,
2822+
translation_tolerance=None, rotation_tolerance=None):
27442823
"""Internal batch IK solver."""
27452824
# Use integrated batch IK functions
27462825

@@ -2781,7 +2860,9 @@ def _solve_batch_ik_internal(
27812860
joint_list_without_fixed, min_angles, max_angles,
27822861
joint_list_for_fk=None, # Will be derived from link_list in the function
27832862
move_target_offset_pos=move_target_offset_pos,
2784-
move_target_offset_rot=move_target_offset_rot
2863+
move_target_offset_rot=move_target_offset_rot,
2864+
translation_tolerance=translation_tolerance,
2865+
rotation_tolerance=rotation_tolerance
27852866
)
27862867

27872868
# Update current angles first
@@ -2805,8 +2886,12 @@ def _solve_batch_ik_internal(
28052886
pose_errors = np.zeros((len(unsolved_indices), 6))
28062887
translation_errors = target_poses_unsolved[:, :3] - current_poses_updated[:, :3]
28072888
pose_errors[:, :3] = translation_errors
2808-
# Rotation errors (simplified - assume no rotation constraint for now)
2809-
pose_errors[:, 3:] = 0
2889+
# Calculate rotation errors using quaternion difference
2890+
current_quat_inv = quaternion_inverse(current_poses_updated[:, 3:])
2891+
rotation_error_quat = quaternion_multiply(
2892+
target_poses_unsolved[:, 3:], current_quat_inv)
2893+
rotation_error_rpy = quaternion2rpy(rotation_error_quat)[0][:, ::-1]
2894+
pose_errors[:, 3:] = rotation_error_rpy
28102895

28112896
# Check convergence with respect to constraints
28122897
converged = np.zeros(len(unsolved_indices), dtype=bool)
@@ -2857,6 +2942,31 @@ def _solve_batch_ik_internal(
28572942
if 'z' not in rot_axis.lower():
28582943
constrained_pose_error[5] = 0
28592944

2945+
# Apply translation tolerance in target's local frame
2946+
# Note: tol > 0 check ensures 0.0 means "no special tolerance"
2947+
if translation_tolerance is not None:
2948+
# Get target rotation matrix
2949+
target_rot = quaternion2matrix(target_poses_unsolved[i, 3:])
2950+
# Calculate error in target's local frame
2951+
world_error = target_poses_unsolved[i, :3] - current_poses_updated[i, :3]
2952+
local_error = target_rot.T @ world_error
2953+
for axis_idx, tol in enumerate(translation_tolerance):
2954+
if tol is not None and tol > 0:
2955+
if abs(local_error[axis_idx]) <= tol:
2956+
# Zero out this axis in constrained_pose_error
2957+
# by projecting out the target axis component
2958+
target_axis = target_rot[:, axis_idx]
2959+
proj = (constrained_pose_error[:3] @ target_axis)
2960+
constrained_pose_error[:3] -= proj * target_axis
2961+
2962+
# Apply rotation tolerance: if error is within tolerance,
2963+
# treat as reached (set to 0)
2964+
if rotation_tolerance is not None:
2965+
for axis_idx, tol in enumerate(rotation_tolerance):
2966+
if tol is not None and tol > 0:
2967+
if abs(constrained_pose_error[3 + axis_idx]) <= tol:
2968+
constrained_pose_error[3 + axis_idx] = 0.0
2969+
28602970
# Check convergence only for active constraints
28612971
# Note: pose_errors is in [translation, rotation] order after reordering
28622972
position_error = np.linalg.norm(constrained_pose_error[:3])
@@ -2998,7 +3108,8 @@ def _batch_ik_step_with_constraints(
29983108
rotation_axis_list, translation_axis_list,
29993109
actual_joint_list, min_angles, max_angles,
30003110
joint_list_for_fk=None,
3001-
move_target_offset_pos=None, move_target_offset_rot=None):
3111+
move_target_offset_pos=None, move_target_offset_rot=None,
3112+
translation_tolerance=None, rotation_tolerance=None):
30023113
"""Batch IK step with per-problem axis constraints.
30033114
30043115
Args:
@@ -3140,6 +3251,32 @@ def _batch_ik_step_with_constraints(
31403251
pose_errors[:, 3:] = rotation_error_rpy
31413252
pose_errors = pose_errors[:, [3, 4, 5, 0, 1, 2]]
31423253

3254+
# Apply tolerance in target's local frame
3255+
# pose_errors is now in [rot, rot, rot, trans, trans, trans] order
3256+
# Note: tol > 0 check ensures 0.0 means "no special tolerance"
3257+
if translation_tolerance is not None:
3258+
for b in range(batch_size):
3259+
# Get target rotation matrix
3260+
target_rot = quaternion2matrix(target_poses[b, 3:])
3261+
# Calculate error in target's local frame
3262+
world_error = target_poses[b, :3] - current_poses[b, :3]
3263+
local_error = target_rot.T @ world_error
3264+
for axis_idx, tol in enumerate(translation_tolerance):
3265+
if tol is not None and tol > 0:
3266+
if abs(local_error[axis_idx]) <= tol:
3267+
# Zero out this axis by projecting out the target axis
3268+
target_axis = target_rot[:, axis_idx]
3269+
# Translation is at indices 3, 4, 5 after reordering
3270+
proj = pose_errors[b, 3:6] @ target_axis
3271+
pose_errors[b, 3:6] -= proj * target_axis
3272+
3273+
if rotation_tolerance is not None:
3274+
for axis_idx, tol in enumerate(rotation_tolerance):
3275+
if tol is not None and tol > 0:
3276+
# Rotation is at indices 0, 1, 2 after reordering
3277+
mask = np.abs(pose_errors[:, axis_idx]) <= tol
3278+
pose_errors[mask, axis_idx] = 0.0
3279+
31433280
# Apply constraints and calculate delta for each problem
31443281
joint_angle_deltas = np.zeros((batch_size, ndof))
31453282

0 commit comments

Comments
 (0)