Skip to content

Commit f5eaf90

Browse files
committed
[Test] Feed real ee_vel to OSC; assert on tail mean not min
OSC's task-space PD: F = kp * pose_err - kd * ee_vel. The test was passing ee_vel = zeros, which silenced the kd damping term and made the impedance an undamped spring. Result was a sustained oscillation around the target with pos_min = 7-12 mm (bottom of swing) but pos_mean = 35-44 mm. Tail-min on an oscillating envelope can pass spuriously; tail-mean is the right regression metric. Fix: - Compute ee_vel_b as J . q_dot (the formal definition; backend- symmetric; uses bridge accessors that are already pinned). Avoids Newton's lazy body_vel_w buffer which can return zeros until forced materialization. - Switch all four IK/OSC accuracy assertions from pos_min to pos_mean. - Tighten OSC threshold to 5 mm (was 2 cm) to match IK. Final accuracy on both backends: - Newton IK: pos_mean 0.00000 m (machine precision) - Newton OSC: pos_mean 0.00000 m (machine precision) - PhysX IK: pos_mean 0.00001 m (10 um) - PhysX OSC: pos_mean 0.00000 m (machine precision)
1 parent c21963d commit f5eaf90

6 files changed

Lines changed: 92 additions & 33 deletions

File tree

source/isaaclab_newton/config/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "0.5.27"
4+
version = "0.5.28"
55

66
# Description
77
title = "Newton simulation interfaces for IsaacLab core package"

source/isaaclab_newton/docs/CHANGELOG.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
Changelog
22
---------
33

4+
0.5.28 (2026-05-02)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Changed
8+
^^^^^^^
9+
10+
* OSC accuracy test now feeds OSC the actual end-effector velocity
11+
(computed as ``J · q_dot``, not ``zeros``) so OSC's ``kd · v`` damping
12+
term engages. With damping active, OSC converges to machine
13+
precision on the 5 cm Cartesian step instead of oscillating around
14+
the target. Both accuracy tests now assert on the tail mean rather
15+
than the tail min (the latter is the bottom of any oscillation
16+
envelope and can pass spuriously). OSC threshold tightened from
17+
2 cm/min to 5 mm/mean.
18+
419
0.5.27 (2026-05-01)
520
~~~~~~~~~~~~~~~~~~~
621

source/isaaclab_newton/test/assets/test_articulation.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,21 @@ def _compute_jacobian_root_frame(robot, ee_jacobi_idx, arm_joint_ids):
424424
return jacobian
425425

426426

