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:

  1. Sample surface points and find antipodal contact pairs on the full mesh or an annotated region.

  2. Construct 6-DoF grasp frames that align the gripper opening axis with each contact pair and respect the requested approach direction.

  3. 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:

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:

  1. Load the mesh file via trimesh.

  2. Launch a browser-based annotator (default port 15531).

  3. Open http://localhost:15531 in your browser, use Rect Select Region to highlight the graspable area, then click Confirm Selection.

  4. 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
CLI options#

Option

Default

Description

--mesh_path

(required)

Path to the mesh file (.ply, .obj, .stl, etc.).

--viser_port

15531

Port for the browser-based annotation UI.

--n_sample

20000

Number of surface points to sample for antipodal pair detection.

--max_length

0.1

Maximum distance (metres) between antipodal pairs; should match the gripper’s maximum opening width.

--min_length

0.001

Minimum distance (metres) between antipodal pairs; filters out degenerate pairs.

--device

cpu

Compute device (cpu or cuda).