Skip to content

Mimic gripper

41 · Mimic joints and loop closures

A parallel gripper whose right finger mimics the left (q_R = −q_L, one bilateral row) squeezes a 0.7 kg box with 40 N, lifts it and shakes it at 3 Hz. The chart shows how far the box slips between the fingers. Mimic rows and the finger contacts share articulation_constraint_max, so the script raises it to 128.

0.00 s

What to look at

  • μ = 0.6: the box stays within a few millimetres, with patch or point friction.
  • μ = 0.15: the box slides out of the fingers as soon as the lift starts.

Key parameter

set_joint_mimic(follower, leader, coeffs)

q_follower = coeffs[0] + coeffs[1]·q_leader, within one articulation. The coefficients are read every step.

Try changing

  • Squeeze with 20 N.
  • Use drive_mode="rows" for the lift drive.

Concept

Mimic rows

What it is

A mimic joint is one bilateral row per coordinate that enforces q_f = a + b·q_l inside the articulation, solved with contacts. Coefficients are read every step, so they can change at runtime.

q_f = a + b · q_l

In this scene

With μ = 0.6 the box stays within a few millimetres through the shake; with μ = 0.15 it slides out of the fingers as the lift starts.

Considerations

Cost
One row per follower coordinate plus finger contacts: the gripper needs articulation_constraint_max = 128.
Accuracy
Rows converge with iterations; pre-elimination solves them more exactly.
Interactions
Both joints must be in one articulation and not BALL, FREE or DISTANCE.
Use it when
Parallel grippers and gear trains.

The script

cd tutorials && python 41_mimic_gripper.py prints each variant's final metric; --record out.json also writes the replay. Every tutorial imports replay.py from the same folder.

tutorials/41_mimic_gripper.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""A parallel gripper whose fingers are coupled by a mimic joint grasps, lifts and shakes a box."""
import math
import newton
import numpy as np
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
SQUEEZE = 40.0 # N on the leader finger
def tf(x, y, z):
return wp.transform((x, y, z), wp.quat_identity())
def build(mu):
builder = newton.ModelBuilder()
builder.add_ground_plane(cfg=newton.ModelBuilder.ShapeConfig(mu=0.8))
finger_cfg = newton.ModelBuilder.ShapeConfig(density=800.0, mu=mu)
base = builder.add_link(xform=tf(0.0, 0.0, 0.45))
builder.add_shape_box(
base, hx=0.16, hy=0.03, hz=0.02, cfg=newton.ModelBuilder.ShapeConfig(density=500.0)
)
lift = builder.add_joint_prismatic(
-1,
base,
axis=newton.Axis.Z,
parent_xform=tf(0.0, 0.0, 0.45),
target_ke=4000.0,
target_kd=200.0,
armature=0.5,
)
left = builder.add_link(xform=tf(-0.12, 0.0, 0.3))
builder.add_shape_box(left, hx=0.01, hy=0.03, hz=0.12, cfg=finger_cfg)
leader = builder.add_joint_prismatic(
base,
left,
axis=newton.Axis.X,
parent_xform=tf(-0.12, 0.0, -0.3),
armature=0.05,
limit_lower=0.0,
limit_upper=0.12,
)
right = builder.add_link(xform=tf(0.12, 0.0, 0.3))
builder.add_shape_box(right, hx=0.01, hy=0.03, hz=0.12, cfg=finger_cfg)
follower = builder.add_joint_prismatic(
base,
right,
axis=newton.Axis.X,
parent_xform=tf(0.12, 0.0, -0.3),
armature=0.05,
limit_lower=-0.12,
limit_upper=0.0,
)
builder.add_articulation([lift, leader, follower])
builder.set_joint_mimic(follower, leader, coeffs=(0.0, -1.0)) # q_right = -q_left
box = builder.add_body(xform=tf(0.0, 0.0, 0.0601))
builder.add_shape_box(
box, hx=0.05, hy=0.05, hz=0.06, cfg=newton.ModelBuilder.ShapeConfig(density=600.0, mu=mu)
)
return builder.finalize(), base, box
def simulate(rec, label, mu, **options):
model, base, box = build(mu)
# mimic rows and finger contacts share the articulation's row capacity
solver = SolverSteel(
model, enable_joint_limits=True, articulation_constraint_max=128, **options
)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(label, model)
for frame in range(rec.frames(7.0)):
t = frame / rec.fps
q = state_0.body_q.numpy()
run.frame(state_0, slip_mm=1000.0 * ((q[base, 2] - q[box, 2]) - (0.45 - 0.0601)))
lift = min(0.25, max(0.0, 0.25 * (t - 1.0)))
shake = 0.03 * math.sin(2.0 * math.pi * 3.0 * (t - 2.5)) if 2.5 < t < 5.5 else 0.0
control.joint_target_q.assign(np.array([lift + shake, 0.0, 0.0], dtype=np.float32))
control.joint_f.assign(np.array([0.0, SQUEEZE if t > 0.3 else -5.0, 0.0], dtype=np.float32))
for _ in range(SUBSTEPS):
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, DT)
state_0, state_1 = state_1, state_0
run.check(solver)
if __name__ == "__main__":
rec = Recorder()
simulate(rec, "mu 0.6, patch friction (default)", 0.6)
simulate(rec, "mu 0.6, point friction", 0.6, friction_anchor_beta=0.0)
simulate(rec, "mu 0.15, patch friction", 0.15)
rec.save()

Recorded with SolverSteel on Newton 1.9.0.dev0, Warp 1.19.0, NVIDIA RTX PRO 6000 Blackwell Server Edition. Replays read the state back every frame, so they are not a speed benchmark. Script · page text