427+
def _compute_ee_vel_root(jacobian_b, joint_vel):
428+
"""Return the EE 6D velocity in the root frame as ``J · q_dot``.
429+
430+
Required to make OSC's ``kd * ee_vel_b`` damping term meaningful.
431+
Passing zero EE velocity (the convenient hack) leaves the impedance
432+
undamped and the EE oscillates around the target. We use ``J · q_dot``
433+
rather than reading ``data.body_vel_w`` because Newton's lazy
434+
velocity buffers can return stale/zero values until forced
435+
materialization, while ``joint_vel`` and ``J`` are already pulled
436+
by the loop. ``J`` correctness is pinned independently by
437+
``test_get_jacobians_link_origin_contract``.
438+
"""
439+
return torch.bmm(jacobian_b, joint_vel.unsqueeze(-1)).squeeze(-1)
440+
441+
427442
def _build_relative_pose_target(robot, ee_frame_idx, delta_xyz, device):
428443
"""Build a target pose = (current EE pose) + ``delta_xyz``, preserving orientation."""
429444
initial_ee_pos_b, initial_ee_quat_b, _ = _compute_ee_pose_root(robot, ee_frame_idx)
@@ -3049,15 +3064,17 @@ def test_franka_ik_tracking_accuracy(sim, device, articulation_type, gravity_ena
30493064
# Print metrics every run for stress-test capture.
30503065
print(f"IK_METRIC pos_min={pos_min:.5f} pos_mean={pos_mean:.5f} rot_min={rot_min:.5f} rot_mean={rot_mean:.5f}")
30513066

3052-
# Regression sentinel: 5 mm best-of-tail. With the configured home
3053-
# pose and scene gravity off, Newton converges to machine precision
3054-
# (sub-mm) on this 5 cm Cartesian step. The 5 mm bound absorbs any
3055-
# CUDA-kernel-ordering noise while remaining well below the "totally
3056-
# broken" regime: a bridge regression (wrong-frame Jacobian, missing
3057-
# COM->origin shift, DoF mis-ordering) would push the steady-state
3058-
# error well past this bound.
3059-
assert pos_min < 5e-3, f"IK pos_min {pos_min:.5f} > 5 mm — bridge regression?"
3060-
assert rot_min < 5e-2, f"IK rot_min {rot_min:.5f} > 0.05 rad — bridge regression?"
3067+
# Regression sentinel: assert on tail mean rather than min. Tail
3068+
# min is the bottom of any oscillation envelope and can be tiny
3069+
# while the actual tracking error is much larger. With the
3070+
# configured home pose and scene gravity off, Newton converges to
3071+
# machine precision (sub-mm). The 5 mm bound absorbs any CUDA-
3072+
# kernel-ordering noise while remaining well below the "totally
3073+
# broken" regime: a bridge regression (wrong-frame Jacobian,
3074+
# missing COM->origin shift, DoF mis-ordering) would push the
3075+
# steady-state error well past this bound.
3076+
assert pos_mean < 5e-3, f"IK pos_mean {pos_mean:.5f} > 5 mm — bridge regression?"
3077+
assert rot_mean < 5e-2, f"IK rot_mean {rot_mean:.5f} > 0.05 rad — bridge regression?"
30613078

30623079

30633080
@pytest.mark.parametrize("device", ["cuda:0"])
@@ -3104,12 +3121,13 @@ def test_franka_osc_tracking_accuracy(sim, device, articulation_type, gravity_en
31043121

31053122
pos_history: list[float] = []
31063123
rot_history: list[float] = []
3107-
ee_vel_b = torch.zeros(1, 6, device=device) # OSC needs vel; use zero (works at low speed).
31083124
for _ in range(800):
31093125
jacobian_b = _compute_jacobian_root_frame(robot, ee_jacobi_idx, arm_joint_ids)
31103126
mass_matrix = wp.to_torch(robot.get_mass_matrix())[:, arm_joint_ids, :][:, :, arm_joint_ids]
31113127
ee_pos_b, ee_quat_b, _ = _compute_ee_pose_root(robot, ee_frame_idx)
31123128
ee_pose_b = torch.cat([ee_pos_b, ee_quat_b], dim=-1)
3129+
joint_vel = robot.data.joint_vel.torch[:, arm_joint_ids]
3130+
ee_vel_b = _compute_ee_vel_root(jacobian_b, joint_vel)
31133131

31143132
osc.set_command(target_pose_b, current_ee_pose_b=ee_pose_b)
31153133
joint_efforts = osc.compute(
@@ -3134,18 +3152,16 @@ def test_franka_osc_tracking_accuracy(sim, device, articulation_type, gravity_en
31343152

31353153
print(f"OSC_METRIC pos_min={pos_min:.5f} pos_mean={pos_mean:.5f} rot_min={rot_min:.5f} rot_mean={rot_mean:.5f}")
31363154

3137-
# Regression sentinel: 2 cm best-of-tail (matches the PhysX-side
3138-
# test). With the home pose start, zero actuator PD, and no
3139-
# gravity, both backends settle into a sustained sub-cm oscillation
3140-
# around the target driven by the critically damped impedance
3141-
# against ``current_ee_vel_b = 0``. Tighter bounds would require
3142-
# non-zero ee-velocity feedback or a larger ``motion_stiffness_task``,
3143-
# neither of which is part of the bridge-pinning contract. A bridge
3144-
# regression (wrong J, wrong mass matrix, DoF mis-ordering) pushes
3145-
# the error well past 2 cm because OSC consumes both
3146-
# ``get_jacobians`` and ``get_mass_matrix`` per step.
3147-
assert pos_min < 2e-2, f"OSC pos_min {pos_min:.5f} > 2 cm — bridge regression?"
3148-
assert rot_min < 2e-1, f"OSC rot_min {rot_min:.5f} > 0.2 rad — bridge regression?"
3155+
# Regression sentinel: assert on tail mean rather than min. With
3156+
# ``current_ee_vel_b = J · q_dot`` providing OSC's damping term and
3157+
# the actuator PD zeroed, the impedance settles to machine
3158+
# precision -- same ballpark as the IK test. The 5 mm bound is a
3159+
# bridge regression sentinel: a wrong J, wrong mass matrix, or
3160+
# DoF mis-ordering pushes the steady-state error well past it
3161+
# because OSC consumes both ``get_jacobians`` and
3162+
# ``get_mass_matrix`` per step.
3163+
assert pos_mean < 5e-3, f"OSC pos_mean {pos_mean:.5f} > 5 mm — bridge regression?"
3164+
assert rot_mean < 5e-2, f"OSC rot_mean {rot_mean:.5f} > 0.05 rad — bridge regression?"
31493165

31503166

31513167
if __name__ == "__main__":

source/isaaclab_physx/config/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "0.5.29"
4+
version = "0.5.30"
55

66
# Description
77
title = "PhysX simulation interfaces for IsaacLab core package"

source/isaaclab_physx/docs/CHANGELOG.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Changelog
22
---------
33

4+
0.5.30 (2026-05-02)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Changed
8+
^^^^^^^
9+
10+
* Mirrors the same OSC-test fix from ``isaaclab_newton`` 0.5.28: feed
11+
OSC ``J · q_dot`` for the end-effector velocity so its damping term
12+
engages, and assert on the tail mean (not min). Threshold tightened
13+
to 5 mm.
14+
415
0.5.29 (2026-05-01)
516
~~~~~~~~~~~~~~~~~~~
617

source/isaaclab_physx/test/assets/test_articulation.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,20 @@ def _compute_jacobian_root_frame(robot, ee_jacobi_idx, arm_joint_ids):
252252
return jacobian
253253

254254

255+
def _compute_ee_vel_root(jacobian_b, joint_vel):
256+
"""Return the EE 6D velocity in the root frame as ``J · q_dot``.
257+
258+
Required to make OSC's ``kd * ee_vel_b`` damping term meaningful.
259+
Passing zero EE velocity (the convenient hack) leaves the impedance
260+
undamped and the EE oscillates around the target. ``J · q_dot``
261+
avoids relying on ``data.body_vel_w`` (Newton's lazy velocity
262+
buffers can return stale/zero values until forced materialization),
263+
keeping the helper backend-symmetric. ``J`` correctness is pinned
264+
independently by ``test_get_jacobians_link_origin_contract``.
265+
"""
266+
return torch.bmm(jacobian_b, joint_vel.unsqueeze(-1)).squeeze(-1)
267+
268+
255269
def _build_relative_pose_target(robot, ee_frame_idx, delta_xyz, device):
256270
"""Build a target pose = (current EE pose) + ``delta_xyz``, preserving orientation."""
257271
initial_ee_pos_b, initial_ee_quat_b, _ = _compute_ee_pose_root(robot, ee_frame_idx)
@@ -2609,11 +2623,11 @@ def test_franka_ik_tracking_accuracy(sim, device, articulation_type, gravity_ena
26092623

26102624
print(f"IK_METRIC pos_min={pos_min:.5f} pos_mean={pos_mean:.5f} rot_min={rot_min:.5f} rot_mean={rot_mean:.5f}")
26112625

2612-
# Threshold matched to the Newton-side test (5 mm / 0.05 rad). PhysX
2613-
# historically achieves sub-mm here; the slightly looser bound just
2614-
# absorbs run-to-run variance.
2615-
assert pos_min < 5e-3, f"IK pos_min {pos_min:.5f} > 5 mm — bridge regression?"
2616-
assert rot_min < 5e-2, f"IK rot_min {rot_min:.5f} > 0.05 rad — bridge regression?"
2626+
# Assert on tail mean (not min) so an oscillating envelope can't
2627+
# squeeze through. Threshold matched to the Newton-side test
2628+
# (5 mm / 0.05 rad).
2629+
assert pos_mean < 5e-3, f"IK pos_mean {pos_mean:.5f} > 5 mm — bridge regression?"
2630+
assert rot_mean < 5e-2, f"IK rot_mean {rot_mean:.5f} > 0.05 rad — bridge regression?"
26172631

26182632

26192633
@pytest.mark.parametrize("device", ["cuda:0"])
@@ -2649,12 +2663,13 @@ def test_franka_osc_tracking_accuracy(sim, device, articulation_type, gravity_en
26492663

26502664
pos_history: list[float] = []
26512665
rot_history: list[float] = []
2652-
ee_vel_b = torch.zeros(1, 6, device=device)
26532666
for _ in range(800):
26542667
jacobian_b = _compute_jacobian_root_frame(robot, ee_jacobi_idx, arm_joint_ids)
26552668
mass_matrix = wp.to_torch(robot.get_mass_matrix())[:, arm_joint_ids, :][:, :, arm_joint_ids]
26562669
ee_pos_b, ee_quat_b, _ = _compute_ee_pose_root(robot, ee_frame_idx)
26572670
ee_pose_b = torch.cat([ee_pos_b, ee_quat_b], dim=-1)
2671+
joint_vel = robot.data.joint_vel.torch[:, arm_joint_ids]
2672+
ee_vel_b = _compute_ee_vel_root(jacobian_b, joint_vel)
26582673

26592674
osc.set_command(target_pose_b, current_ee_pose_b=ee_pose_b)
26602675
joint_efforts = osc.compute(
@@ -2679,9 +2694,11 @@ def test_franka_osc_tracking_accuracy(sim, device, articulation_type, gravity_en
26792694

26802695
print(f"OSC_METRIC pos_min={pos_min:.5f} pos_mean={pos_mean:.5f} rot_min={rot_min:.5f} rot_mean={rot_mean:.5f}")
26812696

2682-
# Threshold matches the Newton-side test (2 cm / 0.2 rad).
2683-
assert pos_min < 2e-2, f"OSC pos_min {pos_min:.5f} > 2 cm — bridge regression?"
2684-
assert rot_min < 2e-1, f"OSC rot_min {rot_min:.5f} > 0.2 rad — bridge regression?"
2697+
# Assert on tail mean. Threshold matched to the Newton-side test
2698+
# (5 mm / 0.05 rad). Both backends converge to machine precision
2699+
# with proper ee-velocity feedback (``J · q_dot``).
2700+
assert pos_mean < 5e-3, f"OSC pos_mean {pos_mean:.5f} > 5 mm — bridge regression?"
2701+
assert rot_mean < 5e-2, f"OSC rot_mean {rot_mean:.5f} > 0.05 rad — bridge regression?"
26852702

26862703

26872704
if __name__ == "__main__":

0 commit comments

Comments
 (0)