Robot allegro hand
Articulations
An Allegro hand articulation from USD.
steel preset (the default), recordedon an NVIDIA RTX PRO 6000 Blackwell.Run it
cd examples# the default preset (steel), then the othersuv run shelf play robot_allegro_handuv run shelf play robot_allegro_hand --solver mujocouv run shelf play robot_allegro_hand --solver kamino# change solver settings liveuv run shelf tune robot_allegro_handSource: examples/shelf/07_articulations/robot_allegro_hand/ · ported from newton/examples/robot/example_robot_allegro_hand.py @ skild-ai/newton 7565a26b1
Presets
| Preset | Solver | Status | Checked |
|---|---|---|---|
steeldefault | steel | works | 2026-11-04, newton 7565a26b1, NVIDIA GeForce RTX 3090 |
mujoco | mujoco | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
kamino | kamino | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
Solvers with no row are untested on this example. The full settings are inpresets.toml.
Code
# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers# SPDX-License-Identifier: Apache-2.0# Modified for Steel Shelf: preset-driven execution and shared runner integration.
############################################################################ Example Robot Allegro Hand## Shows how to set up a simulation of a Allegro hand articulation# from a USD file using newton.ModelBuilder.add_usd().# We also apply a sinusoidal trajectory to the joint targets and# apply a continuous rotation to the fixed root joint in the form# of the joint parent transform. The solver is updated# about this change in the joint parent transform by calling# self.solver.notify_model_changed(ModelFlags.JOINT_PROPERTIES).## Command: python -m shelf run robot_allegro_hand --world-count 16# python -m shelf run robot_allegro_hand --solver kamino############################################################################
import numpy as npimport warp as wp
import newtonimport newton.examplesfrom newton import JointTargetMode, ModelFlagsfrom common import presets
@wp.kerneldef move_hand( joint_q_start: wp.array[wp.int32], joint_limit_lower: wp.array[wp.float32], joint_limit_upper: wp.array[wp.float32], sim_time: wp.array[wp.float32], sim_dt: float, hand_rotation: wp.quat, # outputs joint_target_q: wp.array[wp.float32], joint_parent_xform: wp.array[wp.transform],): world_id = wp.tid() root_joint_id = world_id * 22 t = sim_time[world_id]
root_dof_start = joint_q_start[root_joint_id]
# animate the finger joints for i in range(20): di = root_dof_start + i target = wp.sin(t + float(i * 6) * 0.1) * 0.08 + 0.3 joint_target_q[di] = wp.clamp(target, joint_limit_lower[di], joint_limit_upper[di])
# animate the root joint transform q = wp.quat_from_axis_angle(wp.vec3(1.0, 0.0, 0.0), wp.sin(t) * 0.1) root_xform = joint_parent_xform[root_joint_id] joint_parent_xform[root_joint_id] = wp.transform(root_xform.p, q * hand_rotation)
# update the sim time sim_time[world_id] += sim_dt
class Example: def __init__(self, viewer, args): self.preset = presets.load(__file__) newton.use_coord_layout_targets = True self.fps = self.preset.fps self.frame_dt = 1.0 / self.fps
self.sim_time = 0.0 self.sim_substeps = self.preset.substeps self.sim_dt = self.frame_dt / self.sim_substeps
self.solver_type = self.preset.name self.world_count = args.world_count if self.world_count is None: self.world_count = 100
self.viewer = viewer
self.device = wp.get_device()
self.hand_rotation = wp.normalize(wp.quat(0.21643, 0.706218, -0.648166, 0.185191))
allegro_hand = newton.ModelBuilder() if self.solver_type == "kamino": newton.solvers.SolverKamino.register_custom_attributes(allegro_hand) else: newton.solvers.SolverMuJoCo.register_custom_attributes(allegro_hand) allegro_hand.default_shape_cfg.ke = 1.0e3 allegro_hand.default_shape_cfg.kd = 1.0e2 allegro_hand.default_shape_cfg.margin = 0.005 allegro_hand.default_shape_cfg.gap = 0.015
asset_path = newton.utils.download_asset("wonik_allegro") asset_file = str(asset_path / "usd" / "allegro_left_hand_with_cube.usda") allegro_hand.add_usd( asset_file, xform=wp.transform(wp.vec3(0, 0, 0.5)), enable_self_collisions=False, ignore_paths=[".*Dummy", ".*CollisionPlane"], hide_collision_shapes=True, load_static_visual_shapes=False, )
# set joint targets and joint drive gains (only on hand, not the floating-body cube) for i in range(allegro_hand.joint_dof_count - 6): allegro_hand.joint_target_ke[i] = 150 allegro_hand.joint_target_kd[i] = 5 allegro_hand.joint_q[i] = 0.3 allegro_hand.joint_target_q[i] = 0.3 if allegro_hand.joint_label[i][-2:] == "_0": allegro_hand.joint_q[i] = 0.6 allegro_hand.joint_target_q[i] = 0.6 allegro_hand.joint_target_mode[i] = int(JointTargetMode.POSITION) if allegro_hand.joint_type[i] == newton.JointType.REVOLUTE: allegro_hand.joint_armature[i] = 1e-2
# Update root pose of the cube (free joint) q = np.array(allegro_hand.joint_q) q[-7:-4] += np.array([0.0, 0.0, 0.05]) q[-4:] = wp.quat_rpy(0.3, 0.5, 0.1) allegro_hand.joint_q = q.tolist()
builder = newton.ModelBuilder() builder.replicate(allegro_hand, self.world_count)
builder.default_shape_cfg.ke = 1.0e3 builder.default_shape_cfg.kd = 1.0e2 builder.add_ground_plane()
self.model = builder.finalize()
newton.eval_fk(self.model, self.model.joint_q, self.model.joint_qd, self.model) self.initial_world_positions = self.model.body_q.numpy()[:: allegro_hand.body_count, :3].copy()
# Find the cube body index (it's the last body in each world) self.cube_body_offset = allegro_hand.body_count - 1
self.world_time = wp.zeros(self.world_count, dtype=wp.float32)
self.solver = self.preset.make_solver(self.model)
self.state_0 = self.model.state() self.state_1 = self.model.state() self.control = self.model.control() self.collision_pipeline = newton.CollisionPipeline(self.model) self.contacts = self.collision_pipeline.contacts()
self.viewer.set_model(self.model)
self.capture()
def capture(self): self.graph = None with wp.ScopedCapture() as capture: self.simulate() self.graph = capture.graph
def simulate(self): if self.solver_type != "kamino": self.collision_pipeline.collide(self.state_0, self.contacts) for substep in range(self.sim_substeps): self.state_0.clear_forces()
# apply forces to the model for picking, wind, etc self.viewer.apply_forces(self.state_0)
wp.launch( move_hand, dim=self.world_count, inputs=[ self.model.joint_q_start, self.model.joint_limit_lower, self.model.joint_limit_upper, self.world_time, self.sim_dt, self.hand_rotation, ], outputs=[self.control.joint_target_q, self.model.joint_X_p], )
# Update the solver since we have updated the joint parent transforms. self.solver.notify_model_changed(ModelFlags.JOINT_PROPERTIES) if self.solver_type == "kamino": self.collision_pipeline.collide(self.state_0, self.contacts)
self.solver.step(self.state_0, self.state_1, self.control, self.contacts, self.sim_dt)
# swap states if self.sim_substeps % 2 == 1 and substep == self.sim_substeps - 1: # an odd substep count ends in state_1; copy it back so the captured graph keeps its buffers self.state_0.assign(self.state_1) else: self.state_0, self.state_1 = self.state_1, self.state_0
def step(self): if self.graph: wp.capture_launch(self.graph) else: self.simulate()
self.sim_time += self.frame_dt
def render(self): self.viewer.begin_frame(self.sim_time) self.viewer.log_state(self.state_0) self.viewer.log_contacts(self.contacts, self.state_0) self.viewer.end_frame()
def test_final(self): """Check compact viewer layout and stable hand and cube motion.""" # Scale the bound with the grid size so larger world counts remain valid. offset_limit = np.ceil(np.sqrt(self.world_count)) max_world_offset = float(np.abs(self.viewer.world_offsets.numpy()).max()) assert max_world_offset < offset_limit, ( f"World offsets reach {max_world_offset:g} m, expected less than {offset_limit:g} m for a compact hand layout" )
num_bodies_per_world = self.model.body_count // self.world_count cubes_held = 0
for i in range(self.world_count): world_offset = i * num_bodies_per_world world_pos = wp.vec3(*self.initial_world_positions[i])
# Test hand bodies - must stay near initial position hand_lower = world_pos - wp.vec3(0.5, 0.5, 0.5) hand_upper = world_pos + wp.vec3(0.5, 0.5, 0.5) hand_body_indices = np.arange(num_bodies_per_world - 1, dtype=np.int32) + world_offset newton.examples.test_body_state( self.model, self.state_0, f"hand bodies from world {i} are close to the initial position", lambda q, qd: newton.math.vec_inside_limits(q.p, hand_lower, hand_upper), # noqa: B023 indices=hand_body_indices, )
# Count cubes still in the hand — at least 50% must remain cube_body_idx = world_offset + self.cube_body_offset cube_lower = wp.vec3(world_pos.x - 0.5, world_pos.y - 0.5, 0.9) cube_upper = world_pos + wp.vec3(0.5, 0.5, 0.5) cube_pos = wp.vec3(*self.state_0.body_q.numpy()[cube_body_idx, :3]) if newton.math.vec_inside_limits(cube_pos, cube_lower, cube_upper): cubes_held += 1
held_ratio = cubes_held / self.world_count assert held_ratio >= 0.5, ( f"Only {cubes_held}/{self.world_count} ({held_ratio:.0%}) cubes stayed in the hand, expected at least 50%" )
@staticmethod def create_parser(): parser = newton.examples.create_parser() newton.examples.add_world_count_arg(parser) parser.set_defaults(world_count=None) return parser
if __name__ == "__main__": parser = Example.create_parser() viewer, args = newton.examples.init(parser)
newton.examples.run(Example(viewer, args), args)example = "robot_allegro_hand"source = "newton/examples/robot/example_robot_allegro_hand.py @ skild-ai/newton 7565a26b1"default = "steel"
[check]# Arguments `python -m shelf check` passes in test mode (from Newton's test_examples.py).args = ["--num-frames", "500"]
[gallery]short = "An Allegro hand articulation from USD."
[xpbd]status = "untested"
[vbd]status = "untested"
[mujoco]status = "works"sim = { fps = 50, substeps = 8 }# nconmax: max contacts per world. cone = "pyramidal" preserves the example's solref-inherited# grasp friction; its purpose is articulation control rather than kf mapping.solver = { solver = "newton", integrator = "implicitfast", njmax = 200, nconmax = 300, impratio = 20.0, cone = "pyramidal", iterations = 100, ls_iterations = 50, use_mujoco_contacts = false }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"
[featherstone]status = "untested"
[semi_implicit]status = "untested"
[feather_pgs]status = "untested"
[steel]status = "works"# 2 substeps x 2 iterations, immediate response: fastest setting with peak and mean penetration below MuJoCo's# (1 substep raises the mean to 0.8 mm). enable_joint_limits: the clamped finger targets reach the joint limits,# which MuJoCo enforces; with them on, finger tracking matches MuJoCo's. articulation_constraint_max: the immediate# response keeps contact and limit rows per world in this family; a hand and its cube use up to 102.sim = { fps = 50, substeps = 2 }solver = { solve_mode = "matrix_free", iterations = 2, articulated_contact_response = "immediate", articulation_constraint_max = 128, enable_joint_limits = true }checked = "2026-11-04, newton 7565a26b1, NVIDIA GeForce RTX 3090"
[kamino]status = "works"sim = { fps = 50, substeps = 8 }solver = { dynamics_solver = "dvi", sparse_dynamics = true, sparse_jacobian = true, dvi = { max_alternating_iterations = 8, bilateral_solve_interval = 8, bilateral_solver_type = "LLTBRCM", omega = 1.2, contact_warmstart_method = "key_and_position_with_net_force_backup_and_tangential_net_force", use_schur_complement = true } }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"# Newton test_examples.py tests the Kamino variant with these arguments.check = { args = ["--num-frames", "500", "--world-count", "1"] }
[implicit_mpm]status = "untested"
[style3d]status = "untested"Steel docs and tutorials are licensed under Apache-2.0. Third-party notices. "Skild", "Skild AI" and the Skild logo are trademarks of Skild AI and are not licensed under Apache-2.0.