Skip to content

Mass ratio

24 · Convergence

Four 1 kg boxes under one 20 kg box. Gauss-Seidel passes load down the stack one contact at a time, and the unequal masses slow that transfer, so at a fixed iteration budget the light boxes stay squeezed and the stack sags.

0.00 s

What to look at

  • 12 iterations: about 6 mm of sag.
  • Nesterov momentum halves it; 48 iterations bring it to about 1.4 mm.

Key parameter

iterations

Large mass ratios need more iterations, momentum, or substeps.

Try changing

  • Make the top box 100× heavier.
  • Put the heavy box at the bottom.

Concept

Mass ratios and conditioning

What it is

Each Gauss-Seidel row sees only its own effective mass. When neighbours differ greatly in mass, corrections from the heavy side dominate and the light bodies are corrected slowly, so a fixed number of sweeps leaves more error.

In this scene

A 20 kg box on four 1 kg boxes sags 6.3 mm at 12 iterations, 2.5 mm with Nesterov and 1.3 mm at 48.

Considerations

Cost
Iterations cost roughly linearly: 24 iterations take 1.5× the time of 12 in 1024 worlds.
Failure modes
Very large ratios can let light bodies sink or jitter.
Use it when
Keep ratios between touching bodies under about 10 where you can; otherwise add momentum, iterations or substeps.

The script

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

tutorials/24_mass_ratio.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Mass ratios: a 20 kg box on four 1 kg boxes at a fixed iteration budget."""
import newton
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
N, H = 5, 0.05
def build():
builder = newton.ModelBuilder()
builder.add_ground_plane(cfg=newton.ModelBuilder.ShapeConfig(mu=0.6))
for i in range(N):
density = 20000.0 if i == N - 1 else 1000.0 # the top box is 20x heavier
box = builder.add_body(
xform=wp.transform((0.0, 0.0, H + 2 * H * i + 0.0002 * i), wp.quat_identity())
)
builder.add_shape_box(
box, hx=H, hy=H, hz=H, cfg=newton.ModelBuilder.ShapeConfig(density=density, mu=0.6)
)
return builder.finalize()
def simulate(rec, label, **options):
model = build()
solver = SolverSteel(model, **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 _ in range(rec.frames(4.0)):
top = state_0.body_q.numpy()[N - 1, 2]
run.frame(state_0, sag_mm=1000.0 * (H * (2 * N - 1) - top))
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, "iterations=12 (default)")
simulate(rec, "iterations=12, nesterov_momentum=True", nesterov_momentum=True)
simulate(rec, "iterations=48", iterations=48)
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