Add SO-101 support in Isaac Lab#5989
Conversation
There was a problem hiding this comment.
🤖 Code Review — PR #5989: Add SO-101 Teleop
Latest review: Commit c8873693 (rebased onto latest develop)
Summary
This PR adds support for the TheRobotStudio SO-101 5-DOF follower arm to Isaac Lab, including:
- Asset configuration (
SO101_CFG,SO101_HIGH_PD_CFG) - Cube-stacking task environments (joint-position and IK-absolute)
- New: Reduced-task position+pitch differential IK controller (
SO101PositionPitchIKController) - Generalization of gripper MDP logic to support single-jaw grippers
- XR teleoperation support with IsaacTeleop
🔴 New Issue Requiring Attention
Test-implementation mismatch in test_so101_teleop_pipeline.py:
The pipeline builder _build_so101_stack_pipeline() produces a 6D action [pos_x, pos_y, pos_z, pitch, roll, gripper] (matching the docstring and SO101IkActionsCfg field order), but the tests still assert 5D:
# test_so101_teleop_pipeline.py, line 37:
assert action_type.shape == (5,), f"expected a 5D action, got shape {action_type.shape}"
# And line 52:
assert output_order == ["pos_x", "pos_y", "pos_z", "roll_value", "gripper_value"], ...Expected assertions should be:
shape == (6,)for the 6D actionoutput_order == ["pos_x", "pos_y", "pos_z", "pitch_value", "roll_value", "gripper_value"](with the correct pitch element label fromSO101WristRetargeter)
Please update test_so101_teleop_pipeline.py to match the 6D action contract.
✅ Previous Issues Addressed
- Yaw tuple typo fixed —
"yaw": (-1.0, 1, 0)→"yaw": (-1.0, 1.0)✓ - Test coverage added — Comprehensive sim-free tests for the position+pitch IK controller ✓
🟢 Positive Aspects of New Changes
- Clean reduced-task IK design: The 4x4
[x, y, z, pitch]solve over 4 joints elegantly handles the 5-DOF arm's kinematic constraints - Proper degenerate handling: Pitch row/error are zeroed when EE is directly above the base (radial reach < ε)
- xyzw quaternion convention documented: Clear docstrings noting IsaacLab's scalar-last convention
- Discriminating regression test:
test_pitch_error_zero_for_non_identity_quat_xyzwcatches quaternion convention bugs - Batch-aware degeneracy mask: Per-environment handling is correctly implemented and tested
🟡 Non-Blocking Suggestions
-
Gripper limit margin (carried from previous review):
_SO101_GRIPPER_OPEN = 1.745is at USD limit (~100°). Consider small margin. -
Table/Cube geometry (carried from previous review): Base
StackEnvCfgtable is atpos=[0.5, 0, 0]but SO-101 cubes atx ∈ [0.15, 0.30]. Verify in-sim.
Verdict: One blocking test inconsistency to fix. Otherwise, the position+pitch IK implementation is well-designed with excellent test coverage.
Changelog:
832734dc→c8873693: Major rewrite adding position+pitch reduced-task IK. Action changed from 5D to 6D. NewSO101PositionPitchIKController,SO101PositionPitchIKAction, and comprehensive unit tests added. Tests need updating to match new 6D contract.
a459b30 to
7d3d44c
Compare
44e01de to
7edb2d1
Compare
Greptile SummaryThis PR adds SO-101 5-DOF follower arm support to Isaac Lab, introducing two cube-stacking environments (
Confidence Score: 5/5This PR is safe to merge; all new code is well-tested and the core math has been verified by thorough sim-free unit tests. The full-pose IK controller, action term, env configs, and teleop pipeline are all self-consistent and well-documented. The No files require special attention — Important Files Changed
Class Diagram%%{init: {'theme': 'neutral'}}%%
classDiagram
class DifferentialIKControllerCfg {
+command_type: str
+use_relative_mode: bool
+ik_method: str
}
class SO101PoseIKControllerCfg {
+orientation_task_weight: float
+orientation_joint_names: tuple
+lambda_min: float
+lambda_max: float
+sigma_thresh: float
+jla_gain: float
+jla_margin: float
}
class DifferentialIKController {
+action_dim: int
+set_command()
+compute()
}
class SO101PoseIKController {
+action_dim: int
+set_command()
+compute()
+_assemble_task()
+_damped_least_squares()
+_adaptive_lambda_sq()
+_joint_limit_avoidance()
+set_joint_pos_limits()
+set_orientation_joint_mask()
}
class DifferentialInverseKinematicsActionCfg {
+controller: DifferentialIKControllerCfg
+scale: float
}
class SO101PoseIKActionCfg {
+controller: SO101PoseIKControllerCfg
}
class DifferentialInverseKinematicsAction {
+apply_actions()
}
class SO101PoseIKAction {
+apply_actions()
-_limits_injected: bool
}
DifferentialIKControllerCfg <|-- SO101PoseIKControllerCfg
DifferentialIKController <|-- SO101PoseIKController
DifferentialInverseKinematicsActionCfg <|-- SO101PoseIKActionCfg
DifferentialInverseKinematicsAction <|-- SO101PoseIKAction
SO101PoseIKActionCfg --> SO101PoseIKControllerCfg : controller
SO101PoseIKAction --> SO101PoseIKController : _ik_controller
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
classDiagram
class DifferentialIKControllerCfg {
+command_type: str
+use_relative_mode: bool
+ik_method: str
}
class SO101PoseIKControllerCfg {
+orientation_task_weight: float
+orientation_joint_names: tuple
+lambda_min: float
+lambda_max: float
+sigma_thresh: float
+jla_gain: float
+jla_margin: float
}
class DifferentialIKController {
+action_dim: int
+set_command()
+compute()
}
class SO101PoseIKController {
+action_dim: int
+set_command()
+compute()
+_assemble_task()
+_damped_least_squares()
+_adaptive_lambda_sq()
+_joint_limit_avoidance()
+set_joint_pos_limits()
+set_orientation_joint_mask()
}
class DifferentialInverseKinematicsActionCfg {
+controller: DifferentialIKControllerCfg
+scale: float
}
class SO101PoseIKActionCfg {
+controller: SO101PoseIKControllerCfg
}
class DifferentialInverseKinematicsAction {
+apply_actions()
}
class SO101PoseIKAction {
+apply_actions()
-_limits_injected: bool
}
DifferentialIKControllerCfg <|-- SO101PoseIKControllerCfg
DifferentialIKController <|-- SO101PoseIKController
DifferentialInverseKinematicsActionCfg <|-- SO101PoseIKActionCfg
DifferentialInverseKinematicsAction <|-- SO101PoseIKAction
SO101PoseIKActionCfg --> SO101PoseIKControllerCfg : controller
SO101PoseIKAction --> SO101PoseIKController : _ik_controller
Reviews (4): Last reviewed commit: "Update SO-101 teleop orientation offset ..." | Re-trigger Greptile |
|
Hi @jiwenc-nv, thanks for the PR! Sorry for the delay, we started a refactor of isaaclab_tasks and the lift task refactor just landed so now would be a good time to merge this. For a task to be in |
549c35a to
c887369
Compare
3940415 to
b8e0c86
Compare
Add the TheRobotStudio SO-101 5-DOF arm and a cube-stacking task with both joint-position and absolute task-space (IK) control, driven over XR teleoperation. - Asset: SO101_CFG and SO101_HIGH_PD_CFG (stiffer PD for IK tracking). - Task: Isaac-Stack-Cube-SO101-v0 (joint-pos) and Isaac-Stack-Cube-SO101-IK-Abs-v0 (absolute pose IK), seated on the table and posed for top-down grasps within the arm's reach. - Controller: a full-SE3-pose differential IK over all 5 arm joints. The 3 linear task rows track position exactly while the 3 orientation rows are soft-weighted (scalar or per-axis, so wz=0 can drop the one DOF a 5-DOF arm cannot serve). Kept well-conditioned via manipulability-aware damped least squares (adaptive lambda keyed off the full weighted task Jacobian) and null-space joint-limit avoidance. - Action term: SO101PoseIKAction wiring the controller into a 7D [pos, quat_xyzw] command; teleop pipeline flattens to the 8D [pos, quat, gripper] action with a clutch-rebased, base-frame pose stream and an analog gripper. - Generalize the stack gripper observations/terminations from parallel-only (2-joint) to single- or N-jaw grippers. - Sim-free unit tests for the controller and the teleop pipeline wiring.
Loading the Isaac-Stack-Cube-SO101-IK-Abs-v0 env cfg eagerly imported
pxr: stack_ik_abs_env_cfg imports SO101PoseIKActionCfg, whose module
imported the SO101PoseIKAction term, which subclasses
DifferentialInverseKinematicsAction and pulls `from pxr import
UsdPhysics` at module load. This broke the forbidden-import contract
that env cfgs must be constructable before SimulationApp launches, so
test_env_cfg_no_forbidden_imports flagged the task.
Split the runtime term into pose_ik_action_term.py and reference it
from the cfg via a lazy "{DIR}.pose_ik_action_term:SO101PoseIKAction"
class_type string, resolved by cfg.validate() after Kit starts. This
mirrors the stock DifferentialInverseKinematicsActionCfg and keeps the
cfg module pxr-free. Update the unit test to resolve the now-string
class_type.
The SO-101 jaw's asset-default actuator (effort 10 N·m, velocity 10 rad/s) snaps shut and pushes through or ejects the cube during teleop. Cap the grip torque (effort_limit_sim 1.0) and closing speed (velocity_limit_sim 2.0) in the IK-abs env so the jaw closes gently and holds without penetrating, behavior-matching the Franka gripper. The jaw is revolute, so the Franka panda_hand's prismatic gains are not copied; stiffness/damping keep the asset defaults.
Retune the SO-101 clutch calibration offset (_SO101_ORIENTATION_OFFSET_XYZW) from RPY (-90, 0, 180) to (-90, 0, 60) degrees (intrinsic XYZ). Tuned in-sim via the retargeting tuning UI so the gripper holds the intended orientation during cube-stack teleop.
0cd81c4 to
d352fef
Compare
…KController — new adaptive_dls ik_method (lambda_min/lambda_max/sigma_thresh), opt-in per-axis orientation_weight, and null-space JLA (joint_limit_avoidance_gain/joint_limit_avoidance_margin); the action term injects joint limits automatically. SO101PoseIKController is now a thin subclass that only adds the wrist-only orientation mask. Franka decoupling: promoted cubes/semantics/ee-frame/reset-events into the robot-neutral StackEnvCfg base; Franka + SO-101 are override-only and SO-101 no longer imports franka_stack_events. randomize_joint_by_gaussian_offset is generalized (resolves gripper joints from gripper_joint_names) and moved to stack_events (shim retained). Galbot/UR10/bin/visuomotor migration tracked as a follow-up.
Description
Adds so101 stack env for teleop + imitation learning
Fixes # (issue)
Type of change
Checklist
pre-commitchecks with./isaaclab.sh --formatconfig/extension.tomlfileCONTRIBUTORS.mdor my name already exists there