Using a task-space controller#
In the previous tutorials, we have joint-space controllers to control the robot. However, in many cases, it is more intuitive to control the robot using a task-space controller. For example, if we want to teleoperate the robot, it is easier to specify the desired end-effector pose rather than the desired joint positions.
In this tutorial, we will learn how to use a task-space controller to control the robot.
We will use the controllers.DifferentialIKController class to track a desired
end-effector pose command.
The Code#
The tutorial corresponds to the run_diff_ik.py script in the
scripts/tutorials/05_controllers directory.
Code for run_diff_ik.py
1# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2# All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5
6"""
7This script demonstrates how to use the differential inverse kinematics controller with the simulator.
8
9The differential IK controller can be configured in different modes. It uses the Jacobians computed by
10PhysX. This helps perform parallelized computation of the inverse kinematics.
11
12.. code-block:: bash
13
14 # Usage
15 ./isaaclab.sh -p scripts/tutorials/05_controllers/run_diff_ik.py
16
17"""
18
19"""Launch Isaac Sim Simulator first."""
20
21import argparse
22
23import warp as wp
24
25from isaaclab.app import AppLauncher
26
27# add argparse arguments
28parser = argparse.ArgumentParser(description="Tutorial on using the differential IK controller.")
29parser.add_argument("--robot", type=str, default="franka_panda", help="Name of the robot.")
30parser.add_argument("--num_envs", type=int, default=128, help="Number of environments to spawn.")
31# append AppLauncher cli args
32AppLauncher.add_app_launcher_args(parser)
33# parse the arguments
34args_cli = parser.parse_args()
35
36# launch omniverse app
37app_launcher = AppLauncher(args_cli)
38simulation_app = app_launcher.app
39
40"""Rest everything follows."""
41
42import torch
43
44import isaaclab.sim as sim_utils
45from isaaclab.assets import AssetBaseCfg
46from isaaclab.controllers import DifferentialIKController, DifferentialIKControllerCfg
47from isaaclab.managers import SceneEntityCfg
48from isaaclab.markers import VisualizationMarkers
49from isaaclab.markers.config import FRAME_MARKER_CFG
50from isaaclab.scene import InteractiveScene, InteractiveSceneCfg
51from isaaclab.utils import configclass
52from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
53from isaaclab.utils.math import subtract_frame_transforms
54
55##
56# Pre-defined configs
57##
58from isaaclab_assets import FRANKA_PANDA_HIGH_PD_CFG, UR10_CFG # isort:skip
59
60
61@configclass
62class TableTopSceneCfg(InteractiveSceneCfg):
63 """Configuration for a cart-pole scene."""
64
65 # ground plane
66 ground = AssetBaseCfg(
67 prim_path="/World/defaultGroundPlane",
68 spawn=sim_utils.GroundPlaneCfg(),
69 init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, -1.05)),
70 )
71
72 # lights
73 dome_light = AssetBaseCfg(
74 prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
75 )
76
77 # mount
78 table = AssetBaseCfg(
79 prim_path="{ENV_REGEX_NS}/Table",
80 spawn=sim_utils.UsdFileCfg(
81 usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/Stand/stand_instanceable.usd", scale=(2.0, 2.0, 2.0)
82 ),
83 )
84
85 # articulation
86 if args_cli.robot == "franka_panda":
87 robot = FRANKA_PANDA_HIGH_PD_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
88 elif args_cli.robot == "ur10":
89 robot = UR10_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
90 else:
91 raise ValueError(f"Robot {args_cli.robot} is not supported. Valid: franka_panda, ur10")
92
93
94def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
95 """Runs the simulation loop."""
96 # Extract scene entities
97 # note: we only do this here for readability.
98 robot = scene["robot"]
99
100 # Create controller
101 diff_ik_cfg = DifferentialIKControllerCfg(command_type="pose", use_relative_mode=False, ik_method="dls")
102 diff_ik_controller = DifferentialIKController(diff_ik_cfg, num_envs=scene.num_envs, device=sim.device)
103
104 # Markers
105 frame_marker_cfg = FRAME_MARKER_CFG.copy()
106 frame_marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1)
107 ee_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_current"))
108 goal_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_goal"))
109
110 # Define goals for the arm (x,y,z,qx,qy,qz,qw)
111 ee_goals = [
112 [0.5, 0.5, 0.7, 0, 0.707, 0, 0.707],
113 [0.5, -0.4, 0.6, 0.707, 0, 0, 0.707],
114 [0.5, 0, 0.5, 1.0, 0.0, 0.0, 0.0],
115 ]
116 ee_goals = torch.tensor(ee_goals, device=sim.device)
117 # Track the given command
118 current_goal_idx = 0
119 # Create buffers to store actions
120 ik_commands = torch.zeros(scene.num_envs, diff_ik_controller.action_dim, device=robot.device)
121 ik_commands[:] = ee_goals[current_goal_idx]
122
123 # Specify robot-specific parameters
124 if args_cli.robot == "franka_panda":
125 robot_entity_cfg = SceneEntityCfg("robot", joint_names=["panda_joint.*"], body_names=["panda_hand"])
126 elif args_cli.robot == "ur10":
127 robot_entity_cfg = SceneEntityCfg("robot", joint_names=[".*"], body_names=["ee_link"])
128 else:
129 raise ValueError(f"Robot {args_cli.robot} is not supported. Valid: franka_panda, ur10")
130 # Resolving the scene entities
131 robot_entity_cfg.resolve(scene)
132 # Obtain the frame index of the end-effector
133 # For a fixed base robot, the frame index is one less than the body index. This is because
134 # the root body is not included in the returned Jacobians.
135 if robot.is_fixed_base:
136 ee_jacobi_idx = robot_entity_cfg.body_ids[0] - 1
137 else:
138 ee_jacobi_idx = robot_entity_cfg.body_ids[0]
139
140 # Define simulation stepping
141 sim_dt = sim.get_physics_dt()
142 count = 0
143 # Simulation loop
144 while simulation_app.is_running():
145 # reset
146 if count % 150 == 0:
147 # reset time
148 count = 0
149 # reset joint state
150 joint_pos = wp.to_torch(robot.data.default_joint_pos).clone()
151 joint_vel = wp.to_torch(robot.data.default_joint_vel).clone()
152 robot.write_joint_position_to_sim_index(position=joint_pos)
153 robot.write_joint_velocity_to_sim_index(velocity=joint_vel)
154 robot.reset()
155 # reset actions
156 ik_commands[:] = ee_goals[current_goal_idx]
157 joint_pos_des = joint_pos[:, robot_entity_cfg.joint_ids].clone()
158 # reset controller
159 diff_ik_controller.reset()
160 diff_ik_controller.set_command(ik_commands)
161 # change goal
162 current_goal_idx = (current_goal_idx + 1) % len(ee_goals)
163 else:
164 # obtain quantities from simulation
165 jacobian = robot.root_view.get_jacobians()[:, ee_jacobi_idx, :, robot_entity_cfg.joint_ids]
166 ee_pose_w = wp.to_torch(robot.data.body_pose_w)[:, robot_entity_cfg.body_ids[0]]
167 root_pose_w = wp.to_torch(robot.data.root_pose_w)
168 joint_pos = wp.to_torch(robot.data.joint_pos)[:, robot_entity_cfg.joint_ids]
169 # compute frame in root frame
170 ee_pos_b, ee_quat_b = subtract_frame_transforms(
171 root_pose_w[:, 0:3], root_pose_w[:, 3:7], ee_pose_w[:, 0:3], ee_pose_w[:, 3:7]
172 )
173 # compute the joint commands
174 joint_pos_des = diff_ik_controller.compute(ee_pos_b, ee_quat_b, jacobian, joint_pos)
175
176 # apply actions
177 robot.set_joint_position_target_index(target=joint_pos_des, joint_ids=robot_entity_cfg.joint_ids)
178 scene.write_data_to_sim()
179 # perform step
180 sim.step()
181 # update sim-time
182 count += 1
183 # update buffers
184 scene.update(sim_dt)
185
186 # obtain quantities from simulation
187 ee_pose_w = wp.to_torch(robot.data.body_state_w)[:, robot_entity_cfg.body_ids[0], 0:7]
188 # update marker positions
189 ee_marker.visualize(ee_pose_w[:, 0:3], ee_pose_w[:, 3:7])
190 goal_marker.visualize(ik_commands[:, 0:3] + scene.env_origins, ik_commands[:, 3:7])
191
192
193def main():
194 """Main function."""
195 # Load kit helper
196 sim_cfg = sim_utils.SimulationCfg(dt=0.01, device=args_cli.device)
197 sim = sim_utils.SimulationContext(sim_cfg)
198 # Set main camera
199 sim.set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0])
200 # Design scene
201 scene_cfg = TableTopSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0)
202 scene = InteractiveScene(scene_cfg)
203 # Play the simulator
204 sim.reset()
205 # Now we are ready!
206 print("[INFO]: Setup complete...")
207 # Run the simulator
208 run_simulator(sim, scene)
209
210
211if __name__ == "__main__":
212 # run the main function
213 main()
214 # close sim app
215 simulation_app.close()
The Code Explained#
While using any task-space controller, it is important to ensure that the provided
quantities are in the correct frames. When parallelizing environment instances, they are
all existing in the same unique simulation world frame. However, typically, we want each
environment itself to have its own local frame. This is accessible through the
scene.InteractiveScene.env_origins attribute.
In our APIs, we use the following notation for frames:
The simulation world frame (denoted as
w), which is the frame of the entire simulation.The local environment frame (denoted as
e), which is the frame of the local environment.The robot’s base frame (denoted as
b), which is the frame of the robot’s base link.
Since the asset instances are not “aware” of the local environment frame, they return their states in the simulation world frame. Thus, we need to convert the obtained quantities to the local environment frame. This is done by subtracting the local environment origin from the obtained quantities.
Creating an IK controller#
The DifferentialIKController class computes the desired joint
positions for a robot to reach a desired end-effector pose. The included implementation
performs the computation in a batched format and uses PyTorch operations. It supports
different types of inverse kinematics solvers, including the damped least-squares method
and the pseudo-inverse method. These solvers can be specified using the
ik_method argument.
Additionally, the controller can handle commands as both relative and absolute poses.
In this tutorial, we will use the damped least-squares method to compute the desired joint positions. Additionally, since we want to track desired end-effector poses, we will use the absolute pose command mode.
# Create controller
diff_ik_cfg = DifferentialIKControllerCfg(command_type="pose", use_relative_mode=False, ik_method="dls")
diff_ik_controller = DifferentialIKController(diff_ik_cfg, num_envs=scene.num_envs, device=sim.device)
Obtaining the robot’s joint and body indices#
The IK controller implementation is a computation-only class. Thus, it expects the user to provide the necessary information about the robot. This includes the robot’s joint positions, current end-effector pose, and the Jacobian matrix.
While the attribute assets.ArticulationData.joint_pos provides the joint positions,
we only want the joint positions of the robot’s arm, and not the gripper. Similarly, while
the attribute assets.ArticulationData.body_state_w provides the state of all the
robot’s bodies, we only want the state of the robot’s end-effector. Thus, we need to
index into these arrays to obtain the desired quantities.
For this, the articulation class provides the methods find_joints()
and find_bodies(). These methods take in the names of the joints
and bodies and return their corresponding indices.
While you may directly use these methods to obtain the indices, we recommend using the
SceneEntityCfg class to resolve the indices. This class is used in various
places in the APIs to extract certain information from a scene entity. Internally, it
calls the above methods to obtain the indices. However, it also performs some additional
checks to ensure that the provided names are valid. Thus, it is a safer option to use
this class.
# Specify robot-specific parameters
if args_cli.robot == "franka_panda":
robot_entity_cfg = SceneEntityCfg("robot", joint_names=["panda_joint.*"], body_names=["panda_hand"])
elif args_cli.robot == "ur10":
robot_entity_cfg = SceneEntityCfg("robot", joint_names=[".*"], body_names=["ee_link"])
else:
raise ValueError(f"Robot {args_cli.robot} is not supported. Valid: franka_panda, ur10")
# Resolving the scene entities
robot_entity_cfg.resolve(scene)
# Obtain the frame index of the end-effector
# For a fixed base robot, the frame index is one less than the body index. This is because
# the root body is not included in the returned Jacobians.
if robot.is_fixed_base:
ee_jacobi_idx = robot_entity_cfg.body_ids[0] - 1
else:
ee_jacobi_idx = robot_entity_cfg.body_ids[0]
Computing robot command#
The IK controller separates the operation of setting the desired command and computing the desired joint positions. This is done to allow for the user to run the IK controller at a different frequency than the robot’s control frequency.
The set_command() method takes in
the desired end-effector pose as a single batched array. The pose is specified in
the robot’s base frame.
# reset controller
diff_ik_controller.reset()
diff_ik_controller.set_command(ik_commands)
We can then compute the desired joint positions using the
compute() method.
The method takes in the current end-effector pose (in base frame), Jacobian, and
current joint positions. We read the Jacobian matrix from the robot’s data, which uses
its value computed from the physics engine.
# obtain quantities from simulation
jacobian = robot.root_view.get_jacobians()[:, ee_jacobi_idx, :, robot_entity_cfg.joint_ids]
ee_pose_w = wp.to_torch(robot.data.body_pose_w)[:, robot_entity_cfg.body_ids[0]]
root_pose_w = wp.to_torch(robot.data.root_pose_w)
joint_pos = wp.to_torch(robot.data.joint_pos)[:, robot_entity_cfg.joint_ids]
# compute frame in root frame
ee_pos_b, ee_quat_b = subtract_frame_transforms(
root_pose_w[:, 0:3], root_pose_w[:, 3:7], ee_pose_w[:, 0:3], ee_pose_w[:, 3:7]
)
# compute the joint commands
joint_pos_des = diff_ik_controller.compute(ee_pos_b, ee_quat_b, jacobian, joint_pos)
The computed joint position targets can then be applied on the robot, as done in the previous tutorials.
# apply actions
robot.set_joint_position_target_index(target=joint_pos_des, joint_ids=robot_entity_cfg.joint_ids)
scene.write_data_to_sim()
The Code Execution#
Now that we have gone through the code, let’s run the script and see the result:
./isaaclab.sh -p scripts/tutorials/05_controllers/run_diff_ik.py --robot franka_panda --num_envs 128
The script will start a simulation with 128 robots. The robots will be controlled using the IK controller. The current and desired end-effector poses should be displayed using frame markers. When the robot reaches the desired pose, the command should cycle through to the next pose specified in the script.
To stop the simulation, you can either close the window, or press Ctrl+C in the terminal.