Crane wrecking ball
Many contacts
A UR10 grasps a toy crane and slews its ball into a dry-stacked miniature building.
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 crane_wrecking_ball# change solver settings liveuv run shelf tune crane_wrecking_ballSource: examples/shelf/02_many_contacts/crane_wrecking_ball/ · ported from newton/examples/robot/example_steel_wrecking_ball.py @ skild-ai/newton 7565a26b1
Presets
| Preset | Solver | Status | Checked |
|---|---|---|---|
steeldefault | steel | works | 2026-11-09, newton c42985a6c, NVIDIA RTX PRO 6000 Blackwell Server Edition |
Solvers with no row are untested on this example. The full settings are inpresets.toml.
Code
# 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 npimport warp as wp
import newton.examplesfrom common import presets
from newton.examples.robot import _wrecking_cranefrom 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)example = "crane_wrecking_ball"source = "newton/examples/robot/example_steel_wrecking_ball.py @ skild-ai/newton 7565a26b1"default = "steel"
[check]# Arguments `python -m shelf check` passes in test mode (from Newton's test_examples.py).args = []
[gallery]short = "A UR10 grasps a toy crane and slews its ball into a dry-stacked miniature building."frame = 720
[xpbd]status = "untested"
[vbd]status = "untested"
[mujoco]status = "untested"
[featherstone]status = "untested"
[semi_implicit]status = "untested"
[feather_pgs]status = "untested"
[steel]status = "works"sim = { substeps = 4 }solver = { solve_mode = "colored", iterations = 16, articulated_contact_response = "propagation" }checked = "2026-11-09, newton c42985a6c, NVIDIA RTX PRO 6000 Blackwell Server Edition"
[kamino]status = "untested"
[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.