Skip to content

Row schedule

27 · Convergence

row_schedule sets the order of a sweep. The default interleaved order sweeps every row family in each iteration. contact_then_internal runs all contact iterations first, then all iterations of the internal rows (drives, mimics, limits, joint friction). phased approximates the order PhysX uses for grasping: internal rows, then contacts, then velocity limits, each in its own launch. Two scenes at 4 iterations: a hinged arm resting on its lower limit under an 8 g/cm³ box, and a gripper whose fingers squeeze with effort-limited drive rows.

0.00 s

Cost

Time per step, CUDA graph replay, median of 7 interleaved rounds on one NVIDIA RTX PRO 6000 Blackwell Server Edition; ratios are against the first row. Solver allocation is the GPU memory the solver allocates at construction, before warm-up and graph capture. Scene: the hinged arm on its stop under the heavy box, 4 iterations.

Option1 world64 worlds1024 worldssolver allocation, 1024 worldsresult in this tutorial
row_schedule="interleaved"0.33 ms0.36 ms0.50 ms104 MB1.6° past the stop, box held
row_schedule="contact_then_internal"0.35 ms 1.08×0.39 ms 1.08×0.55 ms 1.11×104 MBlimit exact, box falls through
row_schedule="phased"0.34 ms 1.03×0.38 ms 1.04×0.52 ms 1.04×104 MBsame as interleaved

What to look at

  • Arm: interleaved and phased let the hinge sink 3.2° past its stop at first and settle 1.6° past it; contact_then_internal holds the limit exactly, but the box falls through the arm to the floor.
  • Grasp: interleaved and phased hold the box within a few millimetres through the shake; contact_then_internal drops it (258 mm of slip).

Key parameter

row_schedule

"interleaved" (default), "contact_then_internal" or "phased". Contact torsion and contact compliance need "interleaved", and sparse mass factors are only used with it.

Try changing

  • Raise iterations to 64: interleaved's overshoot past the stop nearly vanishes, and contact_then_internal still drops the box.
  • Use drive_mode="augmented" in the grasp and see which schedules still differ.

Concept

Row order

What it is

Projected Gauss-Seidel uses each row's latest neighbours, so the order of a sweep decides which constraints are satisfied best when the iterations run out: rows solved last win. Interleaving lets contacts and internal rows trade impulses every iteration.

In this scene

Solving every internal row after the contacts held the hinge limit exactly but let the loaded box pass through the arm, and released the grasp. Phased matched interleaved within 0.2 mm of slip. The box falls at 4, 8, 16 and 64 iterations alike: it is the order, not the iteration count.

Considerations

Cost
On the arm scene, phased was 3 to 4% slower per step than interleaved and contact_then_internal 8 to 11% slower (1, 64 and 1024 worlds).
Failure modes
During the contact iterations a link held by a limit or drive looks free, so the contacts pass the load into it, and the internal rows then take it away again. The load reaches the limit only through position correction: a body of mass M resting on a link of effective mass m sinks about (1 + M/m)·g·h²/baumgarte, at any iteration count, and a heavy enough body falls through.
Interactions
Torsion, compliance and sparse factors require interleaved.
Use it when
Keep interleaved. Use phased to match a PhysX-tuned grasp. Avoid contact_then_internal when contacts load a limit or drive; we found no scene where it was better.

The script

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

