Parallel-Gripper Grasp Generation#
The GraspKit toolkit generates feasible grasp poses for parallel-jaw grippers from a target object’s triangle mesh. It supports programmatic antipodal sampling, browser-based grasp-region annotation, collision filtering, candidate ranking, and on-disk caching of sampled contact pairs.
This page also demonstrates how to execute a generated pose with a robot arm. It covers scene initialization, robot and object creation, grasp pose computation, and trajectory execution in the simulation loop.
Processing Pipeline#
Grasp generation has three stages:
Sample surface points and find antipodal contact pairs on the full mesh or an annotated region.
Construct 6-DoF grasp frames that align the gripper opening axis with each contact pair and respect the requested approach direction.
Remove candidates that collide with the object or ground, rank the remaining poses, and return the best candidates with their required opening lengths.
Tutorial Source#
The tutorial corresponds to the grasp_generator.py script in the scripts/tutorials/grasp directory.
Code for grasp_generator.py
1# ----------------------------------------------------------------------------
2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15# ----------------------------------------------------------------------------
16
17"""
18This script demonstrates the creation and simulation of a robot that grasps a rigid mug
19in a simulated environment using the SimulationManager and grasp planning utilities.
20"""
21
22from __future__ import annotations
23
24import argparse
25import numpy as np
26import time
27import torch
28
29from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
30from embodichain.lab.visualization import visualization_cfg_from_args
31from embodichain.lab.sim.objects import Robot, RigidObject
32from embodichain.compute.trajectory import interpolate_with_distance
33from embodichain.lab.sim.shapes import MeshCfg
34from embodichain.lab.sim.motion.solvers import URSolverCfg
35from embodichain.data import get_data_path
36from embodichain.lab.gym.utils.gym_utils import add_env_launcher_args_to_parser
37from embodichain.toolkits.graspkit import ParallelJawGripperModelCfg
38from dexsim.utility.path import get_resources_data_path
39from embodichain.utils import logger
40from embodichain.lab.sim.cfg import (
41 RenderCfg,
42 JointDrivePropertiesCfg,
43 RobotCfg,
44 LightCfg,
45 RigidBodyAttributesCfg,
46 RigidObjectCfg,
47 URDFCfg,
48)
49from embodichain.toolkits.graspkit.pg_grasp import (
50 AntipodalGraspPoseGenerator,
51 AntipodalGraspPoseGeneratorCfg,
52 GraspAnnotationCfg,
53 ParallelJawGraspCollisionCfg,
54)
55
56
57def parse_arguments():
58 """
59 Parse command-line arguments to configure the simulation.
60
61 Returns:
62 argparse.Namespace: Parsed arguments including number of environments and rendering options.
63 """
64 parser = argparse.ArgumentParser(
65 description="Create and simulate a robot in SimulationManager"
66 )
67 add_env_launcher_args_to_parser(parser)
68 return parser.parse_args()
69
70
71def initialize_simulation(args) -> SimulationManager:
72 """
73 Initialize the simulation environment based on the provided arguments.
74
75 Args:
76 args (argparse.Namespace): Parsed command-line arguments.
77
78 Returns:
79 SimulationManager: Configured simulation manager instance.
80 """
81 config = SimulationManagerCfg(
82 headless=True,
83 sim_device=args.device,
84 render_cfg=RenderCfg(renderer=args.renderer),
85 physics_dt=1.0 / 100.0,
86 arena_space=2.5,
87 visualization=visualization_cfg_from_args(args),
88 )
89 sim = SimulationManager(config)
90
91 light = sim.add_light(
92 cfg=LightCfg(
93 uid="main_light",
94 color=(0.6, 0.6, 0.6),
95 intensity=30.0,
96 init_pos=(1.0, 0, 3.0),
97 )
98 )
99
100 return sim
101
102
103def create_robot(sim: SimulationManager, position=[0.0, 0.0, 0.0]) -> Robot:
104 """
105 Create and configure a robot with an arm and a dexterous hand in the simulation.
106
107 Args:
108 sim (SimulationManager): The simulation manager instance.
109
110 Returns:
111 Robot: The configured robot instance added to the simulation.
112 """
113 # Retrieve URDF paths for the robot arm and hand
114 ur10_urdf_path = get_data_path("UniversalRobots/UR10/UR10.urdf")
115 gripper_urdf_path = get_data_path("DH_PGC_140_50_M/DH_PGC_140_50_M.urdf")
116 # Configure the robot with its components and control properties
117 cfg = RobotCfg(
118 uid="UR10",
119 urdf_cfg=URDFCfg(
120 components=[
121 {"component_type": "arm", "urdf_path": ur10_urdf_path},
122 {"component_type": "hand", "urdf_path": gripper_urdf_path},
123 ]
124 ),
125 drive_pros=JointDrivePropertiesCfg(
126 stiffness={"Joint[0-9]": 1e4, "FINGER[1-2]": 1e3},
127 damping={"Joint[0-9]": 1e3, "FINGER[1-2]": 1e2},
128 max_effort={"Joint[0-9]": 1e5, "FINGER[1-2]": 1e4},
129 drive_type="force",
130 ),
131 control_parts={
132 "arm": ["Joint[0-9]"],
133 "hand": ["FINGER[1-2]"],
134 },
135 solver_cfg={
136 "arm": URSolverCfg(
137 ur_type="ur10",
138 tcp=[
139 [0.0, 1.0, 0.0, 0.0],
140 [-1.0, 0.0, 0.0, 0.0],
141 [0.0, 0.0, 1.0, 0.14],
142 [0.0, 0.0, 0.0, 1.0],
143 ],
144 )
145 },
146 init_qpos=[0.0, -np.pi / 2, -np.pi / 2, np.pi / 2, -np.pi / 2, 0.0, 0.0, 0.0],
147 init_pos=position,
148 )
149 return sim.add_robot(cfg=cfg)
150
151
152def create_obj(sim: SimulationManager):
153 mug_cfg = RigidObjectCfg(
154 uid="table",
155 shape=MeshCfg(
156 fpath=get_resources_data_path("Model", "BakeTexture", "hdr_color_mesh.ply"),
157 ),
158 attrs=RigidBodyAttributesCfg(
159 mass=0.01,
160 dynamic_friction=0.97,
161 static_friction=0.99,
162 ),
163 max_convex_hull_num=16,
164 acd_method="vhacd",
165 init_pos=[0.55, 0.0, 0.08],
166 init_rot=[0.0, 0.0, 0.0],
167 )
168 mug = sim.add_rigid_object(cfg=mug_cfg)
169 return mug
170
171
172def get_grasp_traj(sim: SimulationManager, robot: Robot, grasp_xpos: torch.Tensor):
173 num_envs = sim.num_envs
174 rest_arm_qpos = robot.get_qpos("arm")
175
176 approach_xpos = grasp_xpos.clone()
177 approach_xpos[:, 2, 3] += 0.1
178
179 _, qpos_approach = robot.compute_ik(
180 pose=approach_xpos, joint_seed=rest_arm_qpos, name="arm"
181 )
182 _, qpos_grasp = robot.compute_ik(
183 pose=grasp_xpos, joint_seed=qpos_approach, name="arm"
184 )
185 hand_open_qpos = torch.tensor([0.00, 0.00], dtype=torch.float32, device=sim.device)
186 hand_close_qpos = torch.tensor(
187 [0.025, 0.025], dtype=torch.float32, device=sim.device
188 )
189
190 arm_trajectory = torch.cat(
191 [
192 rest_arm_qpos[:, None, :],
193 qpos_approach[:, None, :],
194 qpos_grasp[:, None, :],
195 qpos_grasp[:, None, :],
196 qpos_approach[:, None, :],
197 rest_arm_qpos[:, None, :],
198 ],
199 dim=1,
200 )
201 hand_trajectory = torch.cat(
202 [
203 hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
204 hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
205 hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
206 hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
207 hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
208 hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
209 ],
210 dim=1,
211 )
212 all_trajectory = torch.cat([arm_trajectory, hand_trajectory], dim=-1)
213 interp_trajectory = interpolate_with_distance(
214 trajectory=all_trajectory, interp_num=200, device=sim.device
215 )
216 return interp_trajectory
217
218
219if __name__ == "__main__":
220 import time
221
222 args = parse_arguments()
223 sim = initialize_simulation(args)
224 robot = create_robot(sim, position=[0.0, 0.0, 0.0])
225 obj = create_obj(sim)
226
227 # get mug grasp pose
228 if not args.headless:
229 sim.open_window()
230
231 # Annotate part of the mug to be grasped by following the instructions in the visualization window:
232 # 1. View grasp object in browser (e.g http://localhost:11801)
233 # 2. press 'Rect Select Region', select grasp region
234 # 3. press 'Confirm Selection' to finish grasp region selection.
235
236 start_time = time.time()
237
238 # Construct one standalone generator. The same instance can also be passed
239 # to AtomicActionEngine(grasp_pose_generators={"hand": grasp_generator}).
240 grasp_generator = AntipodalGraspPoseGenerator(
241 ParallelJawGripperModelCfg(
242 model_id="tutorial_parallel_jaw",
243 min_opening_width=0.003,
244 max_opening_width=0.088,
245 finger_length=0.078,
246 ),
247 algorithm_cfg=AntipodalGraspPoseGeneratorCfg(
248 sample_count=10_000,
249 max_candidates=30,
250 ),
251 collision_cfg=ParallelJawGraspCollisionCfg(
252 point_sample_density=0.012,
253 filter_ground_collision=True,
254 ),
255 annotation_cfg=GraspAnnotationCfg(
256 selection_mode="whole_mesh",
257 viser_port=11801,
258 ),
259 )
260
261 # Extract target-local geometry. The generator, rather than the target,
262 # owns all algorithm and end-effector parameters.
263 vertices = obj.get_vertices(env_ids=[0], scale=True)[0]
264 triangles = obj.get_triangles(env_ids=[0])[0]
265
266 # Compute grasp poses per environment
267 approach_direction = torch.tensor(
268 [0, 0, -1], dtype=torch.float32, device=sim.device
269 )
270 obj_poses = obj.get_local_pose(to_matrix=True)
271 rest_xpos = robot.compute_fk(
272 qpos=robot.get_qpos("arm"), name="arm", to_matrix=True
273 )[0]
274 grasp_success, grasp_xpos, _ = grasp_generator.get_best_grasp_poses(
275 mesh_vertices=vertices,
276 mesh_triangles=triangles,
277 obj_poses=obj_poses,
278 approach_direction=approach_direction,
279 )
280 if not bool(grasp_success.all().item()):
281 logger.log_warning("At least one environment has no valid grasp pose.")
282 grasp_xpos = torch.where(
283 grasp_success[:, None, None],
284 grasp_xpos,
285 rest_xpos.expand_as(grasp_xpos),
286 )
287 cost_time = time.time() - start_time
288 logger.log_info(f"Get grasp pose cost time: {cost_time:.2f} seconds")
289
290 grab_traj = get_grasp_traj(sim, robot, grasp_xpos)
291 input("Press Enter to start the grab mug demo...")
292 n_waypoint = grab_traj.shape[1]
293 for i in range(n_waypoint):
294 robot.set_qpos(grab_traj[:, i, :])
295 sim.update(step=4)
296 time.sleep(1e-2)
297 input("Press Enter to exit the simulation...")
Tutorial Walkthrough#
Configuring the simulation#
Command-line arguments are parsed with argparse to select the number of parallel environments, the compute device, and optional rendering features such as renderer backend and headless mode.
def parse_arguments():
"""
Parse command-line arguments to configure the simulation.
Returns:
argparse.Namespace: Parsed arguments including number of environments and rendering options.
"""
parser = argparse.ArgumentParser(
description="Create and simulate a robot in SimulationManager"
)
add_env_launcher_args_to_parser(parser)
return parser.parse_args()
The parsed arguments are passed to initialize_simulation, which builds a SimulationManagerCfg and creates the SimulationManager instance. When ray tracing is enabled a directional cfg.LightCfg is also added to the scene.
def initialize_simulation(args) -> SimulationManager:
"""
Initialize the simulation environment based on the provided arguments.
Args:
args (argparse.Namespace): Parsed command-line arguments.
Returns:
SimulationManager: Configured simulation manager instance.
"""
config = SimulationManagerCfg(
headless=True,
sim_device=args.device,
render_cfg=RenderCfg(renderer=args.renderer),
physics_dt=1.0 / 100.0,
arena_space=2.5,
visualization=visualization_cfg_from_args(args),
)
sim = SimulationManager(config)
light = sim.add_light(
cfg=LightCfg(
uid="main_light",
color=(0.6, 0.6, 0.6),
intensity=30.0,
init_pos=(1.0, 0, 3.0),
)
)
return sim
Creating a robot and a target object#
A UR10 arm with a parallel-jaw gripper is created via SimulationManager.add_robot(). The gripper URDF and drive properties are configured so that the arm joints and finger joints can be controlled independently.
def create_robot(sim: SimulationManager, position=[0.0, 0.0, 0.0]) -> Robot:
"""
Create and configure a robot with an arm and a dexterous hand in the simulation.
Args:
sim (SimulationManager): The simulation manager instance.
Returns:
Robot: The configured robot instance added to the simulation.
"""
# Retrieve URDF paths for the robot arm and hand
ur10_urdf_path = get_data_path("UniversalRobots/UR10/UR10.urdf")
gripper_urdf_path = get_data_path("DH_PGC_140_50_M/DH_PGC_140_50_M.urdf")
# Configure the robot with its components and control properties
cfg = RobotCfg(
uid="UR10",
urdf_cfg=URDFCfg(
components=[
{"component_type": "arm", "urdf_path": ur10_urdf_path},
{"component_type": "hand", "urdf_path": gripper_urdf_path},
]
),
drive_pros=JointDrivePropertiesCfg(
stiffness={"Joint[0-9]": 1e4, "FINGER[1-2]": 1e3},
damping={"Joint[0-9]": 1e3, "FINGER[1-2]": 1e2},
max_effort={"Joint[0-9]": 1e5, "FINGER[1-2]": 1e4},
drive_type="force",
),
control_parts={
"arm": ["Joint[0-9]"],
"hand": ["FINGER[1-2]"],
},
solver_cfg={
"arm": URSolverCfg(
ur_type="ur10",
tcp=[
[0.0, 1.0, 0.0, 0.0],
[-1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.14],
[0.0, 0.0, 0.0, 1.0],
],
)
},
init_qpos=[0.0, -np.pi / 2, -np.pi / 2, np.pi / 2, -np.pi / 2, 0.0, 0.0, 0.0],
init_pos=position,
)
return sim.add_robot(cfg=cfg)
The target object (a mug) is loaded as a objects.RigidObject from a PLY mesh file:
def create_obj(sim: SimulationManager):
mug_cfg = RigidObjectCfg(
uid="table",
shape=MeshCfg(
fpath=get_resources_data_path("Model", "BakeTexture", "hdr_color_mesh.ply"),
),
attrs=RigidBodyAttributesCfg(
mass=0.01,
dynamic_friction=0.97,
static_friction=0.99,
),
max_convex_hull_num=16,
acd_method="vhacd",
init_pos=[0.55, 0.0, 0.08],
init_rot=[0.0, 0.0, 0.0],
)
mug = sim.add_rigid_object(cfg=mug_cfg)
return mug
Annotating and computing grasp poses#
Grasp generation is performed by
AntipodalGraspPoseGenerator.
It implements the robot-independent
GraspPoseGenerator contract and the
shared
ParallelJawGraspPoseGenerator base for
two-finger parallel-jaw grippers.
The generator owns the gripper model, algorithm, collision, and annotation
configuration. Target-local mesh vertices and triangles are supplied to each
call. This separation lets a handwritten environment call the service directly
and lets an AtomicActionEngine install the same instance under
grasp_pose_generators={"hand": generator}. Scene affordances do not own a
live generator or robot-specific parameters.
For each environment,
get_best_grasp_poses()
returns a success flag, a (4, 4) world-frame grasp pose, and the required
opening width. Antipodal contact pairs are cached and reused automatically.
Set GraspAnnotationCfg.selection_mode="interactive" to select a partial
region through Viser, or use "whole_mesh" for unattended generation.
The approach direction is the unit vector along which the gripper approaches the object. In this tutorial, we use a fixed approach direction (straight down in world frame) for simplicity, but it can be customized based on the task or object geometry.
# Construct one standalone generator. The same instance can also be passed
# to AtomicActionEngine(grasp_pose_generators={"hand": grasp_generator}).
grasp_generator = AntipodalGraspPoseGenerator(
ParallelJawGripperModelCfg(
model_id="tutorial_parallel_jaw",
min_opening_width=0.003,
max_opening_width=0.088,
finger_length=0.078,
),
algorithm_cfg=AntipodalGraspPoseGeneratorCfg(
sample_count=10_000,
max_candidates=30,
),
collision_cfg=ParallelJawGraspCollisionCfg(
point_sample_density=0.012,
filter_ground_collision=True,
),
annotation_cfg=GraspAnnotationCfg(
selection_mode="whole_mesh",
viser_port=11801,
),
)
# Extract target-local geometry. The generator, rather than the target,
# owns all algorithm and end-effector parameters.
vertices = obj.get_vertices(env_ids=[0], scale=True)[0]
triangles = obj.get_triangles(env_ids=[0])[0]
# Compute grasp poses per environment
approach_direction = torch.tensor(
[0, 0, -1], dtype=torch.float32, device=sim.device
)
obj_poses = obj.get_local_pose(to_matrix=True)
rest_xpos = robot.compute_fk(
qpos=robot.get_qpos("arm"), name="arm", to_matrix=True
)[0]
grasp_success, grasp_xpos, _ = grasp_generator.get_best_grasp_poses(
mesh_vertices=vertices,
mesh_triangles=triangles,
obj_poses=obj_poses,
approach_direction=approach_direction,
)
if not bool(grasp_success.all().item()):
logger.log_warning("At least one environment has no valid grasp pose.")
grasp_xpos = torch.where(
grasp_success[:, None, None],
grasp_xpos,
rest_xpos.expand_as(grasp_xpos),
)
cost_time = time.time() - start_time
logger.log_info(f"Get grasp pose cost time: {cost_time:.2f} seconds")
Building and executing the grasp trajectory#
Once a grasp pose is obtained, a waypoint trajectory is built that moves the arm from its rest configuration to an approach pose (offset above the grasp), down to the grasp pose, closes the fingers, lifts, and returns. The trajectory is interpolated for smooth motion and executed step-by-step in the simulation loop.
def get_grasp_traj(sim: SimulationManager, robot: Robot, grasp_xpos: torch.Tensor):
num_envs = sim.num_envs
rest_arm_qpos = robot.get_qpos("arm")
approach_xpos = grasp_xpos.clone()
approach_xpos[:, 2, 3] += 0.1
_, qpos_approach = robot.compute_ik(
pose=approach_xpos, joint_seed=rest_arm_qpos, name="arm"
)
_, qpos_grasp = robot.compute_ik(
pose=grasp_xpos, joint_seed=qpos_approach, name="arm"
)
hand_open_qpos = torch.tensor([0.00, 0.00], dtype=torch.float32, device=sim.device)
hand_close_qpos = torch.tensor(
[0.025, 0.025], dtype=torch.float32, device=sim.device
)
arm_trajectory = torch.cat(
[
rest_arm_qpos[:, None, :],
qpos_approach[:, None, :],
qpos_grasp[:, None, :],
qpos_grasp[:, None, :],
qpos_approach[:, None, :],
rest_arm_qpos[:, None, :],
],
dim=1,
)
hand_trajectory = torch.cat(
[
hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
],
dim=1,
)
all_trajectory = torch.cat([arm_trajectory, hand_trajectory], dim=-1)
interp_trajectory = interpolate_with_distance(
trajectory=all_trajectory, interp_num=200, device=sim.device
)
return interp_trajectory
Configuration#
Configuration ownership is split by meaning:
ParallelJawGripperModelCfgdescribes physical opening limits, finger dimensions, and palm depth. A concrete EEF name belongs only in itsmodel_id.AntipodalGraspPoseGeneratorCfgcontrols sample count, angular deviations, approach variants, and result count.ParallelJawGraspCollisionCfgcontrols collision margin, point density, decomposition cost, and ground filtering.GraspAnnotationCfgcontrols whole-mesh versus interactive selection, the Viser port, and explicit cache refresh.
Running the Tutorial#
To run the script, execute the following command from the project root:
python scripts/tutorials/grasp/grasp_generator.py
A simulation window will open showing the robot and the mug. The tutorial uses whole-mesh annotation by default and therefore does not require a browser.
You can customize the run with additional arguments:
python scripts/tutorials/grasp/grasp_generator.py --num_envs <n> --device <cuda/cpu> --renderer <legacy|hybrid|fast-rt|rt> --headless
The script computes a grasp pose, prints the elapsed time, and then waits for you to press Enter before executing the full grasp trajectory. Press Enter again to exit once the motion is complete.
Grasp Annotation CLI#
EmbodiChain provides a dedicated CLI for interactively annotating grasp regions on a mesh and caching the resulting antipodal point pairs, without requiring a full simulation environment.
The CLI constructs the same AntipodalGraspPoseGenerator
used by tasks and calls its prepare_mesh()
method; it does not use a separate annotation-time generator.
Basic usage:
embodichain annotate-grasp --mesh_path /path/to/object.ply
This will:
Load the mesh file via
trimesh.Launch a browser-based annotator (default port
15531).Open
http://localhost:15531in your browser, use Rect Select Region to highlight the graspable area, then click Confirm Selection.Compute antipodal point pairs on the selected region and cache them to disk.
Common options:
embodichain annotate-grasp \
--mesh_path /path/to/object.ply \
--viser_port 15531 \
--n_sample 20000 \
--max_length 0.1 \
--min_length 0.001
Option |
Default |
Description |
|---|---|---|
|
(required) |
Path to the mesh file ( |
|
|
Port for the browser-based annotation UI. |
|
|
Number of surface points to sample for antipodal pair detection. |
|
|
Maximum distance (metres) between antipodal pairs; should match the gripper’s maximum opening width. |
|
|
Minimum distance (metres) between antipodal pairs; filters out degenerate pairs. |
|
|
Compute device ( |