Skip to content

Relaxation and CFM

28 · Convergence

relaxation scales every Gauss-Seidel update: each row moves its impulse ω times the step that would satisfy it. ω above 1 over-relaxes and carries corrections further per sweep; below 1 under-relaxes. cfm adds a small constant to every row's effective-mass diagonal. The eight-box stack runs at 6 iterations with ω = 1, 1.4 and 1.8 and with cfm = 0.01; a second scene drops a ball with restitution 0.8 at ω = 1 and 1.8; a third rests a 1 kg sphere on the ground with and without contact_regularization, at cfm 1e-6 (default) and 1.

0.00 s

What to look at

  • Stack: sag falls from 4.3 mm (ω = 1) to 2.5 mm (1.4) and 1.1 mm (1.8).
  • cfm 0.01 leaves the sag at about 4.4 mm. cfm only enters the denominator of each update, so it scales each step by A/(A + cfm), with A the row's unit response (1/m for a contact in line with a body's center of mass, more off it); for these 1 kg boxes 0.01 changes each step by about 1% or less.
  • Resting sphere: without regularization, cfm 1 still leaves it on the surface, since the sweep converges to the same impulse. With contact_regularization=0.5 it settles 0.43 mm deep, and cfm 1, equal to its A, doubles that to 0.85 mm.
  • Bounce: at ω = 1.8 the second bounce reaches 23 cm instead of 32 cm, and the ball stops sooner.

Key parameter

relaxation / cfm

relaxation defaults to 1.0 and is read every step. cfm defaults to 1e-6 and is fixed at construction; drive rows of drive_mode="rows" use their exact response instead.

Try changing

  • Try ω = 1.8 on the four-bar loop closure tutorial at 2 iterations.
  • Raise cfm to 0.1 on the stack: the converged sag barely changes; lower iterations to 2 to see the slower sweep.

Concept

Successive over-relaxation and constraint force mixing

What it is

Over-relaxation multiplies each row's Gauss-Seidel correction by ω. On slowly converging problems, such as load running down a stack, it speeds convergence. Coupled friction modes are not scaled. cfm adds a value to the diagonal of ordinary scalar rows: it shortens each step like ω below 1 and leaves the answer of an unregularized row unchanged, but a row regularized by contact_regularization settles at a residual that grows with A + cfm.

λ ← λ + ω · Δλ

In this scene

ω = 1.8 cut the stack's sag from 4.3 to 1.1 mm at 6 iterations, but took 28% off the second bounce of a restitution 0.8 ball (32 → 23 cm). In separate runs ω = 1.6 cut a four-bar's closure gap at 2 iterations from 163 to 52 mm; ω = 1.4 left a 40-object pile with five times the residual kinetic energy of ω = 1; cfm from 1e-6 to 0.01 changed neither scene. On the regularized sphere cfm 1 doubled the resting penetration (0.43 → 0.85 mm).

Considerations

Cost
None: the same sweeps with a different step.
Interactions
cfm also floors the regularization of enable_bilateral_preelimination. Drive rows of drive_mode="rows" and the coupled friction modes' contact blocks leave it out, and compliant normal rows of contact_compliance replace it with their compliance term.
Accuracy
ω > 1 overshoots single contacts, which damps bounces and can leave piles jittering.
Use it when
Raise ω to 1.4–1.8 for stacks and loops at low iteration counts after checking bounces and resting piles. Leave cfm at its default; to soften contacts use contact_regularization or contact_compliance.

The script

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

tutorials/28_relaxation_cfm.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""relaxation over-relaxes each Gauss-Seidel update; cfm shifts each row's diagonal."""
import newton
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
N, H = 8, 0.05 # eight 10 cm boxes
R = 0.05 # m, a 1 kg sphere: its contact row's unit response is about 1 / (1 kg)
def build_stack():
builder = newton.ModelBuilder()
builder.add_ground_plane(cfg=newton.ModelBuilder.ShapeConfig(mu=0.6))
for i in range(N):
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(mu=0.6))
return builder.finalize()
def build_ball():
builder = newton.ModelBuilder()
cfg = newton.ModelBuilder.ShapeConfig(restitution=0.8)
builder.add_ground_plane(cfg=cfg)
ball = builder.add_body(xform=wp.transform((0.0, 0.0, 0.56), wp.quat_identity()))
builder.add_shape_sphere(ball, radius=0.06, cfg=cfg)
return builder.finalize()
def build_sphere():
builder = newton.ModelBuilder()
builder.add_ground_plane()
sphere = builder.add_body(xform=wp.transform((0.0, 0.0, R), wp.quat_identity()))
density = 1.0 / (4.0 / 3.0 * 3.14159265 * R**3)
builder.add_shape_sphere(sphere, radius=R, cfg=newton.ModelBuilder.ShapeConfig(density=density))
return builder.finalize()
def simulate(rec, label, scene, **options):
model = {"stack": build_stack, "ball": build_ball, "rest": build_sphere}[scene]()
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, scene=scene)
for _ in range(rec.frames(4.0 if scene == "stack" else 2.0)):
z = state_0.body_q.numpy()[:, 2]
if scene == "stack":
run.frame(state_0, sag_mm=1000.0 * (H * (2 * N - 1) - z[N - 1]))
elif scene == "rest":
run.frame(state_0, penetration_mm=1000.0 * (R - z[0]))
else:
run.frame(state_0, height=z[0] - 0.06)
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()
for relaxation in [1.0, 1.4, 1.8]:
default = " (default)" if relaxation == 1.0 else ""
simulate(
rec,
f"stack, relaxation={relaxation}{default}",
"stack",
iterations=6,
relaxation=relaxation,
)
simulate(rec, "stack, cfm=1e-2", "stack", iterations=6, cfm=1.0e-2)
for relaxation in [1.0, 1.8]:
simulate(rec, f"bounce, relaxation={relaxation}", "ball", relaxation=relaxation)
# cfm moves the resting point only of a regularized row: the residual is -g (A + cfm) lambda
simulate(rec, "rest, cfm=1", "rest", cfm=1.0)
for cfm in [1.0e-6, 1.0]:
simulate(
rec, f"rest, regularized, cfm={cfm:g}", "rest", cfm=cfm, contact_regularization=0.5
)
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