Skip to content

Commit 4f8ec85

Browse files
authored
fix(tasks): index Franka gripper/arm joints by sorted-name order in ManiSkill rewards (#774)
MetaSim reports RobotState.joint_pos/joint_vel in alphabetically-sorted joint-name order. For the Franka Panda that is [panda_finger_joint1, panda_finger_joint2, panda_joint1..7] -> fingers at columns 0-1, arm at 2-8 (franka_cfg.py ee_joint_indices=[0,1]). The dense rewards sliced joint_pos[:,7:9] for the gripper and joint_vel[:,:7] for the arm (URDF order), so is_grasped, gripper_width and the static/velocity terms read the wrong joints on every backend. Correct to [:,0:2] (gripper) and [:,2:9] (arm) in pick_cube_v1, lift_peg_upright, place_sphere, poke_cube, pick_single_ycb, stack_cube. The _native/ bitwise-parity rewards are fed raw native-order qpos and are intentionally left untouched. Adds a backend-free regression test (constructs RobotState/ObjectState and calls the pure _reward): grasp reward must respond to the finger columns, plus a source guard against the URDF-order slice across all six files. Verified the test is red on the pre-fix slices and green after.
1 parent 6786f3b commit 4f8ec85

7 files changed

Lines changed: 115 additions & 12 deletions

File tree

roboverse_pack/tasks/maniskill/lift_peg_upright.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def _reward(self, env_states):
119119

120120
to_grip = torch.linalg.norm(peg - tcp, dim=-1)
121121
reaching = 1.0 - torch.tanh(5.0 * to_grip)
122-
gripper_width = rs.joint_pos[:, 7:9].sum(dim=-1)
122+
gripper_width = rs.joint_pos[:, 0:2].sum(dim=-1)
123123
is_grasping = (to_grip < 0.04) & (gripper_width < 0.07)
124124
reaching = torch.where(is_grasping, torch.ones_like(reaching), reaching)
125125
reaching = reaching / 5.0

roboverse_pack/tasks/maniskill/pick_cube_v1.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _reward(self, env_states):
153153
reaching = 1.0 - torch.tanh(5.0 * tcp_to_obj)
154154
reward = reaching
155155

156-
finger_q = rs.joint_pos[:, 7:9]
156+
finger_q = rs.joint_pos[:, 0:2]
157157
gripper_width = finger_q.sum(dim=-1)
158158
is_grasped = ((tcp_to_obj < 0.03) & (gripper_width < 0.07)).float()
159159
reward = reward + is_grasped
@@ -162,7 +162,7 @@ def _reward(self, env_states):
162162
place = 1.0 - torch.tanh(5.0 * obj_to_goal)
163163
reward = reward + place * is_grasped
164164

165-
arm_qvel = rs.joint_vel[:, :7]
165+
arm_qvel = rs.joint_vel[:, 2:9]
166166
static = 1.0 - torch.tanh(5.0 * torch.linalg.norm(arm_qvel, dim=-1))
167167
is_obj_placed = (obj_to_goal <= _GOAL_THRESH).float()
168168
reward = reward + static * is_obj_placed

roboverse_pack/tasks/maniskill/pick_single_ycb.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def _reward(self, env_states):
9898

9999
tcp_to_obj = torch.linalg.norm(obj - tcp, dim=-1)
100100
reaching = 1.0 - torch.tanh(5.0 * tcp_to_obj)
101-
gripper_w = rs.joint_pos[:, 7:9].sum(dim=-1)
101+
gripper_w = rs.joint_pos[:, 0:2].sum(dim=-1)
102102
is_grasped = ((tcp_to_obj < 0.03) & (gripper_w < 0.07)).float()
103103
reward = reaching + is_grasped
104104

@@ -109,7 +109,7 @@ def _reward(self, env_states):
109109
is_obj_placed = (obj_to_goal <= _YCB_GOAL_THRESH).float()
110110
reward = reward + is_obj_placed * is_grasped
111111

112-
arm_qvel = rs.joint_vel[:, :7]
112+
arm_qvel = rs.joint_vel[:, 2:9]
113113
static = 1.0 - torch.tanh(5.0 * torch.linalg.norm(arm_qvel, dim=-1))
114114
reward = reward + static * is_obj_placed * is_grasped
115115

roboverse_pack/tasks/maniskill/place_sphere.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,14 @@ def _reward(self, env_states):
157157

158158
o_to_bin = torch.linalg.norm(bin_top - obj, dim=-1)
159159
place = 1.0 - torch.tanh(5.0 * o_to_bin)
160-
gripper_w = rs.joint_pos[:, 7:9].sum(dim=-1)
160+
gripper_w = rs.joint_pos[:, 0:2].sum(dim=-1)
161161
is_grasped = (o_to_tcp < 0.03) & (gripper_w < 0.07)
162162
reward = torch.where(is_grasped, 4.0 + place, reward)
163163

164-
ungrasp = rs.joint_pos[:, 7:9].sum(dim=-1) / _PS_GRIPPER_WIDTH
164+
ungrasp = rs.joint_pos[:, 0:2].sum(dim=-1) / _PS_GRIPPER_WIDTH
165165
ungrasp = torch.where(is_grasped, ungrasp, torch.full_like(ungrasp, 16.0))
166166
static = 1.0 - torch.tanh(torch.linalg.norm(obj_v, dim=-1) * 10.0 + torch.linalg.norm(obj_w, dim=-1))
167-
arm_qvel = rs.joint_vel[:, :7]
167+
arm_qvel = rs.joint_vel[:, 2:9]
168168
robot_static = (arm_qvel.abs().amax(dim=-1) < 0.2).float()
169169

170170
off = obj - objs["bin"].root_state[:, :3]

roboverse_pack/tasks/maniskill/poke_cube.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,15 @@ def _reward(self, env_states):
195195
h2c = torch.linalg.norm(peg_head[:, :2] - cube[:, :2], dim=-1)
196196
close = 1.0 - torch.tanh(5.0 * h2c)
197197

198-
gripper_w = rs.joint_pos[:, 7:9].sum(dim=-1)
198+
gripper_w = rs.joint_pos[:, 0:2].sum(dim=-1)
199199
grasped = (tcp_to_peg < 0.04) & (gripper_w < 0.07) & reached
200200
reward = torch.where(grasped, 4.0 + close + align, reward)
201201

202202
place = 1.0 - torch.tanh(5.0 * torch.linalg.norm(goal - cube, dim=-1))
203203
fit = (angle < 0.05) & (h2c <= _PK_CUBE_HALF + 0.005) & grasped
204204
reward = torch.where(fit, 7.0 + place, reward)
205205

206-
arm_qvel = rs.joint_vel[:, :7]
206+
arm_qvel = rs.joint_vel[:, 2:9]
207207
static = 1.0 - torch.tanh(5.0 * torch.linalg.norm(arm_qvel, dim=-1))
208208
cube_placed = torch.linalg.norm(cube[:, :2] - goal[:, :2], dim=-1) < _PK_GOAL_RADIUS
209209
reward = torch.where(cube_placed, reward + static, reward)

roboverse_pack/tasks/maniskill/stack_cube.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ def _reward(self, env_states):
120120
a_to_goal = torch.linalg.norm(goal_xyz - cubeA, dim=-1)
121121
place = 1.0 - torch.tanh(5.0 * a_to_goal)
122122

123-
gripper_w = rs.joint_pos[:, 7:9].sum(dim=-1)
123+
gripper_w = rs.joint_pos[:, 0:2].sum(dim=-1)
124124
is_grasped = (a_to_tcp < 0.03) & (gripper_w < 0.07)
125125
reward = torch.where(is_grasped, 4.0 + place, reward)
126126

127-
ungrasp = rs.joint_pos[:, 7:9].sum(dim=-1) / _SC_GRIPPER_WIDTH
127+
ungrasp = rs.joint_pos[:, 0:2].sum(dim=-1) / _SC_GRIPPER_WIDTH
128128
ungrasp = torch.where(is_grasped, ungrasp, torch.ones_like(ungrasp))
129129
static = 1.0 - torch.tanh(torch.linalg.norm(cubeA_vel, dim=-1) * 10.0 + torch.linalg.norm(cubeA_ang, dim=-1))
130130

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Regression: ManiSkill dense rewards index Franka joints by the sorted-name contract.
2+
3+
MetaSim reports ``RobotState.joint_pos`` / ``joint_vel`` in alphabetically-sorted
4+
joint-name order. For the Franka Panda that order is
5+
``[panda_finger_joint1, panda_finger_joint2, panda_joint1..panda_joint7]`` — i.e.
6+
the two gripper fingers are columns 0-1 and the seven arm joints are columns 2-8
7+
(see ``roboverse_pack/robots/franka_cfg.py`` ``ee_joint_indices = [0, 1]``).
8+
9+
The dense reward functions used to slice ``joint_pos[:, 7:9]`` for the gripper and
10+
``joint_vel[:, :7]`` for the arm — the *URDF* declaration order — so ``is_grasped``,
11+
``gripper_width`` and the static/velocity terms were computed from the wrong joints
12+
on every backend. These tests pin the corrected indexing without needing a
13+
simulator backend or downloaded assets (``_reward`` is a pure function of the
14+
passed ``env_states``).
15+
16+
Backend-free by design: no existing ``tests/test_maniskill_*`` file is usable here
17+
because they all ``pytest.importorskip("mani_skill")`` at module load.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import torch
23+
24+
from metasim.types import ObjectState, RobotState
25+
26+
27+
def _pick_cube_states(finger_val: float, arm_tail_val: float = 1.5):
28+
"""Build a minimal env_states for PickCube where the cube sits at the TCP.
29+
30+
Fingers occupy sorted columns 0-1; ``arm_tail_val`` is written to columns 7-8
31+
(the columns the buggy code mistook for the gripper) so a regression cannot
32+
pass by reading the arm. ``finger_val`` per finger: 0.01 -> closed/grasped,
33+
0.04 -> open.
34+
"""
35+
import roboverse_pack.tasks.maniskill.pick_cube_v1 as M
36+
37+
jp = torch.zeros(1, 9)
38+
jp[0, 0] = finger_val
39+
jp[0, 1] = finger_val
40+
jp[0, 7] = arm_tail_val
41+
jp[0, 8] = arm_tail_val
42+
body_state = torch.zeros(1, 2, 13)
43+
body_state[0, 1, 3] = 1.0 # panda_hand quaternion = wxyz identity, at origin
44+
rs = RobotState(
45+
root_state=torch.zeros(1, 13),
46+
body_names=["panda_link0", "panda_hand"],
47+
body_state=body_state,
48+
joint_pos=jp,
49+
joint_vel=torch.zeros(1, 9),
50+
)
51+
cube = torch.zeros(1, 13)
52+
cube[0, :3] = torch.tensor(M._TCP_OFFSET) # cube exactly at the TCP -> reaching satisfied
53+
goal = torch.zeros(1, 13)
54+
goal[0, 2] = 0.5 # goal far away -> not placed -> no success override
55+
56+
class _S:
57+
pass
58+
59+
s = _S()
60+
s.robots = {"franka": rs}
61+
s.objects = {"cube": ObjectState(root_state=cube), "goal_site": ObjectState(root_state=goal)}
62+
return s
63+
64+
65+
def test_pick_cube_grasp_reward_reads_finger_columns():
66+
"""Closing the fingers (sorted cols 0-1) must register as a grasp.
67+
68+
On the pre-fix code (``joint_pos[:, 7:9]``) the reward read arm columns 7-8
69+
(here a large constant), so it never saw the grasp and both branches scored
70+
identically — this asserts the gap that only the corrected indexing produces.
71+
"""
72+
from roboverse_pack.tasks.maniskill.pick_cube_v1 import PickCubeV1DenseTask
73+
74+
grasped = float(PickCubeV1DenseTask._reward(None, _pick_cube_states(0.01))[0])
75+
ungrasped = float(PickCubeV1DenseTask._reward(None, _pick_cube_states(0.04))[0])
76+
77+
assert grasped > ungrasped + 0.9, (
78+
f"grasp (fingers closed) should raise the reward via the is_grasped term; "
79+
f"got grasped={grasped:.4f} ungrasped={ungrasped:.4f}. A near-zero gap means "
80+
f"the reward is reading arm columns, not the gripper fingers (cols 0-1)."
81+
)
82+
83+
84+
def test_no_maniskill_reward_uses_urdf_order_finger_slice():
85+
"""Guard the whole dense-reward family against the URDF-order regression.
86+
87+
The five sibling tasks (stack_cube, poke_cube, place_sphere, pick_single_ycb,
88+
lift_peg_upright) share the same Franka layout but build different object
89+
states, so this asserts the corrected slice convention at the source level
90+
rather than re-deriving each task's full env_states.
91+
"""
92+
import pathlib
93+
94+
pack = pathlib.Path(__file__).resolve().parents[1] / "roboverse_pack" / "tasks" / "maniskill"
95+
offenders = []
96+
for py in pack.glob("*.py"):
97+
text = py.read_text()
98+
if "rs.joint_pos[:, 7:9]" in text or "rs.joint_vel[:, :7]" in text:
99+
offenders.append(py.name)
100+
assert not offenders, (
101+
f"these files slice Franka joints in URDF order (gripper 7:9 / arm :7) "
102+
f"instead of the sorted-name order (gripper 0:2 / arm 2:9): {offenders}"
103+
)

0 commit comments

Comments
 (0)