Skip to content

Gyroscopic stability

47 · Bodies

A T-handle spun about its intermediate principal axis in zero gravity flips over periodically (the Dzhanibekov effect). Steel integrates single free bodies with an energy-preserving update, so the flips stay periodic instead of drifting. The script sets angular damping to zero; the default is 0.05 s⁻¹, as in PhysX.

0.00 s

What to look at

  • The handle flips every 2.9 s.
  • Spin speed rises at each flip while energy and angular momentum stay constant.

Key parameter

steel:rigid_body_angular_damping

Per body [1/s], default 0.05. Damps free bodies and floating bases only.

Try changing

  • Spin about the long axis instead.
  • Restore the default damping and run for 60 s.

Concept

Energy-preserving gyroscopics

What it is

A spinning rigid body's angular momentum is constant, but its angular velocity changes as inertia rotates. An explicit update of that term adds energy. Steel updates single free bodies with Cayley steps that preserve kinetic energy exactly, using 1 to 32 microsteps.

In this scene

The T-handle flips about every 2.9 s and keeps doing so; spin rises to 9.4 rad/s at each flip.

Considerations

Cost
A few small 3×3 solves per body per step.
Interactions
Angular damping (default 0.05 s⁻¹) slowly removes energy; set it to 0 to compare.
Use it when
Automatic for free single bodies.

The script

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

tutorials/47_gyroscopic.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Free-body gyroscopics: a T-handle spun about its middle axis flips (Dzhanibekov effect)."""
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
def build():
builder = newton.ModelBuilder(gravity=(0.0, 0.0, 0.0))
SolverSteel.register_custom_attributes(builder)
body = builder.add_body(xform=wp.transform((0.0, 0.0, 0.6), wp.quat_identity()))
along_x = wp.quat_from_axis_angle(wp.vec3(0.0, 1.0, 0.0), np.pi / 2)
along_y = wp.quat_from_axis_angle(wp.vec3(1.0, 0.0, 0.0), np.pi / 2)
builder.add_shape_capsule(
body, xform=wp.transform((0.0, 0.0, 0.0), along_x), radius=0.02, half_height=0.18
)
builder.add_shape_capsule(
body, xform=wp.transform((0.0, 0.09, 0.0), along_y), radius=0.025, half_height=0.07
)
model = builder.finalize()
model.steel.rigid_body_angular_damping.fill_(0.0) # default 0.05 1/s, as in PhysX
# spin about the handle's middle axis
model.joint_qd.assign(np.array([0.0, 0.0, 0.0, 0.02, 8.0, 0.01], dtype=np.float32))
return model
def simulate(rec, label):
model = build()
solver = SolverSteel(model)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
newton.eval_fk(model, model.joint_q, model.joint_qd, state_0)
run = rec.variant(label, model)
for _ in range(rec.frames(8.0)):
x, y, z, w = state_0.body_q.numpy()[0, 3:]
handle_y = 1.0 - 2.0 * (x * x + z * z) # world y of the body's y axis
run.frame(state_0, handle_y=handle_y, omega=np.linalg.norm(state_0.body_qd.numpy()[0, 3:]))
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, "spin about the intermediate axis")
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