Skip to content

Newton cradle

Basic contacts

A five-ball Newton's cradle: one ball swings in, one swings out.

0.00 s
The steel preset (the default), recordedon an NVIDIA RTX PRO 6000 Blackwell.

Run it

Terminal window
cd examples
# the default preset (steel), then the others
uv run shelf play newton_cradle
uv run shelf play newton_cradle --solver xpbd
uv run shelf play newton_cradle --solver vbd
uv run shelf play newton_cradle --solver mujoco
uv run shelf play newton_cradle --solver featherstone
uv run shelf play newton_cradle --solver kamino
# change solver settings live
uv run shelf tune newton_cradle

Source: examples/shelf/01_basic_contacts/newton_cradle/ · ported from newton/examples/contacts/example_newton_cradle.py @ skild-ai/newton 7565a26b1

Presets

PresetSolverStatusChecked
xpbdxpbdworks2026-11-13, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition
vbdvbdworks2026-11-13, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition
mujocomujocoworks2026-11-13, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition
featherstonefeatherstoneworks2026-11-13, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition
kaminokaminoworks2026-11-13, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition

Solvers with no row are untested on this example. The full settings are inpresets.toml.

Code

examples/shelf/01_basic_contacts/newton_cradle/example.py
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
# Modified for Steel Shelf: preset-driven execution and shared runner integration. The balls hang
# 5 mm apart on rubbing hinges, as in MuJoCo's speculative-contact cradle (model/speculative/newton.xml).
###########################################################################
# Example Newton Cradle
#
# Builds a five-ball Newton's cradle with revolute-joint pendulums and
# sphere-sphere contact. Each impact is one pair of balls: the balls hang
# a small clearance apart, and the contact gap lets the closing pair touch
# within one substep instead of overlapping.
#
# Command: python -m shelf run newton_cradle [--lifted-balls 2]
#
###########################################################################
from __future__ import annotations
import math
import warp as wp
import newton
import newton.examples
from common import presets
NUM_BALLS = 5
BALL_MASS = 1.0
BALL_RADIUS = 0.05
STRING_LENGTH = 1.0
INITIAL_ANGLE = math.radians(45.0)
GRAVITY = -9.81
# Touching balls make every impact a simultaneous chain of contacts, which a velocity solver shares
# out over all the balls; a clearance wider than one substep of travel resolves one pair at a time.
BALL_CLEARANCE = 0.005
# Coulomb hinge torque [N m]: holds a struck middle ball where it stops and slowly damps the swing.
HINGE_FRICTION = 0.08
NATIVE_CONTACT_SOLVERS = {"mujoco", "kamino"}
class Example:
def __init__(self, viewer, args):
self.preset = presets.load(__file__)
self.viewer = viewer
self.solver_name = self.preset.name
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
builder = newton.ModelBuilder(gravity=(0.0, 0.0, GRAVITY))
builder.rigid_gap = BALL_CLEARANCE
builder.default_shape_cfg.ke = 1.0e5
builder.default_shape_cfg.kd = 0.0
builder.default_shape_cfg.kf = 0.0
self.preset.apply_model(builder) # friction coefficient mu
builder.default_shape_cfg.restitution = 1.0
self.lifted_balls = args.lifted_balls if args is not None else 1
pitch = 2.0 * BALL_RADIUS + BALL_CLEARANCE
total_width = (NUM_BALLS - 1) * pitch
x_start = -0.5 * total_width
joint_indices = []
for i in range(NUM_BALLS):
pivot_x = x_start + i * pitch
angle = INITIAL_ANGLE if i < self.lifted_balls else 0.0
ball_pos = wp.vec3(
pivot_x + STRING_LENGTH * math.sin(angle),
0.0,
-STRING_LENGTH * math.cos(angle),
)
inertia = 0.4 * BALL_MASS * BALL_RADIUS * BALL_RADIUS
body = builder.add_link(
xform=wp.transform(p=ball_pos, q=wp.quat_identity()),
mass=BALL_MASS,
inertia=wp.mat33(inertia, 0.0, 0.0, 0.0, inertia, 0.0, 0.0, 0.0, inertia),
com=wp.vec3(0.0, 0.0, 0.0),
lock_inertia=True,
label=f"ball_{i}",
)
color_t = i / max(NUM_BALLS - 1, 1)
builder.add_shape_sphere(
body,
radius=BALL_RADIUS,
color=wp.vec3(0.05 + 0.1 * color_t, 0.2 + 0.6 * color_t, 0.9 - 0.55 * color_t),
)
builder.add_shape_capsule(
body,
xform=wp.transform(p=wp.vec3(0.0, 0.0, 0.5 * STRING_LENGTH), q=wp.quat_identity()),
radius=0.004,
half_height=0.5 * STRING_LENGTH - BALL_RADIUS,
as_site=True,
color=wp.vec3(0.55, 0.45, 0.35),
)
joint = builder.add_joint_revolute(
parent=-1,
child=body,
axis=wp.vec3(0.0, 1.0, 0.0),
parent_xform=wp.transform(p=wp.vec3(pivot_x, 0.0, 0.0), q=wp.quat_identity()),
child_xform=wp.transform(p=wp.vec3(0.0, 0.0, STRING_LENGTH), q=wp.quat_identity()),
limit_lower=-math.pi,
limit_upper=math.pi,
limit_ke=0.0,
limit_kd=0.0,
friction=HINGE_FRICTION,
label=f"string_{i}",
)
joint_indices.append(joint)
builder.add_shape_box(
-1,
xform=wp.transform(p=wp.vec3(0.0, 0.0, 0.0), q=wp.quat_identity()),
hx=0.5 * total_width + 0.12,
hy=0.015,
hz=0.015,
color=wp.vec3(0.3, 0.3, 0.3),
)
builder.add_articulation(joint_indices, label="cradle")
builder.color()
self.model = builder.finalize()
joint_q = self.model.joint_q.numpy()
joint_q[: self.lifted_balls] = INITIAL_ANGLE
self.model.joint_q.assign(joint_q)
# Keep model.body_q synchronized before solver construction; VBD reads
# these transforms during setup.
newton.eval_fk(self.model, self.model.joint_q, self.model.joint_qd, self.model)
if self.solver_name in NATIVE_CONTACT_SOLVERS:
self.collision_pipeline = None
self.contacts = None
else:
self.collision_pipeline = newton.CollisionPipeline(self.model)
self.contacts = self.collision_pipeline.contacts()
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.viewer.set_model(self.model)
self.viewer.set_camera(pos=wp.vec3(-0.25, -2.35, 0.25), pitch=-17.7, yaw=90.0)
self.capture()
def capture(self):
if wp.get_device().is_cuda:
with wp.ScopedCapture() as capture:
self.simulate()
self.graph = capture.graph
else:
self.graph = None
def simulate(self):
for substep in range(self.sim_substeps):
self.state_0.clear_forces()
self.viewer.apply_forces(self.state_0)
if self.solver_name in NATIVE_CONTACT_SOLVERS:
contacts = None
else:
self.collision_pipeline.collide(self.state_0, self.contacts)
contacts = self.contacts
self.solver.step(self.state_0, self.state_1, self.control, contacts, self.sim_dt)
if self.sim_substeps % 2 == 1 and substep == self.sim_substeps - 1:
# an odd count under graph capture: copy the last result into state_0 instead of swapping,
# so every replay of the captured graph starts from the newest state
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 test_final(self):
newton.examples.test_body_state(
self.model,
self.state_0,
"cradle remains in its planar workspace",
lambda q, qd: abs(q[0]) < 1.25 and abs(q[1]) < 1.0e-3 and abs(q[2]) < 1.1,
)
def render(self):
self.viewer.begin_frame(self.sim_time)
self.viewer.log_state(self.state_0)
if self.solver_name not in NATIVE_CONTACT_SOLVERS:
self.viewer.log_contacts(self.contacts, self.state_0)
self.viewer.end_frame()
@staticmethod
def create_parser():
parser = newton.examples.create_parser()
parser.add_argument(
"--lifted-balls", type=int, default=1, choices=range(1, NUM_BALLS), help="How many balls start lifted."
)
return parser
if __name__ == "__main__":
parser = Example.create_parser()
viewer, args = newton.examples.init(parser)
newton.examples.run(Example(viewer, args), args)