Skip to content

Crane wrecking ball

Many contacts

A UR10 grasps a toy crane and slews its ball into a dry-stacked miniature building.

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 crane_wrecking_ball
# change solver settings live
uv run shelf tune crane_wrecking_ball

Source: examples/shelf/02_many_contacts/crane_wrecking_ball/ · ported from newton/examples/robot/example_steel_wrecking_ball.py @ skild-ai/newton 7565a26b1

Presets

PresetSolverStatusChecked

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

Code

examples/shelf/02_many_contacts/crane_wrecking_ball/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.
"""Grasp and slew a passive toy crane to demolish a dry-stacked miniature building.
Command: python -m shelf run crane_wrecking_ball
"""
import numpy as np
import warp as wp
import newton.examples
from common import presets
from newton.examples.robot import _wrecking_crane
from newton.examples.robot._tabletop_demo import TabletopDemo
class Example(TabletopDemo):
task = "crane"
def __init__(self, viewer, args):
self.preset = presets.load(__file__)
# TabletopDemo (installed newton package) builds SolverSteel from these args, so fill them
# from the preset before handing args over. Its frame rate is fixed at 60 Hz.
args.substeps = self.preset.substeps
args.iterations = self.preset.solver.need("iterations")
args.solve_mode = self.preset.solver.need("solve_mode")
args.contact_response = self.preset.solver.need("articulated_contact_response")
super().__init__(viewer, args)
@staticmethod
def create_parser():
# TabletopDemo.create_parser() without its solver-tuning arguments (--substeps, --iterations,
# --solve-mode, --contact-response); those values now live in presets.toml.
parser = newton.examples.create_parser()
parser.set_defaults(num_frames=900)
return parser
def populate(self, builder, info, ee, down, fingers):
_wrecking_crane.populate(builder, info, ee, down, wp.get_device())
def reset(self):
super().reset()
self.ball_struck_building = False
self.slew_min = self.slew_max = 0.0
self.grasp_frames = 0
self.release_angle = None
self.max_release_drift = 0.0
def test_post_step(self):
"""Require grasp, slew and ball impact, not a spontaneous collapse."""
super().test_post_step()
poses = self.state_0.body_q.numpy()
q = poses[self.info["crane_slew"], 3:]
angle = float(2 * np.arctan2(q[2], q[3]))
self.slew_min = min(self.slew_min, angle)
self.slew_max = max(self.slew_max, angle)
if self.sim_time >= 6.3:
if self.release_angle is None:
self.release_angle = angle
self.max_release_drift = max(self.max_release_drift, abs(angle - self.release_angle))
count = int(self.contacts.rigid_contact_count.numpy()[0])
owners = self.model.shape_body.numpy()
shapes_a = self.contacts.rigid_contact_shape0.numpy()[:count]
shapes_b = self.contacts.rigid_contact_shape1.numpy()[:count]
pairs = {(int(owners[a]), int(owners[b])) for a, b in zip(shapes_a, shapes_b, strict=True) if a >= 0 and b >= 0}
def touching(a, b):
return (a, b) in pairs or (b, a) in pairs
if all(touching(finger, self.info["crane_grasp_body"]) for finger in self.info["crane_fingers"]):
self.grasp_frames += 1
self.ball_struck_building |= any(
touching(self.info["tracked_body"], body) for body in self.info["demolition_structure"]
)
if not self.ball_struck_building:
floors = self.info["demolition_upper_floors"]
drop = self.initial.body_q.numpy()[floors, 2] - poses[floors, 2]
assert np.max(np.abs(drop)) < 0.025, f"Building moved before ball impact: {drop}"
def test_final(self):
"""Verify that the ball stays attached and the upper floors collapse."""
removed = ("crane_long_jaw", "handle_support", "grasp_handle")
assert not any(name.endswith(removed) for name in self.model.shape_label), (
"Use stock fingers and the original counterweight"
)
self.check_capacity()
assert self.grasp_frames >= 20, (
f"Rear counterweight was not held between both fingers: {self.grasp_frames} frames"
)
assert self.slew_max - self.slew_min > 1.0, "Crane did not rotate through the strike"
assert self.ball_struck_building, "Ball never contacted the building"
assert self.max_release_drift < 0.05, f"Ball back-drove the crane after release: {self.max_release_drift} rad"
poses = self.state_0.body_q.numpy()
floors = self.info["demolition_upper_floors"]
drop = self.initial.body_q.numpy()[floors, 2] - poses[floors, 2]
assert np.count_nonzero(drop > 0.07) >= 3, f"Upper floor drops: {drop}"
for parent, child, a, b in self.info["wrecking_chain_anchors"]:
pa = np.asarray(wp.transform_point(wp.transform(*poses[parent]), wp.vec3(*a)))
pb = np.asarray(wp.transform_point(wp.transform(*poses[child]), wp.vec3(*b)))
assert np.linalg.norm(pa - pb) < 0.005, "Chain joint detached"
if __name__ == "__main__":
viewer, args = newton.examples.init(Example.create_parser())
example = Example(viewer, args)
newton.examples.run(example, args)