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 time
 26from embodichain.cli.sim import add_sim_args_to_parser
 27
 28
 29def build_parser() -> argparse.ArgumentParser:
 30    """Build CLI options without initializing simulation resources."""
 31    parser = argparse.ArgumentParser(
 32        description="Create and simulate a robot in SimulationManager"
 33    )
 34    add_sim_args_to_parser(parser)
 35    return parser
 36
 37
 38def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
 39    """
 40    Parse command-line arguments to configure the simulation.
 41
 42    Returns:
 43        argparse.Namespace: Parsed arguments including number of environments and rendering options.
 44    """
 45    parser = build_parser()
 46    return parser.parse_args() if argv is None else parser.parse_args(argv)
 47
 48
 49if __name__ == "__main__":
 50    # Parse before importing optional simulation/planning dependencies.
 51    _cli_args = parse_arguments()
 52
 53
 54import numpy as np
 55import torch
 56
 57from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
 58from embodichain.lab.visualization import visualization_cfg_from_args
 59from embodichain.lab.sim.objects import Robot, RigidObject
 60from embodichain.compute.trajectory import interpolate_with_distance
 61from embodichain.lab.sim.shapes import MeshCfg, MeshCollisionCfg
 62from embodichain.lab.sim.motion.solvers import URSolverCfg
 63from embodichain.data import get_data_path
 64from embodichain.toolkits.graspkit import ParallelJawGripperModelCfg
 65from embodichain.utils import logger
 66from embodichain.lab.sim.cfg import (
 67    RenderCfg,
 68    physics_cfg_for_backend,
 69    JointDrivePropertiesCfg,
 70    RobotCfg,
 71    LightCfg,
 72    RigidBodyPhysicsCfg,
 73    RigidObjectCfg,
 74    URDFCfg,
 75)
 76from embodichain.toolkits.graspkit.pg_grasp import (
 77    AntipodalGraspPoseGenerator,
 78    AntipodalGraspPoseGeneratorCfg,
 79    GraspAnnotationCfg,
 80    ParallelJawGraspCollisionCfg,
 81)
 82
 83
 84def initialize_simulation(args) -> SimulationManager:
 85    """
 86    Initialize the simulation environment based on the provided arguments.
 87
 88    Args:
 89        args (argparse.Namespace): Parsed command-line arguments.
 90
 91    Returns:
 92        SimulationManager: Configured simulation manager instance.
 93    """
 94    config = SimulationManagerCfg(
 95        headless=True,
 96        device=args.device,
 97        render_cfg=RenderCfg(renderer=args.renderer),
 98        physics_cfg=physics_cfg_for_backend(args.physics),
 99        physics_dt=1.0 / 100.0,
100        arena_space=2.5,
101        visualization=visualization_cfg_from_args(args),
102    )
103    sim = SimulationManager(config)
104
105    light = sim.add_light(
106        cfg=LightCfg(
107            uid="main_light",
108            color=(0.6, 0.6, 0.6),
109            intensity=30.0,
110            init_pos=(1.0, 0, 3.0),
111        )
112    )
113
114    return sim
115
116
117def create_robot(sim: SimulationManager, position=[0.0, 0.0, 0.0]) -> Robot:
118    """
119    Create and configure a robot with an arm and a dexterous hand in the simulation.
120
121    Args:
122        sim (SimulationManager): The simulation manager instance.
123
124    Returns:
125        Robot: The configured robot instance added to the simulation.
126    """
127    # Retrieve URDF paths for the robot arm and hand
128    ur10_urdf_path = get_data_path("UniversalRobots/UR10/UR10.urdf")
129    gripper_urdf_path = get_data_path("DH_PGC_140_50_M/DH_PGC_140_50_M.urdf")
130    # Configure the robot with its components and control properties
131    cfg = RobotCfg(
132        uid="UR10",
133        urdf_cfg=URDFCfg(
134            components=[
135                {"component_type": "arm", "urdf_path": ur10_urdf_path},
136                {"component_type": "hand", "urdf_path": gripper_urdf_path},
137            ]
138        ),
139        joint_drive_props=JointDrivePropertiesCfg(
140            stiffness={"Joint[0-9]": 1e4, "FINGER[1-2]": 1e3},
141            damping={"Joint[0-9]": 1e3, "FINGER[1-2]": 1e2},
142            max_effort={"Joint[0-9]": 1e5, "FINGER[1-2]": 1e4},
143            drive_type="force",
144        ),
145        control_parts={
146            "arm": ["Joint[0-9]"],
147            "eef": ["FINGER[1-2]"],
148        },
149        solver_cfg={
150            "arm": URSolverCfg(
151                ur_type="ur10",
152                tcp=[
153                    [0.0, 1.0, 0.0, 0.0],
154                    [-1.0, 0.0, 0.0, 0.0],
155                    [0.0, 0.0, 1.0, 0.14],
156                    [0.0, 0.0, 0.0, 1.0],
157                ],
158            )
159        },
160        init_qpos=[0.0, -np.pi / 2, -np.pi / 2, np.pi / 2, -np.pi / 2, 0.0, 0.0, 0.0],
161        init_pos=position,
162    )
163    return sim.add_robot(cfg=cfg)
164
165
166def create_obj(sim: SimulationManager):
167    mug_cfg = RigidObjectCfg(
168        uid="table",
169        shape=MeshCfg(
170            fpath=get_data_path("BakeTextureObj/hdr_color_mesh.ply"),
171            collision=MeshCollisionCfg(
172                approximation="convex_decomposition",
173                max_hulls=16,
174                acd_method="coacd",
175            ),
176        ),
177        attrs=RigidBodyPhysicsCfg.from_dict(
178            {
179                "mass_props": {"mass": 0.01},
180                "material_props": {"dynamic_friction": 0.97, "static_friction": 0.99},
181            }
182        ),
183        init_pos=[0.55, 0.0, 0.08],
184        init_rot=[0.0, 0.0, 0.0],
185    )
186    mug = sim.add_rigid_object(cfg=mug_cfg)
187    return mug
188
189
190def get_grasp_traj(sim: SimulationManager, robot: Robot, grasp_xpos: torch.Tensor):
191    num_envs = sim.num_envs
192    rest_arm_qpos = robot.get_qpos("arm")
193
194    approach_xpos = grasp_xpos.clone()
195    approach_xpos[:, 2, 3] += 0.1
196
197    _, qpos_approach = robot.compute_ik(
198        pose=approach_xpos, joint_seed=rest_arm_qpos, name="arm"
199    )
200    _, qpos_grasp = robot.compute_ik(
201        pose=grasp_xpos, joint_seed=qpos_approach, name="arm"
202    )
203    hand_open_qpos = torch.tensor([0.00, 0.00], dtype=torch.float32, device=sim.device)
204    hand_close_qpos = torch.tensor(
205        [0.025, 0.025], dtype=torch.float32, device=sim.device
206    )
207
208    arm_trajectory = torch.cat(
209        [
210            rest_arm_qpos[:, None, :],
211            qpos_approach[:, None, :],
212            qpos_grasp[:, None, :],
213            qpos_grasp[:, None, :],
214            qpos_approach[:, None, :],
215            rest_arm_qpos[:, None, :],
216        ],
217        dim=1,
218    )
219    hand_trajectory = torch.cat(
220        [
221            hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
222            hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
223            hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
224            hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
225            hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
226            hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
227        ],
228        dim=1,
229    )
230    all_trajectory = torch.cat([arm_trajectory, hand_trajectory], dim=-1)
231    interp_trajectory = interpolate_with_distance(
232        trajectory=all_trajectory, interp_num=200, device=sim.device
233    )
234    return interp_trajectory
235
236
237if __name__ == "__main__":
238    import time
239
240    args = _cli_args
241    sim = initialize_simulation(args)
242    robot = create_robot(sim, position=[0.0, 0.0, 0.0])
243    obj = create_obj(sim)
244    sim.prepare()
245
246    # get mug grasp pose
247    if not args.headless:
248        sim.open_window()
249
250    # Annotate part of the mug to be grasped by following the instructions in the visualization window:
251    # 1. View grasp object in browser (e.g http://localhost:11801)
252    # 2. press 'Rect Select Region', select grasp region
253    # 3. press 'Confirm Selection' to finish grasp region selection.
254
255    start_time = time.time()
256
257    # Construct one standalone generator. The same instance can also be passed
258    # to AtomicActionEngine(grasp_pose_generators={"hand": grasp_generator}).
259    grasp_generator = AntipodalGraspPoseGenerator(
260        ParallelJawGripperModelCfg(
261            model_id="tutorial_parallel_jaw",
262            min_opening_width=0.003,
263            max_opening_width=0.088,
264            finger_length=0.078,
265        ),
266        algorithm_cfg=AntipodalGraspPoseGeneratorCfg(
267            sample_count=10_000,
268            max_candidates=30,
269        ),
270        collision_cfg=ParallelJawGraspCollisionCfg(
271            point_sample_density=0.012,
272            filter_ground_collision=True,
273        ),
274        annotation_cfg=GraspAnnotationCfg(
275            selection_mode="whole_mesh",
276            viser_port=11801,
277        ),
278    )
279
280    # Extract target-local geometry. The generator, rather than the target,
281    # owns all algorithm and end-effector parameters.
282    vertices = obj.get_vertices(env_ids=[0], scale=True)[0]
283    triangles = obj.get_triangles(env_ids=[0])[0]
284
285    # Compute grasp poses per environment
286    approach_direction = torch.tensor(
287        [0, 0, -1], dtype=torch.float32, device=sim.device
288    )
289    obj_poses = obj.get_local_pose(to_matrix=True)
290    rest_xpos = robot.compute_fk(
291        qpos=robot.get_qpos("arm"), name="arm", to_matrix=True
292    )[0]
293    grasp_success, grasp_xpos, _ = grasp_generator.get_best_grasp_poses(
294        mesh_vertices=vertices,
295        mesh_triangles=triangles,
296        obj_poses=obj_poses,
297        approach_direction=approach_direction,
298    )
299    if not bool(grasp_success.all().item()):
300        logger.log_warning("At least one environment has no valid grasp pose.")
301        grasp_xpos = torch.where(
302            grasp_success[:, None, None],
303            grasp_xpos,
304            rest_xpos.expand_as(grasp_xpos),
305        )
306    cost_time = time.time() - start_time
307    logger.log_info(f"Get grasp pose cost time: {cost_time:.2f} seconds")
308
309    grab_traj = get_grasp_traj(sim, robot, grasp_xpos)
310    input("Press Enter to start the grab mug demo...")
311    n_waypoint = grab_traj.shape[1]
312    for i in range(n_waypoint):
313        robot.set_qpos(grab_traj[:, i, :])
314        sim.update(step=4)
315        time.sleep(1e-2)
316    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.

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,
        device=args.device,
        render_cfg=RenderCfg(renderer=args.renderer),
        physics_cfg=physics_cfg_for_backend(args.physics),
        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},
            ]
        ),
        joint_drive_props=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]"],
            "eef": ["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_data_path("BakeTextureObj/hdr_color_mesh.ply"),
            collision=MeshCollisionCfg(
                approximation="convex_decomposition",
                max_hulls=16,
                acd_method="coacd",
            ),
        ),
        attrs=RigidBodyPhysicsCfg.from_dict(
            {
                "mass_props": {"mass": 0.01},
                "material_props": {"dynamic_friction": 0.97, "static_friction": 0.99},
            }
        ),
        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 <auto|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).