tutorials/27_row_schedule.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""row_schedule: the order of contact rows and internal rows (drives, limits, mimics)."""
import math
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
SCHEDULES = ["interleaved", "contact_then_internal", "phased"]
LIMIT = 0.05 # rad; the hinge stops this far below level
def tf(x, y, z):
return wp.transform((x, y, z), wp.quat_identity())
def build_stop():
"""A hinged arm resting on its lower limit, loaded by a heavy box near its tip."""
builder = newton.ModelBuilder()
builder.add_ground_plane()
arm = builder.add_link(xform=tf(0.25, 0.0, 0.3))
builder.add_shape_box(
arm, hx=0.25, hy=0.05, hz=0.01, cfg=newton.ModelBuilder.ShapeConfig(density=300.0)
)
hinge = builder.add_joint_revolute(
-1,
arm,
axis=(0.0, -1.0, 0.0),
parent_xform=tf(0.0, 0.0, 0.3),
child_xform=tf(-0.25, 0.0, 0.0),
limit_lower=-LIMIT,
limit_upper=0.5,
)
builder.add_articulation([hinge])
box = builder.add_body(xform=tf(0.42, 0.0, 0.36))
builder.add_shape_box(
box, hx=0.05, hy=0.05, hz=0.05, cfg=newton.ModelBuilder.ShapeConfig(density=8000.0)
)
return builder.finalize()
def build_gripper(mu=0.3):
"""Fingers squeezed by effort-limited drive rows lift and shake a box."""
builder = newton.ModelBuilder()
builder.add_ground_plane(cfg=newton.ModelBuilder.ShapeConfig(mu=0.8))
base = builder.add_link(xform=tf(0.0, 0.0, 0.45))
builder.add_shape_box(
base, hx=0.16, hy=0.03, hz=0.02, cfg=newton.ModelBuilder.ShapeConfig(density=500.0)
)
joints = [
builder.add_joint_prismatic(
-1,
base,
axis=newton.Axis.Z,
parent_xform=tf(0.0, 0.0, 0.45),
target_ke=4000.0,
target_kd=200.0,
armature=0.5,
)
]
finger_cfg = newton.ModelBuilder.ShapeConfig(density=800.0, mu=mu)
for side in (-1.0, 1.0):
finger = builder.add_link(xform=tf(0.12 * side, 0.0, 0.3))
builder.add_shape_box(finger, hx=0.01, hy=0.03, hz=0.12, cfg=finger_cfg)
joints.append(
builder.add_joint_prismatic(
base,
finger,
axis=newton.Axis.X,
parent_xform=tf(0.12 * side, 0.0, -0.3),
armature=0.05,
limit_lower=0.0 if side < 0 else -0.12,
limit_upper=0.12 if side < 0 else 0.0,
target_ke=2000.0,
target_kd=40.0,
effort_limit=40.0,
)
)
builder.add_articulation(joints)
box = builder.add_body(xform=tf(0.0, 0.0, 0.0601))
builder.add_shape_box(
box, hx=0.05, hy=0.05, hz=0.06, cfg=newton.ModelBuilder.ShapeConfig(density=600.0, mu=mu)
)
return builder.finalize(), base, box
def simulate_stop(rec, schedule):
model = build_stop()
solver = SolverSteel(model, enable_joint_limits=True, iterations=4, row_schedule=schedule)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(f'stop, "{schedule}"', model, scene="stop")
for _ in range(rec.frames(2.0)):
angle = state_0.joint_q.numpy()[0]
box_z = state_0.body_q.numpy()[1, 2]
run.frame(state_0, past_limit_deg=math.degrees(max(0.0, -LIMIT - angle)), box_height=box_z)
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)
def simulate_grasp(rec, schedule):
model, base, box = build_gripper()
solver = SolverSteel(
model,
enable_joint_limits=True,
articulation_constraint_max=128,
iterations=4,
drive_mode="rows",
row_schedule=schedule,
)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(f'grasp, "{schedule}"', model, scene="grasp")
for frame in range(rec.frames(7.0)):
t = frame / rec.fps
q = state_0.body_q.numpy()
run.frame(state_0, slip_mm=1000.0 * ((q[base, 2] - q[box, 2]) - (0.45 - 0.0601)))
lift = min(0.25, max(0.0, 0.25 * (t - 1.0)))
shake = 0.03 * math.sin(2.0 * math.pi * 3.0 * (t - 2.5)) if 2.5 < t < 5.5 else 0.0
close = (
0.1 if t > 0.3 else 0.0
) # targets past the box; the effort limit sets the grip force
control.joint_target_q.assign(np.array([lift + shake, close, -close], dtype=np.float32))
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 schedule in SCHEDULES:
simulate_stop(rec, schedule)
for schedule in SCHEDULES:
simulate_grasp(rec, schedule)
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