Concepts/Actuators
.. SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
.. SPDX-License-Identifier: CC-BY-4.0
.. currentmodule:: newton.actuators
Actuators
=========
.. experimental::
The actuator API may change without prior notice. Feedback is welcome โ
please file issues or discussion threads.
Actuators provide composable implementations that read physics simulation
state, compute effort, and accumulate (scatter-add) the effort into
control arrays for application to the simulation. The caller must zero the
output array before stepping actuators each frame. The simulator does not
need to be part of Newton: actuators are designed to be reusable anywhere the
caller can provide state arrays and consume effort.
Each :class:Actuator instance is vectorized: a single actuator object
operates on a batch of DOF indices in global state and control arrays, allowing
efficient integration into RL workflows with many parallel environments.
The goal is to provide canonical actuator models with support for
differentiability and graphable execution where the underlying
controller implementation supports it. Actuators are designed to be easy to
customize and extend for specific actuator models.
Architecture
------------
An actuator is composed from three building blocks, applied in this order:
.. code-block:: text
Actuator
โโโ Delay (optional: delays command inputs by N actuator timesteps)
โโโ Controller (control law that computes raw effort)
โโโ Clamping[] (clamps raw effort based on motor-limit modeling)
โโโ ClampingMaxEffort (ยฑmax_effort symmetric clamp)
โโโ ClampingDCMotor (velocity-dependent saturation)
โโโ ClampingPositionBased (position-dependent lookup table)
Delay
Optionally delays command inputs (control targets and feedforward terms)
by N actuator timesteps before they reach the controller, modeling
communication or processing latency. The delay always produces output;
when the buffer is empty or a DOF has `delay_steps == 0, the current
command inputs are used directly. When underfilled, the lag is clamped
to the available history so the oldest available entry is returned.
Controller
Computes raw actuator effort [N or Nยทm] from the current simulator state
and control targets. This is the actuator's control law โ for example PD,
PID, or neural-network-based control. See the individual controller class
documentation for the control-law equations.
Clamping
Clamps raw effort based on motor-limit modeling. This applies
post-controller output limits to the computed effort to model motor limits
such as saturation, back-EMF losses, performance envelopes, or
position-dependent effort limits. Multiple clamping stages can be combined
on a single actuator.
The per-step pipeline is:
.. code-block:: text
Delay read โ Controller โ Clamping โ Scatter-add โ State updates (controller + delay write)
Controllers and clamping objects are pluggable: implement the
:class:Controller or :class:Clamping base class to add new models.
.. note::
Current limitations: the first version does not include a transmission
model (gear ratios / linkage transforms), supports only single-input
single-output (SISO) actuators (one DOF per actuator), and does not model
actuator dynamics (inertia, friction, thermal effects).
Usage
-----
Actuators are registered during model construction with
:meth:~newton.ModelBuilder.add_actuator and are instantiated automatically
when the model is finalized:
.. testsetup:: actuator-usage
import warp as wp
import newton
from newton.actuators import (
Actuator, ClampingMaxEffort, ControllerPD, Delay,
)
builder = newton.ModelBuilder()
link = builder.add_link()
joint = builder.add_joint_revolute(parent=-1, child=link, axis=newton.Axis.Z)
builder.add_articulation([joint])
dof_index = builder.joint_qd_start[joint]
.. testcode:: actuator-usage
builder.add_actuator(
ControllerPD,
index=dof_index,
kp=100.0,
kd=10.0,
delay_steps=5,
clamping=[(ClampingMaxEffort, {"max_effort": 50.0})],
)
model = builder.finalize()
For manual construction (outside of :class:~newton.ModelBuilder), compose the
components directly:
.. testcode:: actuator-usage
indices = wp.array([0], dtype=wp.uint32)
kp = wp.array([100.0], dtype=wp.float32)
kd = wp.array([10.0], dtype=wp.float32)
max_e = wp.array([50.0], dtype=wp.float32)
actuator = Actuator(
indices,
controller=ControllerPD(kp=kp, kd=kd),
delay=Delay(delay_steps=wp.array([5], dtype=wp.int32), max_delay=5),
clamping=[ClampingMaxEffort(max_effort=max_e)],
control_target_pos_attr="joint_target_q",
control_target_vel_attr="joint_target_qd",
)
The simulator state and control objects do not need to be a full
:class:newton.Model / :class:newton.Control โ any objects exposingjoint_q, joint_qd, joint_target_q, joint_target_qd,joint_act (optional), and joint_f will do. This makes actuators
reusable from a custom simulator or test harness:
.. testcode:: actuator-usage
import types
sim_state = types.SimpleNamespace(
joint_q=wp.array([0.0], dtype=wp.float32),
joint_qd=wp.array([0.0], dtype=wp.float32),
)
sim_control = types.SimpleNamespace(
joint_target_q=wp.array([1.0], dtype=wp.float32),
joint_target_qd=wp.array([0.0], dtype=wp.float32),
joint_act=None,
joint_f=wp.zeros(1, dtype=wp.float32),
)
state_a = actuator.state()
state_b = actuator.state()
sim_control.joint_f.zero_()
actuator.step(sim_state, sim_control, state_a, state_b, dt=0.01)
Stateful Actuators
------------------
Controllers that maintain internal state (e.g. :class:ControllerPID with anControllerNeuralLSTM
integral accumulator, or :class: with hidden/cell state) andDelay
actuators with a :class: require explicit double-buffered stateActuator.state
management. Create two state objects with :meth: and swap them
after each step:
.. testcode:: actuator-usage
state_0 = model.actuators[0].state()
state_1 = model.actuators[0].state()
state = model.state()
control = model.control()
for step in range(3):
control.joint_f.zero_() # zero output before stepping actuators
model.actuators[0].step(state, control, state_0, state_1, dt=0.01)
state_0, state_1 = state_1, state_0
Stateless actuators (e.g. a plain PD controller without delay) do not require
state objects โ simply omit them:
.. testcode:: actuator-usage
# Build a stateless actuator (no delay, stateless controller)
b2 = newton.ModelBuilder()
lk = b2.add_link()
jt = b2.add_joint_revolute(parent=-1, child=lk, axis=newton.Axis.Z)
b2.add_articulation([jt])
b2.add_actuator(ControllerPD, index=b2.joint_qd_start[jt], kp=50.0)
m2 = b2.finalize()
m2.actuators[0].step(m2.state(), m2.control())
Neural-Network Checkpoints
--------------------------
Neural-network controllers (:class:ControllerNeuralMLP,ControllerNeuralLSTM
:class:) support two checkpoint backends: ONNX
checkpoints (.onnx) run on Warp-NN's Warp-backed runtime, while Torch
checkpoints use the Torch backend and require PyTorch.
Torch checkpoints are pt2 archives (.pt2) saved with torch.export.save.
Checkpoint metadata (scales and network configuration) is stored as a JSON
extra file:
.. code-block:: python
import json
import torch
exported = torch.export.export(net, example_inputs)
metadata = {"effort_scale": 2.0, "num_layers": 2, "hidden_size": 8}
torch.export.save(exported, "policy.pt2", extra_files={"metadata.json": json.dumps(metadata)})
:class:ControllerNeuralLSTM requires num_layers and hidden_size in
the metadata of both pt2 and ONNX checkpoints. Only legacy Torch checkpoints
may omit them: they contain the original module, whose torch.nn.LSTM
submodule is inspected directly, while torch.export flattens the network
into a computation graph that no longer exposes it.
Differentiability and Graph Capture
-----------------------------------
Whether an actuator supports differentiability and CUDA graph capture depends on
its controller. :class:ControllerPD and :class:ControllerPID are fullyActuator.is_graphable
graphable. For neural-network controllers it depends on the checkpoint
backend: ONNX checkpoints are graphable, while Torch checkpoints are not due
to framework interop overhead. :meth: returns True
when all components can be captured in a CUDA graph.
Available Components
--------------------
Delay
^^^^^
* :class:Delay โ circular-buffer delay for control targets (stateful).
Controllers
^^^^^^^^^^^
* :class:ControllerPD โ proportional-derivative control law (stateless).ControllerPID
* :class: โ proportional-integral-derivative control lawControllerNeuralMLP
(stateful: integral accumulator with anti-windup clamp).
* :class: โ MLP neural-network controllerControllerNeuralLSTM
(stateful: position/velocity history buffers).
* :class: โ LSTM neural-network controller
(stateful: hidden/cell state).
See the API documentation for each controller's control-law equations.
Clamping
^^^^^^^^
* :class:ClampingMaxEffort โ symmetric clamp to ยฑmax_effort per actuator.ClampingDCMotor
* :class: โ velocity-dependent effort saturation using the DCClampingPositionBased
motor effort-speed characteristic.
* :class: โ position-dependent effort limits via
interpolated lookup table (e.g. for linkage-driven joints).
Multiple clamping objects can be stacked on a single actuator; they are applied
in sequence.
Customization
-------------
Any actuator can be assembled from the existing building blocks โ mix and
match controllers, clamping stages, and delay to fit a specific use case.
When the built-in components are not sufficient, implement new ones by
subclassing :class:Controller or :class:Clamping.
For example, a custom controller needs to implement
:meth:~Controller.compute, :meth:~Controller.resolve_arguments,~Controller.is_stateful
:meth:, and :meth:~Controller.is_graphable:
.. code-block:: python
:caption: Skeleton โ the compute body is omitted; see existing
controllers for complete examples.
import warp as wp
from newton.actuators import Controller
class MyController(Controller):
@classmethod
def resolve_arguments(cls, args):
return {"gain": args.get("gain", 1.0)}
def __init__(self, gain: wp.array):
self.gain = gain
def is_stateful(self):
return False
def is_graphable(self):
return True
def compute(self, positions, velocities, target_pos, target_vel,
feedforward, pos_indices, vel_indices,
target_pos_indices, target_vel_indices,
forces, state, dt, device=None):
# Launch a Warp kernel that writes effort into forces
...
resolve_arguments maps user-provided keyword arguments (from~newton.ModelBuilder.add_actuator
:meth: or USD schemas) to constructor
parameters, filling in defaults where needed.
Similarly, a custom clamping stage subclasses :class:Clamping and implements~Clamping.modify_forces
:meth: (which reads effort from a source buffer and writes bounded effort to a destination buffer).
See Also
--------
* :mod:newton.actuators โ full API referencenewton.ModelBuilder.add_actuator
* :meth: โ registering actuators during
model construction
---
Concepts/Articulations
.. SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
.. SPDX-License-Identifier: CC-BY-4.0
.. currentmodule:: newton
.. _Articulations:
Articulations
=============
Articulations are a way to represent a collection of rigid bodies that are connected by joints.
.. _Articulation parameterization:
Generalized and maximal coordinates
-----------------------------------
There are two types of parameterizations to describe the configuration of an articulation:
generalized coordinates and maximal coordinates.
Generalized (sometimes also called "reduced") coordinates describe an articulation in terms of its joint positions and velocities.
For example, a double-pendulum articulation has two revolute joints, so its generalized state consists of two joint angles in :attr:newton.State.joint_q and two corresponding joint velocities in :attr:newton.State.joint_qd.
See the table below for the number of generalized coordinates for each joint type.
For a floating-base articulation (one connected to the world by a free joint), the generalized coordinates also include the base link pose: a 3D position and an XYZW quaternion.
Maximal coordinates describe the configuration of an articulation in terms of the body link positions and velocities.
Each rigid body's pose is represented by 7 parameters (3D position and XYZW quaternion) in :attr:newton.State.body_q,newton.State.body_qd
and its velocity by 6 parameters (3D linear and 3D angular) in :attr:.newton.State.body_qd
The linear component of :attr: is the world-frame velocity
of the body's center of mass. For public FREE and DISTANCE joints,newton.State.joint_qd
:attr: stores the child-COM twist in the joint parent
frame: the linear slice is child-COM velocity and the angular slice is angular
velocity in that same frame.
For floating-base articulations, the root FREE joint usually has the world
as parent, so this parent-frame twist matches the world-frame body twist in
practice.
To convert between these two representations, we use forward and inverse kinematics:
forward kinematics (:func:newton.eval_fk) converts generalized coordinates to maximal coordinates, and inverse kinematics (:func:newton.eval_ik) converts maximal coordinates to generalized coordinates.
Newton supports both parameterizations, and each solver chooses which one it treats as the primary articulation state representation.
For example, :class:~newton.solvers.SolverMuJoCo and :class:~newton.solvers.SolverFeatherstone~newton.solvers.SolverXPBD
use generalized coordinates, while :class:,~newton.solvers.SolverSemiImplicit
:class:, and :class:~newton.solvers.SolverVBDnewton.CollisionPipeline.collide
use maximal coordinates.
Note that collision detection via :meth: requires the maximal coordinates to be current in the state.
Cable joints
^^^^^^^^^^^^
:attr:newton.JointType.CABLE is represented in Newton's joint data model, but~newton.solvers.SolverVBD.JointSlot
it is not a conventional generalized-coordinate joint. Its four entries are
VBD constraint/material slots defined by
:class:: stretch (STRETCH, slot 0),
shear (SHEAR, slot 1), bend (BEND, slot 2), and
twist (TWIST, slot 3). These slots store independent per-cable stiffnessnewton.Model.joint_target_ke
and damping through
:attr: and :attr:newton.Model.joint_target_kd.
Generic joint storage allocates matching joint_q / joint_qd entries, but
they are not generalized coordinates or velocities that reconstruct the child
body pose.
Cable body poses and velocities are maximal-coordinate state stored in
:attr:newton.State.body_q and :attr:newton.State.body_qd, and are advanced bynewton.solvers.SolverVBD
:class:. Therefore :func:newton.eval_fk does not
update cable child body transforms from joint_q / joint_qd.
To showcase how an articulation state is initialized using reduced coordinates, let's consider an example where we create an articulation with a single revolute joint and initialize
its joint angle to 0.5 and joint velocity to 10.0:
.. testcode::
builder = newton.ModelBuilder()
# create an articulation with a single revolute joint
body = builder.add_link()
builder.add_shape_box(body) # add a shape to the body to add some inertia
joint = builder.add_joint_revolute(parent=-1, child=body, axis=wp.vec3(0.0, 0.0, 1.0)) # add a revolute joint to the body
builder.add_articulation([joint]) # create articulation from the joint
builder.joint_q[-1] = 0.5
builder.joint_qd[-1] = 10.0
model = builder.finalize()
state = model.state()
# The generalized coordinates have been initialized by the revolute joint:
assert all(state.joint_q.numpy() == [0.5])
assert all(state.joint_qd.numpy() == [10.0])
While the generalized coordinates have been initialized by the values we set through the :attr:newton.ModelBuilder.joint_q and :attr:newton.ModelBuilder.joint_qd definitions,
the body poses (maximal coordinates) are still initialized by the identity transform (since we did not provide a xform argument to the :meth:newton.ModelBuilder.add_link call, it defaults to the identity transform).
This is not a problem for generalized-coordinate solvers, as they do not use the body poses (maximal coordinates) to represent the state of the articulation but only the generalized coordinates.
In order to update the body poses (maximal coordinates), we need to use the forward kinematics function :func:newton.eval_fk:
.. testcode::
newton.eval_fk(model, state.joint_q, state.joint_qd, state)
Now, the body poses (maximal coordinates) have been updated by the forward kinematics and a maximal-coordinate solver can simulate the scene starting from these initial conditions.
As mentioned above, this call is not needed for generalized-coordinate solvers.
When declaring an articulation using the :class:~newton.ModelBuilder, the rigid body poses (maximal coordinates :attr:newton.State.body_q) are initialized by the xform argument:
.. testcode::
builder = newton.ModelBuilder()
tf = wp.transform(wp.vec3(1.0, 2.0, 3.0), wp.quat_from_axis_angle(wp.vec3(0.0, 0.0, 1.0), 0.5 * wp.pi))
body = builder.add_body(xform=tf)
builder.add_shape_box(body) # add a shape to the body to add some inertia
model = builder.finalize()
state = model.state()
# The body poses (maximal coordinates) are initialized by the xform argument:
assert all(state.body_q.numpy()[0] == [*tf])
# Note: add_body() automatically creates a free joint, so generalized coordinates exist:
assert len(state.joint_q) == 7 # 7 DOF for a free joint (3 position + 4 quaternion)
In this setup, we have a body with a box shape that both maximal-coordinate and generalized-coordinate solvers can simulate.
Since :meth:~newton.ModelBuilder.add_body automatically adds a free joint, the body already has the necessary degrees of freedom in generalized coordinates (:attr:newton.State.joint_q).
.. testcode::
builder = newton.ModelBuilder()
tf = wp.transform(wp.vec3(1.0, 2.0, 3.0), wp.quat_from_axis_angle(wp.vec3(0.0, 0.0, 1.0), 0.5 * wp.pi))
body = builder.add_link(xform=tf)
builder.add_shape_box(body) # add a shape to the body to add some inertia
joint = builder.add_joint_free(body) # add a free joint to connect the body to the world
builder.add_articulation([joint]) # create articulation from the joint
# The free joint's coordinates (joint_q) are initialized by its child body's pose,
# so we do not need to specify them here
# builder.joint_q[-7:] = *tf
model = builder.finalize()
state = model.state()
# The body poses (maximal coordinates) are initialized by the xform argument:
assert all(state.body_q.numpy()[0] == [*tf])
# Now, the generalized coordinates are initialized by the free joint:
assert len(state.joint_q) == 7
assert all(state.joint_q.numpy() == [*tf])
This scene can now be simulated by both maximal-coordinate and generalized-coordinate solvers.
.. _Kinematic links:
Kinematic links and bodies
--------------------------
Newton distinguishes three motion modes for rigid bodies:
Static
Does not move. Typical examples are world-attached shapes or links attached to world with a fixed joint.
Kinematic
Moves only from user-prescribed state updates. It can have joint DOFs (free, revolute, etc.), but external forces do not accelerate it.
Dynamic
Moves from forces, constraints, and contacts during solver integration.
Kinematic bodies are created through the is_kinematic=True flag on :meth:~newton.ModelBuilder.add_link~newton.ModelBuilder.add_body
or :meth:. Only root links (joint parent -1) may be kinematic.ValueError
Setting a non-root link to kinematic raises a :class: during articulation construction.
Common combinations
^^^^^^^^^^^^^^^^^^^
The following patterns are valid and commonly used:
1. Kinematic free-base body: add_body(is_kinematic=True) (free joint root).
2. Kinematic articulated root: root link is kinematic and attached to world with a non-fixed joint
(for example revolute), with dynamic descendants.
3. Static fixed-root body: root link is kinematic and attached to world with a fixed joint.
This has zero joint DOFs and behaves as static.
.. testcode:: articulation-kinematic-combinations
builder = newton.ModelBuilder()
# 1) Kinematic free-base body (add_body creates free joint + articulation)
kinematic_free = builder.add_body(is_kinematic=True, mass=1.0)
# 2) Kinematic revolute root with a dynamic child
root = builder.add_link(is_kinematic=True, mass=1.0)
child = builder.add_link(mass=1.0)
j_root = builder.add_joint_revolute(parent=-1, child=root, axis=newton.Axis.Z)
j_child = builder.add_joint_revolute(parent=root, child=child, axis=newton.Axis.Z)
builder.add_articulation([j_root, j_child])
# 3) Static fixed-root body (zero joint DOFs)
static_root = builder.add_link(is_kinematic=True, mass=1.0)
j_static = builder.add_joint_fixed(parent=-1, child=static_root)
builder.add_articulation([j_static])
model = builder.finalize()
.. list-table:: Static vs kinematic vs dynamic bodies/links
:header-rows: 1
:widths: 22 26 26 26
* - Property
- Static
- Kinematic
- Dynamic
* - Typical definition
- World-attached shape, or root link fixed to world
- is_kinematic=True on a root link/body with free/revolute/etc. joint
- Default link/body (no kinematic flag)
* - Joint DOFs
- 0 for fixed-root links
- Joint-dependent (free/revolute/D6/etc.)
- Joint-dependent
* - Position/velocity state
- Constant (not integrated)
- User-prescribed q/qd (or body_q/body_qd depending on solver coordinates)Mass and Inertia
- Integrated by solver from dynamics
* - Response to applied force/torque
- No acceleration
- No acceleration (force-immune for own motion)
- Accelerates according to dynamics
* - Collision/contact participation
- Yes (acts as obstacle/support)
- Yes (can push dynamic bodies while following prescribed motion)
- Yes
* - Mass/inertia (see :ref:)
- Not used for motion when fixed
- Preserved for body properties and future dynamic switching
- Fully used by dynamics
* - Mass matrix / constraint role
- No active DOFs when fixed to world
- Solver-dependent infinite-mass approximation along kinematic DOFs
- Standard articulated mass matrix
* - Typical applications
- Environment geometry, fixtures
- Conveyors, robot bases on trajectories, scripted mechanism roots
- Physically simulated robots and objects
Velocity consistency for prescribed motion
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For prescribed motion, it is up to the user to keep position and velocity updates consistent across time.
In particular, qd should be consistent with the finite-differenced motion implied by q.
For scalar coordinates, this is the familiar q_next = q + qd * dt relation; quaternion-based coordinates
(for example FREE/BALL joint) require manifold-consistent quaternion integration instead of direct addition.
When writing kinematic state values:
- For generalized-coordinate workflows, write :attr:newton.State.joint_q and :attr:newton.State.joint_qd,newton.eval_fk
then call :func: so maximal coordinates (for collisions and body-space consumers) are current.newton.State.body_q
- For maximal-coordinate workflows, write :attr: and :attr:newton.State.body_qd directly.
Rigid-body solver behavior
^^^^^^^^^^^^^^^^^^^^^^^^^^
The rigid-body solvers (:class:~newton.solvers.SolverMuJoCo,~newton.solvers.SolverFeatherstone
:class:, :class:~newton.solvers.SolverXPBD,~newton.solvers.SolverSemiImplicit
:class:, :class:~newton.solvers.SolverVBD)
support the same user-facing kinematic authoring model:
- Kinematic links keep their declared joint type (free/revolute/etc.).
- A kinematic root attached to world by a fixed joint remains fixed (zero DOFs).
- Kinematic links participate in collisions/contacts and can impart motion to dynamic bodies.
- Applied forces do not drive kinematic motion; motion is user-prescribed.
Implementation details differ by coordinate formulation:
- Generalized-coordinate solvers (:class:~newton.solvers.SolverMuJoCo,~newton.solvers.SolverFeatherstone
:class:) treat kinematic motion through prescribed joint state.~newton.solvers.SolverXPBD
- Maximal-coordinate solvers (:class:,~newton.solvers.SolverSemiImplicit
:class:, :class:~newton.solvers.SolverVBD)~newton.solvers.SolverXPBD
use prescribed body transforms/twists.
- Contact handling of kinematic bodies is not identical across the solvers. :class:,~newton.solvers.SolverVBD
:class:, :class:~newton.solvers.SolverMuJoCo, and~newton.solvers.SolverFeatherstone
:class: treat kinematic bodies like~newton.solvers.SolverSemiImplicit
infinite-mass colliders for contact response, while
:class: currently preserves prescribed state but
does not zero inverse mass/inertia inside its contact solver. Contacts against
kinematic bodies can therefore be softer under SemiImplicit.
In :class:~newton.solvers.SolverMuJoCo, kinematic DOFs are regularized with aKinematic Links and Fixed Roots <mujoco-kinematic-links-and-fixed-roots>
large internal armature value; see :ref: for details.
.. _Joint types:
Joint types
-----------
.. list-table::
:header-rows: 1
:widths: auto
:stub-columns: 0
* - Joint Type
- Description
- Coordinates in joint_q
- DOFs in joint_qd
* - JointType.PRISMATIC
- Prismatic (slider) joint with 1 linear degree of freedom
- 1
- 1
* - JointType.REVOLUTE
- Revolute (hinge) joint with 1 angular degree of freedom
- 1
- 1
* - JointType.BALL
- Ball (spherical) joint with quaternion state representation
- 4
- 3
* - JointType.FIXED
- Fixed (static) joint with no degrees of freedom
- 0
- 0
* - JointType.FREETwist conventions in Newton <Twist conventions>
- Free (floating) joint with 6 degrees of freedom in velocity space
- 7 (3D position + 4D quaternion)
- 6 (see :ref:)
* - JointType.DISTANCE
- Distance joint that keeps two bodies at a distance within its joint limits
- 7
- 6
* - JointType.D6
- Generic D6 joint with up to 3 translational and 3 rotational degrees of freedom
- up to 6
- up to 6
* - JointType.CABLE
- Cable joint with 2 linear material slots (stretch/shear) and 2 angular
material slots (bend/twist)
- 4
- 4
D6 joints are the most general joint type in Newton and can be used to represent any combination of translational and rotational degrees of freedom.
Prismatic, revolute, planar, and universal joints can be seen as special cases of the D6 joint.
For JointType.CABLE, both counts represent allocated material slots, notCable joints
generalized coordinates or velocity DOFs; see _.
Definition of joint_q
^^^^^^^^^^^^^^^^^^^^^^^^^
The :attr:newton.Model.joint_q array stores the default generalized joint positionsnewton.State.joint_q
for generalized-coordinate joints and is used to initialize :attr:.
Both arrays share the same per-joint layout.
For scalar-coordinate joints (for example this D6 joint), the positional coordinates can be queried as follows:
.. testsetup:: articulation-joint-layout
builder = newton.ModelBuilder()
body = builder.add_link()
builder.add_shape_box(body, hx=0.1, hy=0.1, hz=0.1)
joint = builder.add_joint_d6(
parent=-1,
child=body,
linear_axes=[newton.ModelBuilder.JointDofConfig(axis=newton.Axis.X, limit_lower=-0.5, limit_upper=0.5)],
angular_axes=[newton.ModelBuilder.JointDofConfig(axis=newton.Axis.Z, limit_lower=-1.0, limit_upper=1.0)],
)
builder.add_articulation([joint])
model = builder.finalize()
state = model.state()
control = model.control()
joint_id = 0
joint_q_start = model.joint_q_start.numpy()
joint_qd_start = model.joint_qd_start.numpy()
joint_target_q_start = model.joint_target_q_start.numpy()
joint_q = state.joint_q.numpy()
joint_qd = state.joint_qd.numpy()
joint_dof_dim = model.joint_dof_dim.numpy()
joint_axis = model.joint_axis.numpy()
joint_limit_lower = model.joint_limit_lower.numpy()
joint_target_q = control.joint_target_q.numpy()
joint_f = control.joint_f.numpy()
.. testcode:: articulation-joint-layout
q_start = joint_q_start[joint_id]
coord_count = joint_dof_dim[joint_id, 0] + joint_dof_dim[joint_id, 1]
# now the positional coordinates can be queried as follows:
q = joint_q[q_start : q_start + coord_count]
q0 = q[0]
q1 = q[1]
Definition of joint_qd
^^^^^^^^^^^^^^^^^^^^^^^^^^
The :attr:newton.Model.joint_qd array stores the default generalized joint velocitiesnewton.State.joint_qd
for generalized-coordinate joints and is used to initialize :attr:.newton.Control.joint_f
The generalized joint forces at :attr: use the same DOF order.
Several other arrays also use this same DOF-ordered layout, indexed from
:attr:newton.Model.joint_qd_start rather than :attr:newton.Model.joint_q_start.newton.Model.joint_axis
This includes :attr:, joint limits and other per-DOFnewton.ModelBuilder.JointDofConfig
properties defined via :class:, and thenewton.Control.joint_target_qd
velocity targets at :attr:.
The position targets at :attr:newton.Control.joint_target_q instead matchnewton.Model.joint_q
:attr: (coord layout) whennewton.use_coord_layout_targets
:attr: is True; index those withnewton.Model.joint_q_start
:attr:. Under the legacy default
(use_coord_layout_targets = False) the array is still DOF-shaped andnewton.Model.joint_qd_start
indexed via :attr: โ see themigration guide <joint-target-layout>
:ref: for details.
For every generalized-coordinate joint, these per-DOF arrays are stored
consecutively, with linear DOFs first and angular DOFs second. Use
:attr:newton.Model.joint_dof_dim to query how many of each a joint has.
The velocity DOFs for each joint can be queried as follows:
.. testcode:: articulation-joint-layout
qd_start = joint_qd_start[joint_id]
dof_count = joint_dof_dim[joint_id, 0] + joint_dof_dim[joint_id, 1]
# now the velocity DOFs can be queried as follows:
qd = joint_qd[qd_start : qd_start + dof_count]
qd0 = qd[0]
qd1 = qd[1]
# the generalized joint forces can be queried as follows:
f = joint_f[qd_start : qd_start + dof_count]
f0 = f[0]
f1 = f[1]
The same start index can be used to query other per-DOF arrays for that joint:
.. testcode:: articulation-joint-layout
num_linear_dofs = joint_dof_dim[joint_id, 0]
num_angular_dofs = joint_dof_dim[joint_id, 1]
# all per-DOF arrays for this joint start at this index:
dof_start = joint_qd_start[joint_id]
# position targets use the layout-aware mapping (aliases joint_q_start
# under newton.use_coord_layout_targets, joint_qd_start otherwise):
target_q_start = joint_target_q_start[joint_id]
# the axis vector for the first linear DOF
first_lin_axis = joint_axis[dof_start]
# the position target for this linear DOF
first_lin_target = joint_target_q[target_q_start]
# the joint limit of this linear DOF
first_lin_limit = joint_limit_lower[dof_start]
# the axis vector for the first angular DOF comes after all linear DOFs
first_ang_axis = joint_axis[dof_start + num_linear_dofs]
# the position target for this angular DOF
first_ang_target = joint_target_q[target_q_start + num_linear_dofs]
# the joint limit of this angular DOF
first_ang_limit = joint_limit_lower[dof_start + num_linear_dofs]
assert (num_linear_dofs, num_angular_dofs) == (1, 1)
assert np.allclose(first_lin_axis, [1.0, 0.0, 0.0])
assert np.allclose(first_ang_axis, [0.0, 0.0, 1.0])
assert np.allclose([first_lin_limit, first_ang_limit], [-0.5, -1.0])
Common articulation workflows
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Center joint_q between joint limits with Warp kernels
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
Joint limits are stored in DOF order (joint_qd layout), while joint_q stores generalized
joint coordinates (which may include quaternion coordinates for free/ball joints).
The pattern below sets each scalar coordinate to the midpoint between its lower and upper limits.
A robust pattern is:
1. Loop over joints.
2. Use Model.joint_qd_start to find the first DOF index for each joint.
3. Use Model.joint_dof_dim to get the number of linear and angular DOFs for that joint.
4. Use Model.joint_q_start to find where that joint starts in State.joint_q.
5. Center only scalar coordinates (for example, revolute/prismatic axes) and skip quaternion joints.
.. testsetup:: articulation-center-joint-q
builder = newton.ModelBuilder()
parent = builder.add_link()
child = builder.add_link(xform=wp.transform(wp.vec3(1.0, 0.0, 0.0), wp.quat_identity()))
builder.add_shape_box(parent, hx=0.1, hy=0.1, hz=0.1)
builder.add_shape_box(child, hx=0.1, hy=0.1, hz=0.1)
j0 = builder.add_joint_revolute(
parent=-1,
child=parent,
axis=wp.vec3(0.0, 0.0, 1.0),
limit_lower=-1.0,
limit_upper=1.0,
)
j1 = builder.add_joint_revolute(
parent=parent,
child=child,
axis=wp.vec3(0.0, 0.0, 1.0),
parent_xform=wp.transform(wp.vec3(1.0, 0.0, 0.0), wp.quat_identity()),
child_xform=wp.transform_identity(),
limit_lower=0.0,
limit_upper=2.0,
)
builder.add_articulation([j0, j1])
model = builder.finalize()
state = model.state()
.. testcode:: articulation-center-joint-q
@wp.kernel
def center_joint_q_from_limits(
joint_q_start: wp.array[wp.int32],
joint_qd_start: wp.array[wp.int32],
joint_dof_dim: wp.array2d[wp.int32],
joint_type: wp.array[wp.int32],
joint_limit_lower: wp.array[float],
joint_limit_upper: wp.array[float],
joint_q: wp.array[float],
):
joint_id = wp.tid()
# First DOF index for this joint in qd-order arrays (limits/axes/forces)
qd_begin = joint_qd_start[joint_id]
dof_count = joint_dof_dim[joint_id, 0] + joint_dof_dim[joint_id, 1]
# Start index for this joint in generalized coordinates q
q_begin = joint_q_start[joint_id]
# Skip free/ball joints because their q entries include quaternion coordinates.
jt = joint_type[joint_id]
if (
jt == newton.JointType.FREE
or jt == newton.JointType.BALL
or jt == newton.JointType.DISTANCE
):
return
# For scalar joints, q coordinates align with this joint's total DOF count.
for local_dof in range(dof_count):
qd_idx = qd_begin + local_dof
q_idx = q_begin + local_dof
lower = joint_limit_lower[qd_idx]
upper = joint_limit_upper[qd_idx]
if wp.isfinite(lower) and wp.isfinite(upper):
joint_q[q_idx] = 0.5 * (lower + upper)
# Launch over all joints in the model
wp.launch(
kernel=center_joint_q_from_limits,
dim=model.joint_count,
inputs=[
model.joint_q_start,
model.joint_qd_start,
model.joint_dof_dim,
model.joint_type,
model.joint_limit_lower,
model.joint_limit_upper,
state.joint_q,
],
)
# Recompute transforms after editing generalized coordinates
newton.eval_fk(model, state.joint_q, state.joint_qd, state)
ArticulationView: selection interface for RL and batched control
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
:class:newton.selection.ArticulationView is the high-level interface for selecting a subset
of articulations and accessing their joints/links/DOFs with stable tensor shapes. This is
especially useful in RL pipelines where the same observation/action logic is applied to many
parallel environments.
Construct a view by matching articulation keys with a pattern and optional filters:
.. testsetup:: articulation-view
builder = newton.ModelBuilder()
for i in range(2):
root = builder.add_link(
xform=wp.transform(wp.vec3(float(i) * 2.0, 0.0, 0.0), wp.quat_identity())
)
tip = builder.add_link(
xform=wp.transform(wp.vec3(float(i) * 2.0 + 1.0, 0.0, 0.0), wp.quat_identity())
)
builder.add_shape_box(root, hx=0.1, hy=0.1, hz=0.1)
builder.add_shape_box(tip, hx=0.1, hy=0.1, hz=0.1)
j_root = builder.add_joint_free(parent=-1, child=root)
j_tip = builder.add_joint_revolute(
parent=root,
child=tip,
axis=wp.vec3(0.0, 0.0, 1.0),
parent_xform=wp.transform(wp.vec3(1.0, 0.0, 0.0), wp.quat_identity()),
child_xform=wp.transform_identity(),
)
builder.add_articulation([j_root, j_tip], label=f"robot_{i}")
model = builder.finalize()
state = model.state()
.. testcode:: articulation-view
# select all articulations whose key starts with "robot"
view = newton.selection.ArticulationView(model, pattern="robot*")
assert view.count == 2
# select only scalar-joint articulations (exclude quaternion-root joint types)
scalar_view = newton.selection.ArticulationView(
model,
pattern="robot*",
include_joint_types=[newton.JointType.PRISMATIC, newton.JointType.REVOLUTE],
exclude_joint_types=[newton.JointType.FREE, newton.JointType.BALL],
)
assert scalar_view.get_dof_positions(state).shape == (1, 2, 1)
Use views to read/write batched state slices (joint positions/velocities, root transforms,
link transforms) without manual index bookkeeping.
Move articulations in world space
"""""""""""""""""""""""""""""""""
Use :meth:newton.selection.ArticulationView.set_root_transforms to move selected articulations:
.. testcode:: articulation-view
view = newton.selection.ArticulationView(model, pattern="robot*")
root_tf = view.get_root_transforms(state).numpy()
# shift +0.2 m along world x for all selected articulations
root_tf[..., 0] += 0.2
view.set_root_transforms(state, root_tf)
# recompute link transforms from generalized coordinates
newton.eval_fk(model, state.joint_q, state.joint_qd, state)
assert np.allclose(view.get_root_transforms(state).numpy()[0, :, 0], [0.2, 2.2])
For floating-base articulations (root joint type FREE or DISTANCE), this updates
the root coordinates in joint_q.
For non-floating-base articulations (for example FIXED or a world-attachedREVOLUTE root), set_root_transforms() moves the articulation by writingModel.joint_X_p because there is no root pose stored in state coordinates.
Use ArticulationView to inspect and modify selected articulations
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
ArticulationView provides stable, per-articulation access to links, joints, DOFs, and attributes:
.. testcode:: articulation-view
view = newton.selection.ArticulationView(model, pattern="robot*")
scalar_view = newton.selection.ArticulationView(
model,
pattern="robot*",
include_joint_types=[newton.JointType.PRISMATIC, newton.JointType.REVOLUTE],
exclude_joint_types=[newton.JointType.FREE, newton.JointType.BALL],
)
# inspect
q = scalar_view.get_dof_positions(state) # shape [world_count, articulation_count, dof_count]
qd = scalar_view.get_dof_velocities(state) # shape [world_count, articulation_count, dof_count]
link_q = view.get_link_transforms(state) # shape [world_count, articulation_count, link_count]
assert q.shape == (1, 2, 1)
assert qd.shape == (1, 2, 1)
assert link_q.shape == (1, 2, 2)
# edit selected articulation values in-place
q_np = q.numpy()
q_np[..., 0] = 0.0
scalar_view.set_dof_positions(state, q_np)
assert np.allclose(scalar_view.get_dof_positions(state).numpy()[0, :, 0], 0.0)
# if model attributes are edited through the view, notify the solver afterwards
# solver.notify_model_changed()
.. _FK-IK:
Forward / Inverse Kinematics
----------------------------
Articulated rigid-body mechanisms are kinematically described by the joints that connect the bodies as well as the
relative transform from the parent and child body to the respective anchor frames of the joint in the parent and child body:
.. image:: /_static/joint_transforms.png
:width: 400
:align: center
.. list-table:: Variable names in the articulation kernels
:widths: 10 90
:header-rows: 1
* - Symbol
- Description
* - x_wp
- World transform of the parent body (stored at :attr:State.body_q)State.body_q
* - x_wc
- World transform of the child body (stored at :attr:)Model.joint_X_p
* - x_pj
- Transform from the parent body to the joint parent anchor frame (defined by :attr:)Model.joint_X_c
* - x_cj
- Transform from the child body to the joint child anchor frame (defined by :attr:)
* - x_j
- Joint transform from the joint parent anchor frame to the joint child anchor frame
In the forward kinematics, the joint transform is determined by the joint coordinates (generalized joint positions :attr:State.joint_q and velocities :attr:State.joint_qd).x_{wp}
Given the parent body's world transform :math: and the joint transform :math:x_{j}, the child body's world transform :math:x_{wc} is computed as:
.. math::
x_{wc} = x_{wp} \cdot x_{pj} \cdot x_{j} \cdot x_{cj}^{-1}.
Newton's public :func:newton.eval_fk writes :attr:State.body_qd using thatnewton.eval_ik
COM/world convention, and :func: expects the same convention
when recovering generalized state from maximal body state. For FREE andDISTANCE joints, the
recovered generalized velocities are rotated back into the joint parent frame.
.. autofunction:: newton.eval_fk
:noindex:
.. autofunction:: newton.eval_ik
:noindex:
.. _Inverse Dynamics:
Inverse Dynamics
----------------
.. experimental::
Newton can evaluate the manipulator equation for an articulated rigid-body system:
.. math::
\tau = M(q)\, \ddot{q} + C(q, \dot{q})\, \dot{q} + g(q)
.. list-table:: Manipulator-equation terms
:widths: 25 75
:header-rows: 1
* - Symbol
- Description
* - :math:qState.joint_q
- Generalized joint coordinates (:attr:).\dot{q}
* - :math:State.joint_qd
- Generalized joint velocities (:attr:).\ddot{q}
* - :math:
- Generalized joint accelerations (user-supplied joint_qdd).\tau
* - :math:Control.joint_f
- Generalized joint forces / torques, same layout as :attr:.M(q)
* - :math:
- Joint-space mass matrix, shape (articulation_count, max_dofs_per_articulation, max_dofs_per_articulation).g(q) = \partial U / \partial q
* - :math:U(q) = \sum_i -m_i\, \mathbf{g} \cdot \mathbf{x}_{\text{com},i}
- Gravity force, where :math: is the system's gravitational potential energy (sum over bodies of mass ร gravity-vector ยท CoM position). Equivalently, the feed-forward joint-space force a controller must apply to hold the articulation static under gravity.C(q, \dot{q})\, \dot{q}
* - :math:
- Coriolis + centrifugal force.
:func:newton.eval_inverse_dynamics_passive populates any requestedM(q)
combination of :math:, :math:g(q), andC(q, \dot{q})\, \dot{q}
:math: into caller-allocated arrays. An output set toNone is not computed.newton.eval_inverse_dynamics_force
:func: then combines them with a\ddot{q}
user-supplied :math: to produce :math:\tau.
Both functions require state.body_q to be consistent withstate.joint_q: callers must invoke :func:newton.eval_fk (or
otherwise update state.body_q) first.
.. testcode:: articulation-view
# bring state.body_q in sync with state.joint_q (precondition of
# eval_inverse_dynamics_passive)
newton.eval_fk(model, state.joint_q, state.joint_qd, state)
# allocate the requested outputs
mass_matrix = wp.empty(
(
model.articulation_count,
model.max_dofs_per_articulation,
model.max_dofs_per_articulation,
),
dtype=wp.float32,
device=model.device,
)
gravity_force = wp.empty_like(state.joint_qd)
coriolis_force = wp.empty_like(state.joint_qd)
joint_f = wp.empty_like(state.joint_qd)
# populate M(q), g(q), and C(q, q_dot)*q_dot in one call
newton.eval_inverse_dynamics_passive(
model,
state,
mass_matrix=mass_matrix,
gravity_force=gravity_force,
coriolis_force=coriolis_force,
)
# combine into the generalized joint force tau = Mjoint_qdd + Cqdot + g
joint_qdd = wp.zeros_like(state.joint_qd)
newton.eval_inverse_dynamics_force(
model,
state,
mass_matrix=mass_matrix,
joint_qdd=joint_qdd,
coriolis_force=coriolis_force,
gravity_force=gravity_force,
joint_f=joint_f,
)
Pass only the output arrays you need. For example, supplyinggravity_force= and coriolis_force= while leaving mass_matrix=None
skips the mass-matrix Jacobian pass.
Restricting evaluation with the selection API
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:class:newton.selection.ArticulationView exposes~newton.selection.ArticulationView.eval_inverse_dynamics_passive
:meth:, whichnewton.eval_mass_matrix
masks the computation to a label-matched (and optionally per-world)
subset of articulations. Output buffers stay sized for the whole model;
slots belonging to unselected articulations and DOFs come back as zero,
mirroring the convention :func: uses for its
own mask= argument.
.. testcode:: articulation-view
# only compute M(q), g(q), and C*q_dot for selected articulations
view = newton.selection.ArticulationView(model, pattern="robot*")
view.eval_inverse_dynamics_passive(
state,
mass_matrix=mass_matrix,
gravity_force=gravity_force,
coriolis_force=coriolis_force,
)
# optionally narrow further with a per-world submask (shape [world_count])
per_world_mask = wp.array([True], dtype=bool, device=model.device)
view.eval_inverse_dynamics_passive(
state,
mass_matrix=mass_matrix,
gravity_force=gravity_force,
coriolis_force=coriolis_force,
mask=per_world_mask,
)
The view also applies the same selection when combining the populated arrays
with a desired acceleration:
.. testcode:: articulation-view
view.eval_inverse_dynamics_force(
state,
mass_matrix=mass_matrix,
joint_qdd=joint_qdd,
coriolis_force=coriolis_force,
gravity_force=gravity_force,
joint_f=joint_f,
mask=per_world_mask,
)
.. autofunction:: newton.eval_inverse_dynamics_passive
:noindex:
.. autofunction:: newton.eval_inverse_dynamics_force
:noindex:
.. _Orphan joints:
Orphan joints
-------------
An orphan joint is a joint that is not part of any articulation and whose child body is not reachable through any articulated joint (i.e. the child has no articulated path back to the rest of the model). This situation can arise when:
* The USD asset does not define a PhysicsArticulationRootAPI on any prim, so no articulations are discovered during parsing.
* A joint connects two bodies that are not under any PhysicsArticulationRootAPI prim, even though other articulations exist in the scene.
A joint that is excluded from every :meth:~newton.ModelBuilder.add_articulation call but whose two bodies are already reachable through the articulation tree is not an orphan joint; it is a loop-closing joint (see :ref:Loop closure) and is handled separately. A joint from world to a body is also allowed to remain outside articulation metadata as a standalone world-root joint.
USD import preserves joints outside authored articulations without emitting an articulation warning. The model's validation and the selected solver determine whether the resulting topology is supported.
Validation and finalization
By default, :meth:~newton.ModelBuilder.finalize raises a :class:ValueError for non-root orphan joints. Loop-closing joints and standalone world-root joints pass this check. To proceed with another orphan topology, skip this validation explicitly:
.. testsetup:: articulation-orphan-joints
builder = newton.ModelBuilder()
parent = builder.add_link()
child = builder.add_link()
builder.add_shape_box(parent, hx=0.1, hy=0.1, hz=0.1)
builder.add_shape_box(child, hx=0.1, hy=0.1, hz=0.1)
builder.add_joint_revolute(parent=parent, child=child, axis=newton.Axis.Z)
.. testcode:: articulation-orphan-joints
model = builder.finalize(skip_validation_joints=True)
Solver compatibility
Maximal-coordinate solvers (:class:~newton.solvers.SolverXPBD, :class:~newton.solvers.SolverSemiImplicit) consume joints independently of articulation membership. Semi-implicit joint constraints are penalty forces, so their accuracy and stability depend on the configured stiffness, damping, and time step.
:class:~newton.solvers.SolverMuJoCo converts standalone world-root joints through a solver-specific fallback and emits a warning. It rejects general rootless mechanisms whose remaining bodies cannot be instantiated from articulations or standalone world roots. :class:~newton.solvers.SolverFeatherstone requires reduced-coordinate articulation metadata.
Loop-closing joints are handled separately; see :ref:Loop closure.
.. _Loop closure:
Loop closure
------------
Newton's :meth:~newton.ModelBuilder.add_joint_ methods author *kinematic
trees: each body has at most one parent joint, so the joints alone cannot
form a closed kinematic loop (for example a four-bar linkage or a parallel
mechanism). Closed loops must instead be expressed by declaring the topology
as a tree and adding a separate joint that re-couples the open end.
To close a loop, create the loop-closing joint with
:meth:~newton.ModelBuilder.add_joint_ but *omit it from thejoint_list passed to :meth:~newton.ModelBuilder.add_articulation,orphan joint <Orphan joints>
so the articulation graph remains a tree. The omitted joint is a
loop-closing joint: its two bodies are both already reachable through
the tree, which distinguishes it from an
:ref: (whose child has no articulated path~newton.ModelBuilder.finalize
and which :meth: rejects unlessskip_validation_joints=True).
.. testcode::
builder = newton.ModelBuilder()
# Fixed root attached to the world.
root = builder.add_link()
builder.add_shape_box(root, hx=0.1, hy=0.1, hz=0.1)
j_root = builder.add_joint_fixed(parent=-1, child=root)
# Child A: revolute about Z, hinged on the root at +X.
child_a = builder.add_link()
builder.add_shape_box(child_a, hx=0.5, hy=0.05, hz=0.05)
j_a = builder.add_joint_revolute(
parent=root,
child=child_a,
axis=newton.Axis.Z,
parent_xform=wp.transform(wp.vec3(1.0, 0.0, 0.0), wp.quat_identity()),
)
# Child B: revolute about Z, hinged on the root at -X.
child_b = builder.add_link()
builder.add_shape_box(child_b, hx=0.5, hy=0.05, hz=0.05)
j_b = builder.add_joint_revolute(
parent=root,
child=child_b,
axis=newton.Axis.Z,
parent_xform=wp.transform(wp.vec3(-1.0, 0.0, 0.0), wp.quat_identity()),
)
# Loop-closing joint: a fixed joint between the two children. Authored with
# add_joint_* exactly like a tree joint, but deliberately left out of the
# articulation below.
j_loop = builder.add_joint_fixed(parent=child_a, child=child_b)
# Only the tree joints (j_root, j_a, j_b) go into the articulation;
# j_loop is excluded so the articulation graph remains a tree.
builder.add_articulation([j_root, j_a, j_b])
model = builder.finalize()
Importing from USD. The same omit-from-articulation pattern is the
standard way UsdPhysics expresses loop closures, and Newton's USD importer
honors it. Set the physics:excludeFromArticulation attribute to true
on a PhysicsJoint prim, and :meth:~newton.ModelBuilder.add_usd will
register the joint with the builder via the normal add_joint_* path but~newton.ModelBuilder.add_articulation
leave it out of the surrounding :meth:
call โ producing exactly the topology shown above. This is how
a USD asset can author a four-bar linkage or other parallel mechanism.
.. note::
A loop-closing joint passes :meth:~newton.ModelBuilder.finalize
validation by default โ because its two bodies are already reachable
through the tree, the orphan-joint check does not fire and
skip_validation_joints=True is not required. Each solver then
handles the loop-closing joint differently:
- Maximal-coordinate solvers track state as per-body transforms
(:attr:~newton.State.body_q / :attr:~newton.State.body_qd) and~newton.solvers.SolverXPBD
enforce joints as pairwise body constraints, so the loop-closure joint is
solved alongside the tree joints with no special-casing. Under
:class: and~newton.solvers.SolverSemiImplicit
:class:, j_loop keeps its full
joint behavior โ drive (joint_target_ke/joint_target_kd,
control.joint_f) and joint limits are applied alongside theJoint feature support
loop-closure constraint, subject to each solver's general joint-feature
support (see :ref:).~newton.solvers.SolverVBD
:class: and~newton.solvers.SolverKamino
:class: use the same flat per-joint
iteration but support a narrower set of joint types and features, so
the same loop-closure pattern works only within their respective
supported subsets.
- Generalized-coordinate solvers carry only tree-joint coordinates in
their state vector and must handle the loop closure separately.
:class:~newton.solvers.SolverMuJoCo enforces each loop-closure joint as a~newton.solvers.SolverFeatherstone
bilateral coupling at compile time, which restricts the supported
joint types and drops joint-level features (see the note below).
:class: has no such synthesis
path: the loop-closure joint contributes no DOFs and the loop closure is
silently not enforced.
In all cases the loop-closing joint is invisible to :func:newton.eval_fk,newton.eval_ik
:func:, and :class:~newton.selection.ArticulationView โ
those walk the articulation tree only.
.. note::
:class:~newton.solvers.SolverMuJoCo supports only a subset of jointmujoco-loop-closures
types as loop closures, and the loop-closing joint loses its joint-level
features (drive, limits, armature, friction). See
:ref: for the supported types and MuJoCo-specific
behavior.
---
Concepts/Collisions
.. SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
.. SPDX-License-Identifier: CC-BY-4.0
.. currentmodule:: newton
.. _Collisions:
Collisions
==========
Newton provides a GPU-accelerated collision detection system with:
- Full shape-pair coverage โ every shape type collides with every other shape type
(see :ref:Shape Compatibility).~solvers.SolverMuJoCo
- Mesh-mesh contacts via precomputed SDFs for O(1) distance queries on complex
geometry.
- Hydroelastic contacts that sample contacts across the contact surface for improved
fidelity in torsional friction and force distribution, especially in non-convex and
manipulation scenarios.
- Drop-in replacement for MuJoCo's contacts โ use Newton's pipeline with
:class: for advanced contact models (seeMuJoCo Warp Integration
:ref:).
This page starts with a :ref:conceptual overview <Collision Overview> of how geometry
representations and narrow phase algorithms combine, then covers each stage in detail.
.. _Collision Overview:
Conceptual Overview
-------------------
Newton's collision pipeline runs in two stages: a broad phase that quickly
eliminates shape pairs whose bounding boxes do not overlap, followed by a narrow
phase that computes the actual contact geometry for surviving pairs.
The narrow phase algorithm used for a given pair depends on how the shapes are
represented:
.. mermaid::
:config: {"theme": "forest", "themeVariables": {"lineColor": "#76b900"}}
flowchart LR
BP["Broad Phase<br/>(AABB culling)"] --> Triage
subgraph Triage ["Pair Triage"]
G1["Convex / primitive<br/>pairs"]
G2["Mesh pairs<br/>(BVH or SDF)"]
end
subgraph NP ["Narrow Phase"]
A["MPR / GJK"]
B["Distance queries<br/>+ contact reduction"]
C["Hydroelastic<br/>+ contact reduction"]
end
G1 --> A --> Contacts
G2 --> B --> Contacts
G2 -.->|"both shapes<br/>hydroelastic"| C --> Contacts
Geometry representations
1. Convex hulls and primitives โ sphere, box, capsule, cylinder, cone, ellipsoid,
and convex mesh shapes expose canonical support functions. These feed directly into
the MPR/GJK narrow phase which produces contact points without further reduction.
See :ref:Narrow Phase.
2. Live BVH queries โ triangle meshes that do not have a precomputed SDF are
queried through Warp's BVH (Bounding Volume Hierarchy). This path computes on-the-fly
distance queries and generates contacts with optional contact reduction. It works out
of the box but can be slow for high-triangle-count meshes. Hydroelastic contacts are
not available on this path. See :ref:Mesh Collisions.
3. Precomputed SDFs โ calling mesh.build_sdf(...) on a mesh precomputes a
signed distance field that provides O(1) distance lookups. Primitive shapes can also
generate SDF grids via ShapeConfig SDF parameters (see :ref:Shape Configuration).Mesh Collisions
This path supports both distance-query and hydroelastic contact generation
(with contact reduction). Hydroelastic contacts require SDF on both shapes in a
pair. See :ref: and :ref:Hydroelastic Contacts.
.. note::
Contact reduction applies to the SDF-based and hydroelastic paths where many raw
contacts are generated from distance field queries. The direct MPR/GJK path for
convex pairs produces a small number of contacts and does not require reduction. See
:ref:Contact Reduction.
.. tip::
For scenes with expensive collision (SDF or hydroelastic), running collide onceCommon Patterns
per frame instead of every substep can significantly improve performance. See
:ref: for the different collision-frequency patterns.
.. _Contact Model:
Contact Geometry
^^^^^^^^^^^^^^^^
The output of the narrow phase is a set of contacts: lightweight geometric
descriptors that decouple the solver from the underlying shape complexity. A mesh may
contain hundreds of thousands of triangles, but the collision pipeline distills the
interaction into a manageable number of contacts that the solver can process efficiently.
Each contact carries the following geometric data:
.. figure:: ../images/contact_model.svg
:alt: Contact geometry: normal, contact points, contact distance
:width: 70%
:align: center
A contact between two shapes (A and B). The contact normal (blue, unit length)
points from shape A to shape B. Body-frame contact points (yellow) are stored in
each body's local frame. The contact midpoint (red) โ the average of the two
world-space contact points โ is not stored but is useful for visualization and
debugging. The contact distance encodes the signed separation or penetration depth.
- Contact normal (world frame) โ a unit vector pointing from shape A toward shape B.
- Contact points (body frame) โ the contact location on each shape
(rigid_contact_point0/1), stored in the parent body's local coordinate frame.
- Contact distance โ the signed separation between the two contact points along the
normal. Negative values indicate penetration.
Because contacts are self-contained geometric objects, the solver never needs to query
mesh triangles or SDF grids โ it only works with the contact arrays stored in
:class:~Contacts. See :ref:Contact Generation for the full data layout.
.. _MuJoCo Warp Integration:
MuJoCo Warp Integration
^^^^^^^^^^^^^^^^^^^^^^^^
:class:~solvers.SolverMuJoCo (the MuJoCo Warp backend) ships with its own
built-in collision pipeline that handles convex primitive contacts. For many use cases
this is sufficient and requires no extra setup.
Newton's collision pipeline can also replace MuJoCo's contact generation, enabling
SDF-based mesh-mesh contacts and hydroelastic contacts that MuJoCo's built-in pipeline
does not support.
Examples:
- Hydroelastic mesh contacts โ
:github:newton/examples/contacts/example_nut_bolt_hydro.pynewton/examples/contacts/example_nut_bolt_sdf.py
- SDF mesh contacts โ
:github:newton/examples/contacts/example_brick_stacking.py
- Robot manipulation with SDF โ
:github:
See :ref:Solver Integration for the full code pattern showing how to configure
this.
.. _Geometry Pair Contact Behavior:
Geometry-Pair Contact Behavior
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The two tables below describe contacts generated by Newton's
:class:~CollisionPipeline only; they do not apply to contacts generated by the
native MuJoCo CPU or MuJoCo Warp collision pipelines. The values are theoretical
upper bounds per shape pair and collision pass before the sharedrigid_contact_max capacity is applied. Actual counts vary with pose, margin,
and collision settings.
CollisionPipeline primitive and convex-hull pairs
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 18 9 9 10 11 11 8 9 13
* - Shape A / Shape B
- Plane
- Sphere
- Capsule
- Ellipsoid
- Cylinder
- Box
- Cone
- Convex hull
* - Plane
- 5
- 1
- 2
- 1
- 4
- 4
- 5
- 5
* - Sphere
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
* - Capsule
- 2
- 1
- 2
- 1
- 5
- 5
- 5
- 5
* - Ellipsoid
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
* - Cylinder
- 4
- 1
- 5
- 1
- 5
- 5
- 5
- 5
* - Box
- 4
- 1
- 5
- 1
- 5
- 5
- 5
- 5
* - Cone
- 5
- 1
- 5
- 1
- 5
- 5
- 5
- 5
* - Convex hull
- 5
- 1
- 5
- 1
- 5
- 5
- 5
- 5
The plane--plane upper bound is five when at least one plane is finite. Two
infinite planes produce no contacts.
CollisionPipeline mesh, heightfield, and SDF routes
V is a mesh vertex count, T_overlap is the number of overlapping
triangles, and E is the number of collision edges. The estimated typical
count is the sizing heuristic used for allocation, not a measured statistical
average or a per-pair limit.
.. list-table::
:header-rows: 1
:widths: 27 18 23 12 20
* - Pair or route
- Reduced maximum (default)
- Unreduced maximum
- Estimated typical count
- Notes
* - Triangle mesh--infinite plane
- 240
- V_mesh
- About 40
- At most one candidate per mesh vertex.
* - Mesh/heightfield--sphere or ellipsoid
- 240
- T_overlap
- About 40
- At most one contact per overlapping triangle.
* - Mesh/heightfield--other primitive or convex hull
- 240
- 5 * T_overlap
- About 40
- Each overlapping triangle uses the convex manifold path.
* - Mesh--mesh
- 240
- E_A + E_B
- About 40
- Uses edge-vs-SDF queries, with BVH distance fallback when needed.
* - Heightfield--mesh
- 240
- E_heightfield + E_mesh
- About 40
- Uses the mesh/SDF route with on-the-fly heightfield evaluation.
* - Hydroelastic SDF--SDF
- 240 by default
- Geometry and buffer dependent
- No fixed estimate
- anchor_contact=True can add contacts beyond the reduced set.
The reduced maximum follows the current 240-slot contact-reduction layout.
Disabling reduction exposes the geometry-dependent candidate bounds shown above.
Common pair guidance
.. list-table::
:header-rows: 1
:widths: 16 23 31 30
* - Pair
- Expected behavior
- Backend notes
- Asset guidance
* - Sphere--plane or sphere--box
- A point contact.
- Newton, MuJoCo Warp, and MuJoCo CPU use single-contact primitive paths.
- Use one sphere unless the asset needs a finite support patch; then use
multiple collision shapes or a surface-contact representation.
* - Capsule--plane or capsule--box
- End-on contact is point-like; side-on contact can span the capsule axis.
- Newton and both MuJoCo backends have multi-contact paths for the
line-like side contact.
- A single capsule is normally sufficient. Use a compound only when the
physical profile is not capsule-shaped.
* - Box--plane or box--box
- Face contact forms an area-supporting manifold; edge and corner contacts
use fewer points.
- Newton and both MuJoCo backends generate multi-point face contacts.
- Prefer a single box for box-like parts; it is cheaper and usually more
stable than a tessellated mesh.
* - Cylinder--box
- A cylinder lying across a broad box face should have a manifold spanning
its support region.
- Newton generates a convex manifold. MuJoCo CPU can generate a multi-point
manifold with multi-CCD enabled. MuJoCo Warp currently emits one contact
for this pair even with enable_multiccd=True; contact location canmujoco_warp#1555
alternate between the cylinder ends. This is a known discrepancy tracked
in
<https://github.com/google-deepmind/mujoco_warp/issues/1555>__.
- Keep a single cylinder with Newton contacts. While the MuJoCo Warp issue
is open, use use_mujoco_contacts=False or, if that is not possible,
approximate load-bearing regions with multiple collision shapes.
The MuJoCo Warp cylinder--box behavior above is a known discrepancy, not the intended
single-contact behavior of the geometry pair. For non-convex assets, use a
convex compound or Newton's mesh/SDF paths rather than expecting one primitive
to reproduce the surface. See :ref:Mesh Collisions and :ref:Simulation Tuning.
.. _Collision Pipeline:
Collision Pipeline
------------------
Newton's collision pipeline implementation supports multiple broad phase algorithms and advanced contact models (SDF-based, hydroelastic, cylinder/cone primitives). See :ref:Collision Pipeline Details for details.
Basic usage:
.. testsetup:: pipeline-basics
import warp as wp
import newton
builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
state = model.state()
.. testcode:: pipeline-basics
# Create a pipeline with the desired broad phase mode
from newton import CollisionPipeline
pipeline = CollisionPipeline(
model,
broad_phase="sap",
)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)
.. _Quick Start:
Quick Start
-----------
A minimal end-to-end example that creates shapes, runs collision detection, and steps the
solver (see also the :doc:Introduction tutorial </tutorials/00_introduction> andexample_basic_shapes.py โ :github:newton/examples/basic/example_basic_shapes.py):
.. testcode:: quickstart
import warp as wp
import newton
builder = newton.ModelBuilder()
builder.add_ground_plane()
# Dynamic sphere
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
solver = newton.solvers.SolverXPBD(model, iterations=5)
state_0 = model.state()
state_1 = model.state()
control = model.control()
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
dt = 1.0 / 60.0 / 10.0
for frame in range(120):
for substep in range(10):
state_0.clear_forces()
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, dt)
state_0, state_1 = state_1, state_0
.. _Supported Shape Types:
Supported Shape Types
---------------------
Newton supports the following geometry types via :class:~GeoType:
.. list-table::
:header-rows: 1
:widths: 20 80
* - Type
- Description
* - PLANE
- Infinite plane (ground)
* - HFIELD
- Heightfield terrain (2D elevation grid)
* - SPHERE
- Sphere primitive
* - CAPSULE
- Cylinder with hemispherical ends
* - BOX
- Axis-aligned box
* - CYLINDER
- Cylinder
* - CONE
- Cone
* - ELLIPSOID
- Ellipsoid
* - MESH
- Triangle mesh (arbitrary, including non-convex)
* - CONVEX_MESH
- Convex hull mesh
.. note::
SDF is collision data, not a standalone shape type. For mesh shapes, build and attach
an SDF explicitly with mesh.build_sdf(...) and then pass that mesh to
builder.add_shape_mesh(...). For primitive hydroelastic workflows, SDF generation uses
ShapeConfig SDF parameters.
.. _Shapes and Bodies:
Shapes and Rigid Bodies
-----------------------
Collision shapes are attached to rigid bodies. Each shape has:
- Body index (shape_body): The rigid body this shape is attached to. Use body=-1 for static/world-fixed shapes.
- Local transform (shape_transform): Position and orientation relative to the body frame.
- Scale (shape_scale): 3D scale factors applied to the shape geometry.
- Margin (shape_margin): Surface offset that shifts where contact points are placed. See :ref:Margin and gap semantics <margin-gap-semantics>.
- Gap (shape_gap): Extra detection distance that shifts when contacts are generated. See :ref:Margin and gap semantics <margin-gap-semantics>.
- Source geometry (shape_source): Reference to the underlying geometry object (e.g., :class:~Mesh).
During collision detection, shapes are transformed to world space using their parent body's pose:
.. code-block:: python
# Shape world transform = body_pose * shape_local_transform
X_world_shape = body_q[shape_body] * shape_transform[shape_id]
Contacts are generated between shapes, not bodies. Depending on the type of solver, the motion of the bodies is affected by forces or constraints that resolve the penetrations between their attached shapes.
.. _Collision Filtering:
Collision Filtering
-------------------
The collision pipeline uses filtering rules based on world indices and collision groups.
.. _World IDs:
World Indices
^^^^^^^^^^^^^
World indices enable multi-world simulations, primarily for reinforcement learning, where objects belonging to different worlds coexist but do not interact through contacts:
- Index -1: Global entities that collide with all worlds (e.g., ground plane)
- Index 0, 1, 2, ...: World-specific entities that only interact within their world
.. testcode:: world-indices
builder = newton.ModelBuilder()
# Global ground (default world -1, collides with all worlds)
builder.add_ground_plane()
# Robot template
robot_builder = newton.ModelBuilder()
body = robot_builder.add_link()
robot_builder.add_shape_sphere(body, radius=0.5)
joint = robot_builder.add_joint_free(body)
robot_builder.add_articulation([joint])
# Instantiate in separate worlds - robots won't collide with each other
builder.add_world(robot_builder) # World 0
builder.add_world(robot_builder) # World 1
model = builder.finalize()
For heterogeneous worlds, use :meth:~ModelBuilder.begin_world and :meth:~ModelBuilder.end_world.
For large-scale parallel simulations (e.g., RL), :meth:~ModelBuilder.replicate stamps
out many copies of a template environment builder into separate worlds in one call:
.. testcode:: replicate
# Template environment: one sphere per world
env_builder = newton.ModelBuilder()
body = env_builder.add_body()
env_builder.add_shape_sphere(body, radius=0.5)
# Combined builder: global geometry + 1024 replicated worlds
main = newton.ModelBuilder()
main.add_ground_plane() # global (world -1), shared across all worlds
main.replicate(env_builder, world_count=1024)
model = main.finalize()
.. note::
MJWarp does not currently support heterogeneous environments (different models per world).
World indices are stored in :attr:~Model.shape_world, :attr:~Model.body_world, etc.
.. _Collision Groups:
Collision Groups
^^^^^^^^^^^^^^^^
Collision groups control which shapes collide within the same world:
- Group 0: Collisions disabled
- Positive groups (1, 2, ...): Collide with same group or any negative group
- Negative groups (-1, -2, ...): Collide with shapes in any positive or negative group, except shapes in the same negative group
.. list-table::
:header-rows: 1
:widths: 15 15 15 55
* - Group A
- Group B
- Collide?
- Reason
* - 0
- Any
- โ
- Group 0 disables collision
* - 1
- 1
- โ
- Same positive group
* - 1
- 2
- โ
- Different positive groups
* - 1
- -2
- โ
- Positive with any negative
* - -1
- -1
- โ
- Same negative group
* - -1
- -2
- โ
- Different negative groups
.. testcode:: collision-groups
builder = newton.ModelBuilder()
# Group 1: only collides with group 1 and negative groups
body1 = builder.add_body()
builder.add_shape_sphere(body1, radius=0.5, cfg=builder.ShapeConfig(collision_group=1))
# Group -1: collides with everything (except other -1)
body2 = builder.add_body()
builder.add_shape_sphere(body2, radius=0.5, cfg=builder.ShapeConfig(collision_group=-1))
model = builder.finalize()
Self-collision within articulations
Self-collisions within an articulation can be enabled or disabled with enable_self_collisions when loading models. By default, adjacent body collisions (parent-child pairs connected by joints) are disabled via collision_filter_parent=True.
.. code-block:: python
# Enable self-collisions when loading models
builder.add_usd("robot.usda", enable_self_collisions=True)
builder.add_mjcf("robot.xml", enable_self_collisions=True)
# Or control per-shape (also applies to max-coordinate jointed bodies)
cfg = builder.ShapeConfig(collision_group=-1, collision_filter_parent=False)
Controlling particle collisions
Use has_shape_collision and has_particle_collision for fine-grained control over what a shape collides with. Setting both to False is equivalent to collision_group=0.
.. testcode:: particle-collision
builder = newton.ModelBuilder()
# Shape that only collides with particles (not other shapes)
cfg = builder.ShapeConfig(has_shape_collision=False, has_particle_collision=True)
# Shape that only collides with other shapes (not particles)
cfg = builder.ShapeConfig(has_shape_collision=True, has_particle_collision=False)
UsdPhysics Collision Filtering
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Newton follows the UsdPhysics collision filtering specification <https://openusd.org/dev/api/usd_physics_page_front.html#usdPhysics_collision_filtering>_,
which provides two complementary mechanisms for controlling which shapes collide:
1. Collision Groups - Group-based filtering using UsdPhysicsCollisionGroup
2. Pairwise Filtering - Explicit shape pair exclusions using physics:filteredPairs
Collision Groups
In UsdPhysics, shapes can be assigned to collision groups defined by UsdPhysicsCollisionGroup prims.
When importing USD files, Newton reads the collisionGroups attribute from each shape and maps
each unique collision group name to a positive integer ID (starting from 1). Shapes in different
collision groups will not collide with each other unless their groups are configured to interact.
.. code-block:: usda
# Define a collision group in USD
def "CollisionGroup_Robot" (
prepend apiSchemas = ["PhysicsCollisionGroup"]
) {
}
# Assign shape to a collision group
def Sphere "RobotPart" (
prepend apiSchemas = ["PhysicsCollisionAPI"]
) {
rel physics:collisionGroup = </CollisionGroup_Robot>
}
When loading this USD, Newton automatically assigns each collision group a unique integer ID
and sets the shape's collision_group accordingly.
Pairwise Filtering
For fine-grained control, UsdPhysics supports explicit pair filtering via the physics:filteredPairs
relationship. This allows excluding specific shape pairs from collision detection regardless of their
collision groups.
.. code-block:: usda
# Exclude specific shape pairs in USD
def Sphere "ShapeA" (
prepend apiSchemas = ["PhysicsCollisionAPI"]
) {
rel physics:filteredPairs = [</ShapeB>]
}
Newton reads these relationships during USD import and converts them to
:attr:~ModelBuilder.shape_collision_filter_pairs.
Collision Enabled Flag
Shapes with physics:collisionEnabled=false are excluded from all collisions by adding filter
pairs against all other shapes in the scene.
Shape Collision Filter Pairs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The :attr:~ModelBuilder.shape_collision_filter_pairs list stores explicit shape pair exclusions.
This is Newton's internal representation for pairwise filtering (including pairs imported from
UsdPhysics physics:filteredPairs relationships).
.. testcode:: filter-pairs
builder = newton.ModelBuilder()
# Add shapes
body = builder.add_body()
shape_a = builder.add_shape_sphere(body, radius=0.5)
shape_b = builder.add_shape_box(body, hx=0.5, hy=0.5, hz=0.5)
# Exclude this specific pair from collision detection
builder.add_shape_collision_filter_pair(shape_a, shape_b)
Filter pairs are automatically populated in several cases:
- Adjacent bodies: Parent-child body pairs connected by joints (when
collision_filter_parent=True). For USD joints with two explicit bodies,
physics:collisionEnabled controls this filter with inverse polarity; joints to world do not
create a body-pair filter. Also applies to max-coordinate jointed bodies.
- Same-body shapes: Shapes attached to the same rigid body
- Disabled self-collision: All shape pairs within an articulation when enable_self_collisions=False
- USD filtered pairs: Pairs defined by physics:filteredPairs relationships in USD files
- USD collision disabled: Shapes with physics:collisionEnabled=false (filtered against all other shapes)
The resulting filter pairs are stored in :attr:~Model.shape_collision_filter_pairs as a set of(shape_index_a, shape_index_b) tuples (canonical order: a < b).
.. deprecated:: 1.4
Mutating this finalized-model set is deprecated; update
:attr:~ModelBuilder.shape_collision_filter_pairs before calling finalize() and rebuild the~Model.shape_contact_pairs
model instead, because the precomputed :attr: array is not rebuilt by
post-finalize filter edits.
USD Import Example
.. code-block:: python
# Newton automatically imports UsdPhysics collision filtering
builder = newton.ModelBuilder()
builder.add_usd("scene.usda")
# Collision groups and filter pairs are now populated:
# - shape_collision_group: integer IDs mapped from UsdPhysicsCollisionGroup
# - shape_collision_filter_pairs: pairs from physics:filteredPairs relationships
model = builder.finalize()
.. _Collision Pipeline Details:
Broad Phase and Shape Compatibility
-----------------------------------
:class:~CollisionPipeline provides configurable broad phase algorithms:
.. list-table::
:header-rows: 1
:widths: 15 85
* - Mode
- Description
* - NxN
- All-pairs AABB broad phase. O(Nยฒ), optimal for small scenes (<100 shapes).
* - SAP
- Sweep-and-prune AABB broad phase. O(N log N), better for larger scenes with spatial coherence.
* - EXPLICIT
- Uses precomputed shape pairs (default). Combines static pair efficiency with advanced contact algorithms.
.. testsetup:: broad-phase
import warp as wp
import newton
builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
state = model.state()
.. testcode:: broad-phase
from newton import CollisionPipeline
# Default: EXPLICIT (precomputed pairs)
pipeline = CollisionPipeline(model)
# NxN for small scenes
pipeline = CollisionPipeline(model, broad_phase="nxn")
# SAP for larger scenes
pipeline = CollisionPipeline(model, broad_phase="sap")
contacts = pipeline.contacts()
pipeline.collide(state, contacts)
.. _Shape Compatibility:
Shape Compatibility
^^^^^^^^^^^^^^^^^^^
Shape compatibility summary (rigid + soft particle-shape):
.. list-table::
:header-rows: 1
:widths: 11 7 7 7 7 7 7 7 7 7 7 7 7
* -
- Plane
- HField
- Sphere
- Capsule
- Box
- Cylinder
- Cone
- Ellipsoid
- ConvexHull
- Mesh
- SDF
- Particle
* - Plane
- [1]
- [1]
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
* - HField
- [1]
- [1]
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
โ ๏ธ
- โ
- โ
* - Sphere
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
* - Capsule
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
* - Box
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
* - Cylinder
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
* - Cone
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
* - Ellipsoid
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
* - ConvexHull
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
* - Mesh
- โ
- โ
โ ๏ธ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
โ ๏ธ
- โ
โ ๏ธ
- โ
* - SDF
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
โ ๏ธ
- โ
- โ
* - Particle
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- โ
- [2]
Legend: โ ๏ธ = Can be slow for meshes with high triangle counts; performance can
often be improved by attaching a precomputed SDF to the mesh (mesh.build_sdf(...)).
| [1] Plane and heightfield shapes are static (world-attached) in Newton; static-static pairs are filtered from rigid collision generation.
| [2] Particle-particle interactions are handled by the particle/soft-body solver self-collision path, not by the shape compatibility pipeline in this table.
.. note::
Particle in this table refers to soft particle-shape contacts generated
automatically by the collision pipeline. These contacts additionally require
the shape to have particle collision enabled
(ShapeFlags.COLLIDE_PARTICLES / ShapeConfig.has_particle_collision).
For examples, see cloth and cable scenes that use the collision pipeline for
particle-shape contacts.
.. note::
Heightfield representation: A heightfield (HFIELD) stores a regular 2D grid
of elevation samples. For rigid contacts, Newton uses dedicated heightfield
narrow-phase routes: heightfield-vs-convex uses per-cell triangle GJK/MPR, while
mesh-vs-heightfield routes through the mesh/SDF path with on-the-fly triangle
extraction from the grid. For soft contacts, the collision pipeline automatically
samples the heightfield signed distance and normal.
.. note::
SDF in this table refers to shapes with precomputed SDF data. There is no
GeoType.SDF enum value; this row is a conceptual collision mode for shapes
carrying SDF resources. Mesh SDFs are attached through mesh.build_sdf(...)
and provide O(1) distance queries.
.. _Narrow Phase:
Narrow Phase Algorithms
-----------------------
After broad phase identifies candidate pairs, the narrow phase generates contact points.
The algorithm used depends on the shape types in each pair.
.. _Convex Primitive Contacts:
Convex Primitive Contacts
^^^^^^^^^^^^^^^^^^^^^^^^^
MPR (Minkowski Portal Refinement) and GJK
MPR is the primary algorithm for convex shape pairs. It uses support mapping functions to
find the closest points between shapes via Minkowski difference sampling. Works with all
convex primitives (sphere, box, capsule, cylinder, cone, ellipsoid) and convex meshes.
Newton uses MPR for penetration depth computation (not EPA); GJK handles the
separated-shapes distance query.
Multi-contact generation
For convex primitive pairs, multiple contact points are generated for stable stacking and
resting contacts. The collision pipeline estimates buffer sizes based on the model; you
can override this value with rigid_contact_max when instantiating the pipeline.
.. _Mesh Collisions:
Mesh Collision Handling
^^^^^^^^^^^^^^^^^^^^^^^
Mesh collisions use different strategies depending on the pair type:
Mesh vs Primitive (e.g., Sphere, Box)
Uses BVH (Bounding Volume Hierarchy) queries to find nearby triangles, then generates contacts between primitive vertices and triangle surfaces, plus triangle vertices against the primitive.
.. important::
Triangle winding order matters. Newton uses counter-clockwise (CCW) winding
to determine the outward face normal of each triangle. The collision pipeline
performs back-face culling: when a convex shape is on the back side of a
triangle (behind the face normal), the contact is discarded. This prevents
shapes that tunnel through a mesh surface from being trapped by inverted
contact normals.
Supply mesh indices in CCW order when viewed from the outside of the surface.
If your mesh has inconsistent or clockwise winding, convex shapes may pass
through the surface without generating contacts.
Mesh vs Plane
Projects mesh vertices onto the plane and generates contacts for vertices below the plane surface.
Mesh vs Mesh
Two approaches available:
1. BVH-based (default when no SDF configured): Iterates mesh vertices against the other mesh's BVH.
Performance scales with triangle count - can be very slow for complex meshes.
2. SDF-based (recommended): Uses precomputed signed distance fields for fast queries.
For mesh shapes, call mesh.build_sdf(...) once and reuse the mesh.
.. warning::
If SDF is not precomputed, mesh-mesh contacts fall back to on-the-fly BVH distance queries
which are significantly slower. For production use with complex meshes, precompute and
attach SDF data on meshes:
.. code-block:: python
my_mesh.build_sdf(max_resolution=64)
builder.add_shape_mesh(body, mesh=my_mesh)
.. tip::
Build an SDF on every mesh that can collide, even when high-precision contacts are
not required. A low-resolution SDF (e.g., max_resolution=64) uses very little memory
yet still provides O(1) distance queries that are dramatically faster than the BVH
fallback. Without an SDF, mesh-vs-mesh and mesh-vs-primitive contacts must walk the BVH
for every query point, which dominates collision cost in most scenes. Attaching even a
coarse SDF eliminates this bottleneck.
:meth:~Mesh.build_sdf accepts several optional keyword arguments
(defaults shown in parentheses):
.. code-block:: python
mesh.build_sdf(
max_resolution=256, # Max voxels along longest AABB axis; must be divisible by 8 (None)
narrow_band_range=(-0.005, 0.005), # SDF narrow band [m] ((-0.1, 0.1))
margin=0.005, # Extra AABB padding [m] (0.05)
shape_margin=0.001, # Shrink SDF surface inward [m] (0.0)
scale=(1.0, 1.0, 1.0), # Bake non-unit scale into the SDF (None)
edge_lower_angle_threshold_rad=math.radians(0.1), # Drop near-coplanar edges below this angle (0.1 deg)
edge_box_absorption=False, # Drop edges fully covered by another edge's oriented box
)
max_resolution sets the voxel count along the longest AABB axis (must be divisible by 8);
voxel size is uniform across all axes. Use target_voxel_size instead to specify resolution
in meters directly โ it takes precedence over max_resolution when both are provided. Usenarrow_band_range to limit the SDF computation to a thin shell around the surface (saves
memory and build time). Set the SDF margin to at least the sum of the shape's :ref:margin and gap <margin-gap-semantics> so the SDF covers the
full contact detection range. Pass scale when the shape will be added with non-unit scale
to bake it into the SDF grid. shape_margin is mainly useful for hydroelastic collision
where a compliant-layer offset is desired.
Edge simplification. mesh.build_sdf(...) also runs a dihedral-angle pre-filter over~Mesh.edges
the mesh's manifold edges and caches the surviving subset on the mesh; the SDF-mesh contact
pipeline picks up that cached set in preference to the unfiltered :attr:,
which materially reduces edge-vs-shape work for typical CAD or scanned meshes. The default
threshold (edge_lower_angle_threshold_rad=math.radians(0.1)) drops only edges that are
geometrically coplanar to within 0.1 degrees, so it is safe for most meshes; raise it to
prune more aggressively, set it to 0 to keep every manifold edge, or pass a negative
value (e.g. -1.0) to opt out of the simplification pass entirely. Setedge_box_absorption=True to additionally drop manifold edges that are fully covered by
another nearby edge's oriented box โ useful for densely tessellated curved surfaces.edge_box_half_normal/edge_box_half_normal_rel andedge_box_half_lateral/edge_box_half_lateral_rel tune the box extents (absolute~Mesh.build_sdf
metres or fractions of the mesh AABB diagonal); see :meth: for full
parameter docs.
On-disk SDF cache. Pass cache_dir to persist the cooked SDF and skip the cook on
subsequent runs:
.. code-block:: python
mesh.build_sdf(max_resolution=64, cache_dir="./sdf_cache")
Entries are content-addressed by mesh data and build parameters; changing any of them
produces a fresh entry automatically. shape_margin is applied at sample time and is
not part of the cache key. The on-disk format is internal and may change between Newton
versions โ caches are invalidated and re-cooked transparently.
.. note::
Watertight meshes are preferred. An SDF works best on a closed
surface, so meshes whose every edge is shared by exactly two triangles give the most
reliable inside/outside classification. Newton detects this automatically via
:attr:~Mesh.is_watertight and switches to a faster parity-based construction path
when it applies. Non-watertight meshes fall back to the slower winding-number path;
SDFs on terrain meshes work too, but mind the resolution (terrains have large
extents so surface features are easy to under-resolve) and expect noticeably
longer construction times.
Mesh simplification for collision
For imported models (URDF, MJCF, USD) whose visual meshes are too detailed for efficient
collision, :meth:~ModelBuilder.approximate_meshes replaces mesh collision shapes
with convex hulls, bounding boxes, or convex decompositions:
.. code-block:: python
builder.add_usd("robot.usda")
# Replace all collision meshes with convex hulls (default)
builder.approximate_meshes()
# Or target specific shapes and keep visual geometry
builder.approximate_meshes(
method="convex_hull",
shape_indices=non_finger_shapes,
keep_visual_shapes=True,
)
Supported methods: "convex_hull" (default), "bounding_box", "bounding_sphere","coacd" (convex decomposition), "vhacd".
.. note::
approximate_meshes() modifies the builder's shape geometry in-place. By default
(keep_visual_shapes=False), the original mesh is replaced for both collision and
rendering. Pass keep_visual_shapes=True to preserve the original mesh as a
visual-only shape alongside the simplified collision shape.
.. _Contact Reduction:
Contact Reduction
^^^^^^^^^^^^^^^^^
Contact reduction is enabled by default. For scenes with many mesh-mesh interactions that generate thousands of contacts, reduction selects a significantly smaller representative set that maintains stable contact behavior while improving solver performance.
How it works:
1. Contacts are binned by normal direction (polyhedron face directions)
2. Within each bin, contacts are scored by spatial distribution and penetration depth
3. Representative contacts are selected to preserve coverage and depth cues
To disable reduction, set reduce_contacts=False when creating the pipeline.
Configuring contact reduction (HydroelasticSDF.Config):
For hydroelastic and SDF-based contacts, use :class:~geometry.HydroelasticSDF.Config to tune reduction behavior:
.. testsetup:: hydro-config
import warp as wp
import newton
from newton import CollisionPipeline
builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
.. testcode:: hydro-config
from newton.geometry import HydroelasticSDF
config = HydroelasticSDF.Config(
reduce_contacts=True, # Enable contact reduction (default)
buffer_fraction=0.2, # Reduce GPU buffer allocations (default: 1.0)
normal_matching=True, # Align reduced normals with aggregate force
anchor_contact=False, # Optional center-of-pressure anchor contact
)
pipeline = CollisionPipeline(model, sdf_hydroelastic_config=config)
Other reduction options:
.. list-table::
:header-rows: 1
:widths: 30 70
* - Parameter
- Description
* - normal_matching
- Rotates selected contact normals so their weighted sum aligns with the aggregate force direction
from all unreduced contacts. Preserves net force direction after reduction. Default: True.
* - anchor_contact
- Adds an anchor contact at the center of pressure for each normal bin to better preserve moments.
Default: False.
* - margin_contact_area
- Lower bound on contact area. Hydroelastic stiffness is area * k_eff, but contacts
within the contact margin that are not yet penetrating have zero
geometric area. This provides a floor value so they still generate repulsive force. Default: 0.01.
.. _Shape Configuration:
Shape Configuration
-------------------
Shape collision behavior is controlled via :class:~ModelBuilder.ShapeConfig:
Collision control:
.. list-table::
:header-rows: 1
:widths: 30 70
* - Parameter
- Description
* - collision_group
- Collision group ID. 0 disables collisions. Default: 1.
* - collision_filter_parent
- Filter collisions with adjacent body (parent in articulation or connected via joint). Default: True.
* - has_shape_collision
- Whether shape collides with other shapes. Default: True.
* - has_particle_collision
- Whether shape collides with particles. Default: True.
Geometry parameters:
.. list-table::
:header-rows: 1
:widths: 25 75
* - Parameter
- Description
* - margin
- Surface offset used by narrow phase. Pairwise effect is additive (m_a + m_b): contacts are evaluated against the signed distance to the margin-shifted surfaces, so resting separation is m_a + m_b. Helps thin shells/cloth stability and reduces self-intersections. Default: 0.0.
* - gap
- Additional detection threshold. Pairwise effect is additive (g_a + g_b). Broad phase expands each shape AABB by (margin + gap) per shape; narrow phase then keeps a candidate contact when d <= g_a + g_b (with d measured relative to margin-shifted surfaces). Increasing gap detects contacts earlier and helps reduce tunneling. Default: None (uses builder.rigid_gap, which defaults to 0.1).
* - is_solid
- Whether shape is solid or hollow. Affects inertia and SDF sign. Default: True.
* - is_hydroelasticHydroelastic Contacts
- Whether the shape uses SDF-based hydroelastic contacts. Both shapes in a pair must have this enabled. See :ref:. Default: False.
* - kh
- Hydroelastic contact stiffness coefficient. Under the default linear
pressure law, pressure scales with kh and penetration depth; contact
force also scales with contact area. Default: 1.0e10.
.. _margin-gap-semantics:
Margin and gap semantics (where vs when):
- Where contacts are placed is controlled by margin.
- When contacts are generated is controlled by gap.
For a shape pair (a, b):
- Pair margin: m = margin_a + margin_b
- Pair gap: g = gap_a + gap_b
- Surface distance (true geometry, no offsets): s
- Contact-space distance used by Newton: d = s - m
Contacts are generated when:
.. math::
d \leq g \quad\Leftrightarrow\quad s \leq (m + g)
Broad phase uses the same idea by expanding each shape AABB by:
.. math::
margin_i + gap_i
This keeps broad-phase culling and narrow-phase contact generation consistent.
The solver enforces d >= 0, so objects at rest settle with surfaces separated
by margin_a + margin_b.
.. figure:: ../images/margin_and_gap.svg
:alt: Margin and gap contact generation phases
:width: 90%
:align: center
Margin sets contact location (surface offset), while gap adds an early
detection distance on top of margin. Left: no contact generated. Middle:
contact generated but not yet active. Right: active contact support.
SDF configuration (primitive generation defaults):
.. list-table::
:header-rows: 1
:widths: 30 70
* - Parameter
- Description
* - sdf_max_resolution
- Maximum SDF grid dimension (must be divisible by 8) for primitive SDF generation.
* - sdf_target_voxel_size
- Target voxel size for primitive SDF generation. Takes precedence over sdf_max_resolution.
* - sdf_narrow_band_range
- SDF narrow band distance range (inner, outer). Default: (-0.1, 0.1).
The :meth:~ModelBuilder.ShapeConfig.configure_sdf helper sets SDF and hydroelastic
options in one call:
.. testcode:: configure-sdf
builder = newton.ModelBuilder()
cfg = builder.ShapeConfig()
cfg.configure_sdf(max_resolution=64, is_hydroelastic=True, kh=1.0e11)
Example (mesh SDF workflow):
.. code-block:: python
cfg = builder.ShapeConfig(
collision_group=-1, # Collide with everything
margin=0.001, # 1mm margin
gap=0.01, # 1cm detection gap
)
my_mesh.build_sdf(max_resolution=64)
builder.add_shape_mesh(body, mesh=my_mesh, cfg=cfg)
Builder default gap:
The builder's rigid_gap (default 0.1) applies to shapes without explicit gap. Alternatively, use builder.default_shape_cfg.gap.
.. _speculative-contacts:
Speculative contacts (velocity-adapted gaps)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A fixed gap uses the same detection distance regardless of motion. Speculative
contacts retain a separated rigid-contact candidate when its contact points can close
the separation before the next collision update.
For a candidate with current contact-space separation d, authored pair gap g,
normal-directed closing speed v, collision-update horizon dt, and configured
limit e_max, the effective admission distance is:
.. math::
g_{effective} = \max\left(g, \min\left(v\,dt, e_{max}\right)\right)
The contact is kept when d <= g_effective. Newton computes v from relative
linear and angular velocity at the contact points. Common motion and receding motion
therefore do not enlarge the gap. Broad phase uses a conservative motion bound; narrow
phase applies the normal-directed test above.
Enable the feature with :class:CollisionPipeline.SpeculativeContactConfig:
.. code-block:: python
pipeline = newton.CollisionPipeline(
model,
speculative_config=newton.CollisionPipeline.SpeculativeContactConfig(
max_speculative_extension=0.1,
),
)
pipeline.collide(state, contacts, dt=1.0 / 60.0)
The per-call dt is the time [s] until the next plannedCollisionPipeline.collide
:meth: call, including skipped solver substeps, and is
required when speculative contacts are enabled. dt=0.0 uses only the fixed
gaps. max_speculative_extension caps the velocity-based distance [m]; 0.0
also disables velocity adaptation.
Speculation changes when a contact is retained, not its geometry: contact points remain
at their current separation rather than a predicted impact pose. Mesh and SDF contact
reduction preserves representative close-clearance and early-impact candidates.
.. note::
Speculative contacts are opt-in and currently apply to rigid, non-hydroelastic
contacts. They do not compute a time of impact or advance bodies to impact.
.. _Common Patterns:
Common Patterns
---------------
Creating static/ground geometry
Use body=-1 to attach shapes to the static world frame:
.. testcode:: static-geometry
builder = newton.ModelBuilder()
# Static ground plane
builder.add_ground_plane() # Convenience method
# Or manually create static shapes
builder.add_shape_plane(body=-1, xform=wp.transform_identity())
builder.add_shape_box(body=-1, hx=5.0, hy=5.0, hz=0.1) # Static floor
Setting default shape configuration
Use builder.default_shape_cfg to set defaults for all shapes:
.. testcode:: default-shape-cfg
builder = newton.ModelBuilder()
# Set defaults before adding shapes
builder.default_shape_cfg.ke = 1.0e6
builder.default_shape_cfg.kd = 1000.0
builder.default_shape_cfg.mu = 0.5
builder.default_shape_cfg.is_hydroelastic = True
builder.default_shape_cfg.sdf_max_resolution = 64 # Primitive SDF defaults
Soft contacts (particle-shape)
Soft contacts are generated automatically when particles are present. They use a separate margin:
.. testsetup:: soft-contacts
import warp as wp
import newton
from newton import CollisionPipeline
builder = newton.ModelBuilder()
builder.add_ground_plane()
builder.add_particle(pos=wp.vec3(0, 0, 1), vel=wp.vec3(0, 0, 0), mass=1.0)
model = builder.finalize()
state = model.state()
.. testcode:: soft-contacts
# Set soft contact margin
pipeline = CollisionPipeline(model, soft_contact_margin=0.01)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)
# Access soft contact data
n_soft = contacts.soft_contact_count.numpy()[0]
particles = contacts.soft_contact_particle.numpy()[:n_soft]
shapes = contacts.soft_contact_shape.numpy()[:n_soft]
.. _collision-frequency-in-the-simulation-loop:
Collision frequency in the simulation loop
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
These patterns apply to contacts generated by :class:~CollisionPipeline; they~solvers.SolverMuJoCo
do not control collision detection performed inside a solver. For example,
:class: generates contacts internally whenuse_mujoco_contacts=True (see :ref:mujoco-collision-pipeline), while~solvers.SolverVBD
:class: handles particle self-contact internally according
to particle_collision_detection_interval.
Start by calling collide every substep when debugging contact behavior.
This keeps contacts current as bodies move. Once the behavior is acceptable,
calling collide less often can reduce collision cost, especially for
hydroelastic or SDF-heavy scenes.
These are the common loop patterns:
.. testsetup:: sim-loop
import warp as wp
import newton
from newton import CollisionPipeline
builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
solver = newton.solvers.SolverXPBD(model, iterations=5)
pipeline = CollisionPipeline(model, broad_phase="sap")
state_0 = model.state()
state_1 = model.state()
control = model.control()
contacts = pipeline.contacts()
num_frames = 2
sim_substeps = 3
sim_dt = 1.0 / 60.0 / sim_substeps
collide_every_n = 2
Every substep (the debugging baseline, used by most basic examples):
.. testcode:: sim-loop
for frame in range(num_frames):
for substep in range(sim_substeps):
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, dt=sim_dt)
state_0, state_1 = state_1, state_0
Once per frame (faster, but contacts can become stale between substeps):
.. testcode:: sim-loop
for frame in range(num_frames):
pipeline.collide(state_0, contacts)
for substep in range(sim_substeps):
solver.step(state_0, state_1, control, contacts, dt=sim_dt)
state_0, state_1 = state_1, state_0
Every N substeps trades contact freshness for collision cost. Use a positive
integer: 1 means every substep, while values greater than or equal tosim_substeps reduce this frame-local loop to once per frame. Start at 1
and increase it while the task behavior remains acceptable:
.. testcode:: sim-loop
for frame in range(num_frames):
for substep in range(sim_substeps):
if substep % collide_every_n == 0:
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, dt=sim_dt)
state_0, state_1 = state_1, state_0
.. _Contact Generation:
Contact Data
------------
The :class:~Contacts class stores the results from the collision detection step~solvers.SolverBase.step
and is consumed by the solver :meth: method for contact handling.
Rigid contacts:
.. list-table::
:header-rows: 1
:widths: 35 65
* - Attribute
- Description
* - rigid_contact_count
- Number of active rigid contacts (scalar).
* - rigid_contact_shape0, rigid_contact_shape1
- Indices of colliding shapes.
* - rigid_contact_point0, rigid_contact_point1
- Contact point on each shape (body frame). This is the narrow-phase contact
location used by the solver for the normal constraint and lever-arm computation.
* - rigid_contact_offset0, rigid_contact_offset1
- Body-frame friction-anchor offset per shape, equal to the contact normal scaled
by effective_radius + margin. Added to the contact point to form a shifted
friction anchor that accounts for rotational effects of finite contact thickness
in tangential friction calculations.
* - rigid_contact_normal
- Contact normal, pointing from shape 0 toward shape 1 (world frame).
* - rigid_contact_margin0, rigid_contact_margin1
- Per-shape thickness: effective radius + margin (scalar).
* - rigid_contact_match_index
- Per-contact frame-to-frame match result (int32). Only allocated when
contact_matching is not "disabled".Contact Matching
See :ref:.
* - rigid_contact_new_indices, rigid_contact_new_count
- Compact index list of new contacts in the current sorted buffer. Only
allocated when contact_report=True.Contact Reports
See :ref:.
* - rigid_contact_broken_indices, rigid_contact_broken_count
- Compact index list of contacts from the previous frame that no current
contact matched. Only allocated when contact_report=True.Contact Reports
See :ref:.
Soft contacts (particle-shape):
.. list-table::
:header-rows: 1
:widths: 35 65
* - Attribute
- Description
* - soft_contact_count
- Total number of soft contacts (single element). With full-surface contact off, this equals the per-particle contact count and is unchanged from earlier releases.
* - soft_contact_indices
- Soft-side particle ids per contact, a vec3i with -1 padding: (p, -1, -1) particle, (v0, v1, -1) edge, (v0, v1, v2) face. The number of non-negative slots gives the feature kind; pair with soft_contact_barycentric to recover the contact point.
* - soft_contact_particle
- Particle id for particle contacts (-1 for edge/face records) โ the particle-only view of soft_contact_indices, for solvers that consume particle contacts exclusively.
* - soft_contact_barycentric
- Barycentric weights of the contact point over the record's soft particles ((1, 0, 0) for a particle contact).
* - soft_contact_shape
- Shape indices.
* - soft_contact_body_pos, soft_contact_body_vel
- Contact position and velocity on shape.
* - soft_contact_normal
- Contact normal.
Extended contact attributes (see :ref:extended_contact_attributes):
.. list-table::
:header-rows: 1
:widths: 22 78
* - Attribute
- Description
* - :attr:~Contacts.force~sensors.SensorContact
- Contact spatial forces (used by :class:).~solvers.SolverBase.update_contacts
Populated by :meth:.
.. note::
:class:~solvers.SolverXPBD with rigid_contact_con_weighting enabled~solvers.SolverXPBD.update_contacts
(the default) does not conserve momentum at contacts. The per-contact
forces written by :meth: are
approximate -- see that method's documentation for details.
Example usage:
.. testsetup:: contact-data
import warp as wp
import newton
builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
state = model.state()
.. testcode:: contact-data
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)
n = contacts.rigid_contact_count.numpy()[0]
points0 = contacts.rigid_contact_point0.numpy()[:n]
points1 = contacts.rigid_contact_point1.numpy()[:n]
normals = contacts.rigid_contact_normal.numpy()[:n]
# Shape indices
shape0 = contacts.rigid_contact_shape0.numpy()[:n]
shape1 = contacts.rigid_contact_shape1.numpy()[:n]
.. _Differentiable Contacts:
Differentiable Contacts
-----------------------
Use :func:newton.eval_rigid_contact_kinematics to reconstruct
selected rigid-contact quantities in caller-provided arrays. When those arrays
and state.body_q require gradients, the reconstruction participates inwp.Tape
:class: autodiff and provides first-order gradients with respect to
body poses.
.. experimental::
Rigid-contact differentiability may change without prior notice. Accuracy
and fitness for real-world optimization or learning workflows should be
validated case by case before relying on these gradients.
Making the full narrow-phase pipeline differentiable end-to-end would be
prohibitively expensive and numerically fragile โ iterative GJK/MPR solvers,
BVH traversals, and discrete contact-set changes all introduce discontinuities
or ill-conditioned gradients. Newton therefore keeps the narrow phase frozen
(enable_backward=False) and applies a lightweight post-processing step:
it re-reads the contact geometry produced by the narrow phase (body-local
points, world normal, margins) and reconstructs the world-space quantities
through the differentiable body_q. The result is a first-order
tangent-plane approximation that is cheap, stable, and sufficient for most
gradient-based optimization and reinforcement-learning workflows.
The optional outputs are signed contact distance and the two world-space
support points. Pass None for outputs that are not needed. The frozen~newton.Contacts.rigid_contact_normal
world-space normal is already available as
:attr:; it is not duplicated by the
helper and gradients do not flow through its direction.
.. testsetup:: diff-contacts
import warp as wp
import newton
.. testcode:: diff-contacts
builder = newton.ModelBuilder(gravity=(0.0, 0.0, 0.0))
body = builder.add_body(xform=wp.transform((0.0, 0.0, 0.3)))
builder.add_shape_sphere(body=body, radius=0.5)
builder.add_ground_plane()
model = builder.finalize(requires_grad=True)
# Disable deprecated automatic rigid-contact outputs. This also disables
# soft-contact gradients, which are independent of the helper below.
pipeline = newton.CollisionPipeline(model, requires_grad=False)
contacts = pipeline.contacts()
state = model.state(requires_grad=True)
distance = wp.empty(
contacts.rigid_contact_max,
dtype=float,
requires_grad=True,
)
with wp.Tape() as tape:
pipeline.collide(state, contacts)
newton.eval_rigid_contact_kinematics(
model,
state,
contacts,
out_distance=distance,
)
# Backpropagate through the active contact distances.
tape.backward(grads={
distance: wp.ones(
contacts.rigid_contact_max, dtype=float
)
})
grad_body_q = tape.gradients[state.body_q]
Starting in Newton 1.6, the Contacts.rigid_contact_diff_* attributes are
deprecated compatibility outputs. The distance and point arrays remain allocated
and populated when the collision pipeline has requires_grad=True during thenewton.eval_rigid_contact_kinematics
deprecation window.
Allocate only the outputs you need and call
:func: explicitly to prepare
for their removal. The deprecated rigid_contact_diff_normal attribute is~newton.Contacts.rigid_contact_normal
already an alias for
:attr: and does not allocate a duplicate
array.
.. _Creating Contacts:
Creating and Populating Contacts
--------------------------------
Create a :class:~CollisionPipeline explicitly, then allocate and populate a~Contacts
:class: buffer through that pipeline:
.. testsetup:: creating-contacts
import warp as wp
import newton
builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
state = model.state()
.. testcode:: creating-contacts
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)
The contacts buffer can be reused across steps -- collide clears it each time.
Construct the pipeline and contacts before CUDA graph capture so all collision
storage is allocated explicitly.
.. testcode:: creating-contacts
from newton import CollisionPipeline
pipeline = CollisionPipeline(
model,
broad_phase="sap",
rigid_contact_max=50000,
)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)
.. _Hydroelastic Contacts:
Hydroelastic Contacts
---------------------
Hydroelastic contacts are an opt-in feature that generates contact areas (not just points) using SDF-based collision detection. This provides more realistic and continuous force distribution, particularly useful for robotic manipulation scenarios.
Default behavior (hydroelastic disabled):
When is_hydroelastic=False (default), shapes use hard SDF contacts - point contacts computed from SDF distance queries. This is efficient and suitable for most rigid body simulations.
Opt-in hydroelastic behavior:
When is_hydroelastic=True on both shapes in a pair, the system generates distributed contact areas instead of point contacts. This is useful for:
- More stable and continuous contact forces for non-convex shape interactions
- Better force distribution across large contact patches
- Realistic friction behavior for flat-on-flat contacts
Requirements:
- Both shapes in a pair must have is_hydroelastic=True
- Shapes must have SDF data available:
- mesh shapes: call mesh.build_sdf(...)
- primitive shapes: use sdf_max_resolution or sdf_target_voxel_size in ShapeConfig
- For non-unit shape scale, the attached SDF must be scale-baked
- Only volumetric shapes supported (not planes, heightfields, or non-watertight meshes)
.. testcode:: hydroelastic
builder = newton.ModelBuilder()
body = builder.add_body()
cfg = builder.ShapeConfig(
is_hydroelastic=True, # Opt-in to hydroelastic contacts
sdf_max_resolution=64, # Required for hydroelastic
kh=1.0e11, # Contact stiffness
)
builder.add_shape_box(body, hx=0.5, hy=0.5, hz=0.5, cfg=cfg)
How it works:
1. SDF intersection finds overlapping regions between shapes
2. Marching cubes extracts the contact iso-surface
3. Contact points are distributed across the surface area
4. Optional contact reduction selects representative points
Hydroelastic stiffness (kh):
The kh parameter on each shape controls area-dependent contact stiffness. For a pair, the effective stiffness is computed as the harmonic mean: k_eff = 2 k_a k_b / (k_a + k_b). Tune this for desired penetration behavior.
Custom pressure laws:
The contact patch is the iso-pressure surface p_a == p_b. signed_depth
follows the SDF sign convention: negative inside the shape, positive outside.
The default linear law p = -kh * signed_depth is positive when penetrating
and continues with negative pressure values just outside the surface. Supplypressure_func and pressure_data on :class:~geometry.HydroelasticSDF.Config
to use a different law, for example a stiffer-with-depth response.
The callback is evaluated on both sides of the contact boundary during
iso-voxel pruning and marching-cubes interpolation, so it must be finite and
monotone non-increasing for every signed_depth value that can be sampled.
Do not clip the non-contact side to zero with wp.max(-signed_depth, 0.0).
When two shapes have different stiffnesses, the pressure-balance surface can
pass through a thin outside region; a flat zero-pressure segment can move or
remove that crossing. Extend the law into the non-contact side instead:
.. code-block:: python
@wp.struct
class PowerPressureData:
shape_kh: wp.array[wp.float32]
depth_ref_m: wp.float32
exponent: wp.float32
@wp.func
def power_pressure(signed_depth: wp.float32, shape_idx: wp.int32, data: PowerPressureData) -> wp.float32:
kh = data.shape_kh[shape_idx]
if signed_depth >= 0.0:
return -kh * signed_depth
depth = -signed_depth
return kh data.depth_ref_m wp.pow(depth / data.depth_ref_m, data.exponent)
model = builder.finalize()
data = PowerPressureData()
data.shape_kh = model.shape_material_kh
data.depth_ref_m = 0.001
data.exponent = 2.0
config = HydroelasticSDF.Config(pressure_func=power_pressure, pressure_data=data)
If pressure_data stores finalized model arrays such asmodel.shape_material_kh, build the config after builder.finalize().
The shape_idx argument passed to the callback indexes those finalized model
shape arrays directly. For simple power laws, avoid fitting both kh and an
additional gain unless you intentionally want a redundant parameterization: only
their product affects the resulting pressure.
When contact reduction is enabled, Newton reduces contacts after evaluating the
same pressure law on the hydroelastic faces; no separate linear stiffness law is
applied to reduced penetrating contacts.
See :github:newton/examples/contacts/example_nut_bolt_hydro.py for a worked example.
Contact reduction options for hydroelastic contacts are configured via :class:~geometry.HydroelasticSDF.Config (see :ref:Contact Reduction).
Hydroelastic memory can be tuned with buffer_fraction on~geometry.HydroelasticSDF.Config
:class:. This scales broadphase, iso-refinement,
and hydroelastic face-contact buffer allocations as a fraction of the worst-case
size. Lower values reduce memory usage but also reduce overflow headroom.
.. testcode:: hydro-buffer
from newton.geometry import HydroelasticSDF
config = HydroelasticSDF.Config(
reduce_contacts=True,
buffer_fraction=0.2, # 20% of worst-case (default: 1.0)
)
The default buffer_fraction is 1.0 (full worst-case allocation). Lowering it
reduces GPU memory usage but may cause overflow in dense contact scenes.
If runtime overflow warnings appear, increase buffer_fraction (or stage-specificbuffer_mult_* values) until warnings disappear in your target scenes.
.. _Contact Material Properties:
Contact Materials
-----------------
Shape material properties control contact resolution. Configure via :class:~ModelBuilder.ShapeConfig:
.. list-table::
:header-rows: 1
:widths: 12 34 10 22 22
* - Property
- Description
- Default
- ShapeConfig
- Model Array
* - mu~ModelBuilder.ShapeConfig.mu
- Coefficient of friction
- 1.0
- :attr:~Model.shape_material_mu
- :attr:
* - ke~ModelBuilder.ShapeConfig.ke
- Normal contact stiffness
- 2.5e3
- :attr:~Model.shape_material_ke
- :attr:
* - kd~ModelBuilder.ShapeConfig.kd
- Normal contact damping
- 100.0
- :attr:~Model.shape_material_kd
- :attr:
* - kf~ModelBuilder.ShapeConfig.kf
- Contact friction gain
- 1000.0
- :attr:~Model.shape_material_kf
- :attr:
* - ka~ModelBuilder.ShapeConfig.ka
- Adhesion distance
- 0.0
- :attr:~Model.shape_material_ka
- :attr:
* - restitution~ModelBuilder.ShapeConfig.restitution
- Bounciness
- 0.0
- :attr:~Model.shape_material_restitution
- :attr:
* - mu_torsional~ModelBuilder.ShapeConfig.mu_torsional
- Resistance to spinning at contact
- 0.005
- :attr:~Model.shape_material_mu_torsional
- :attr:
* - mu_rolling~ModelBuilder.ShapeConfig.mu_rolling
- Resistance to rolling motion
- 0.0001
- :attr:~Model.shape_material_mu_rolling
- :attr:
* - kh~ModelBuilder.ShapeConfig.kh
- Hydroelastic stiffness coefficient
- 1.0e10
- :attr:~Model.shape_material_kh
- :attr:
.. note::
Material properties are generic model data. Solvers and contact backends may
use, combine, or ignore fields according to their formulation. See the
:ref:Contact material support reference for built-in solver behavior, and
external solver documentation for third-party solvers.
.. note::
:class:~newton.solvers.SolverXPBD requires enable_restitution=True on
the solver constructor before restitution takes effect.
Example:
.. testcode:: material-config
builder = newton.ModelBuilder()
cfg = builder.ShapeConfig(
mu=0.8, # High friction
ke=1.0e6, # Stiff contact
kd=1000.0, # Damping
restitution=0.5, # Bouncy where supported
)
.. _USD Collision:
USD Integration
---------------
Newton provides several USD schema APIs for authoring collision and contact
properties directly in USD layers.
NewtonCollisionAPI
Applied to collision shapes to configure per-shape contact detection. Thenewton:contactMargin and newton:contactGap attributes map to~ModelBuilder.ShapeConfig.margin
:attr: and :attr:~ModelBuilder.ShapeConfig.gap
respectively.
.. code-block:: usda
def Cube "Collider" (
prepend apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI"]
) {
float newton:contactMargin = 0.001
float newton:contactGap = 0.02
}
NewtonMaterialAPI
Extends PhysicsMaterialAPI with torsional/rolling friction
(newton:torsionalFriction, newton:rollingFriction) and contact response~ModelBuilder.ShapeConfig
attributes. The contact response attributes map to :class:
fields as follows: newton:contactStiffness โ ke,newton:contactDamping โ kd, newton:contactFrictionGain โ kf,newton:contactAdhesion โ ka. A value of -inf means "use the engine'sContact Material Properties
default" (see :ref:).
.. code-block:: usda
def Material "RubberMaterial" (
prepend apiSchemas = ["PhysicsMaterialAPI", "NewtonMaterialAPI"]
) {
float physics:staticFriction = 1.0
float physics:dynamicFriction = 0.8
float newton:torsionalFriction = 0.1
float newton:rollingFriction = 0.01
float newton:contactStiffness = 5000.0
float newton:contactDamping = 200.0
float newton:contactFrictionGain = 800.0
float newton:contactAdhesion = 0.0
}
def Cube "Collider" (
prepend apiSchemas = ["PhysicsCollisionAPI"]
) {
rel material:binding:physics = </RubberMaterial>
}
NewtonMeshCollisionAPI
Applied on top of PhysicsMeshCollisionAPI to control mesh approximation.
Currently exposes newton:maxHullVertices for convex hull generation.
Custom Properties
Additional per-shape attributes that Newton reads:
.. code-block:: usda
def Cube "Collider" (
prepend apiSchemas = ["PhysicsCollisionAPI"]
) {
custom int newton:collision_group = 1
custom bool newton:is_sensor = false
}
See :doc:custom_attributes and :doc:usd_parsing for details.
.. _Deterministic Contacts:
Deterministic Contact Ordering
------------------------------
GPU thread scheduling is non-deterministic, so the order in which contacts are
written to the output buffer can vary between runs. Pass deterministic=True~newton.CollisionPipeline
to :class: (or :class:~newton.geometry.NarrowPhase) to guarantee
a reproducible contact order:
.. code-block:: python
pipeline = newton.CollisionPipeline(model, deterministic=True)
This enables two mechanisms:
1. Fingerprint tiebreaking โ each contact carries a geometry-derived
fingerprint (triangle/edge index) that is used as a deterministic tiebreaker
in the atomic_max contact reduction, so the reduction winner is
independent of thread scheduling.
2. Radix sort โ after the narrow phase, all contact arrays are reordered by
a 64-bit key encoding (shape_a, shape_b, sub_key) via a radix sort +
gather pass.
The overhead is small: fingerprint storage per contact, modified packing in
the reduction, and one radix sort + gather pass per frame. The sort is
fully CUDA-graph-capturable.
Hydroelastic contacts are covered by the same two mechanisms, using the
marching-cubes voxel and face index as the fingerprint. They additionally need
a third one, because hydroelastic reduction is the only contact path that sums
contributions across threads:
3. Fixed-point aggregation โ the per-normal-bin aggregates (contact force,
center of pressure, depth volume, and friction moments) are accumulated as
int64 fixed point rather than with floating-point atomics. Integer addition
is associative, so the sums are independent of the order in which threads
arrive and no ordering constraint is needed. A first pass records the
largest contribution per bin as a binary exponent, which sizes that bin's
fixed-point grid; the mantissa width is derived from the contact buffer
capacity so the sum cannot overflow.
Together these give bit-exact repeatability across runs on the same GPU
architecture when the consuming solver is deterministic as well.
.. note::
Deterministic mode disables hydroelastic pre-pruning, so the generated
contact set differs from the non-deterministic default.
.. _Contact Matching:
Contact Matching
----------------
Contact matching tracks contacts across frames, identifying which contacts
persist, which are new, and which have broken. The contact_matching~CollisionPipeline
argument on :class: selects one of three modes:
- "disabled" (default) โ no matching, no extra buffers.
- "latest" โ match current contacts against the previousContacts.rigid_contact_match_index
frame and populate :attr:, but keep theContacts
current frame's freshly generated contact geometry in the returned
:class: buffer.
- "sticky" โ match like "latest", then overwrite
each matched contact's body-frame contact points (point0/point1),
offsets (offset0/offset1), and world-frame normal with the
saved previous-frame values. The remaining contact fields
(shape0/shape1, margin0/margin1) are either key-derived
or per-shape constants and so are already identical for a matched
contact โ no extra state is kept for them. Unmatched contacts pass
through with their fresh narrow-phase geometry. Useful for stacking
scenarios where small frame-to-frame geometric jitter on persistent
contacts degrades stability.
.. experimental::
The way sticky contacts are updated across frames may change without prior
notice.
Any non-disabled mode implies deterministic=True.
.. testsetup:: contact-matching
import warp as wp
import newton
builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
state = model.state()
.. testcode:: contact-matching
pipeline = newton.CollisionPipeline(
model,
contact_matching="latest",
contact_matching_pos_threshold=0.005, # metres (default 0.0005)
contact_matching_normal_dot_threshold=0.9, # cos(~25ยฐ)
)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)
Each frame, the matcher binary-searches the current contacts against the
previous frame's sorted keys, then verifies candidates against a world-space
distance threshold and a normal dot-product threshold. The sort key encodes(shape_a, shape_b, sub_key) so only contacts between the same shape pair
are compared.
The distance metric is the world-space contact midpoint0.5 * (world(point0) + world(point1)) โ symmetric in shape 0 and shape 1
โ which means swapping the two shapes of a pair does not change whether a
contact matches. It also means pure changes in penetration depth register
as motion on both sides of the contact, not just one.
Thresholds
- contact_matching_pos_threshold โ maximum world-space distance [m]
between the previous and current contact midpoints for a match. Contacts
that moved more than this between frames are considered broken. Defaults
to 0.0005 m.
- contact_matching_normal_dot_threshold โ minimum dot product between old
and new contact normals. Below this the contact is reported as broken even
if the key and position match.
Sticky mode
Replay of the matched previous-frame geometry happens after the deterministic
sort, so match_index already addresses the final sorted layout. Unmatched
rows are left untouched, so new and threshold-broken contacts keep their fresh
narrow-phase geometry. Because
matching requires both a position delta below the threshold and a normal dot
product above the threshold, the saved values are guaranteed to be a close
approximation of the current geometry and are safe to reuse. The extra
per-contact buffers (four vec3 columns for the body-frame points and
offsets) are only allocated when the mode is "sticky"; "latest" and"disabled" pay zero additional memory and launch no additional kernels.
.. _Contact Reports:
Contact Reports
^^^^^^^^^^^^^^^
Pass contact_report=True to also collect compact index lists of new and
broken contacts each frame. contact_report=True requires a non-disabled
matching mode:
.. testcode:: contact-matching
pipeline = newton.CollisionPipeline(
model,
contact_matching="latest",
contact_report=True,
)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)
n_new = contacts.rigid_contact_new_count.numpy()[0]
new_indices = contacts.rigid_contact_new_indices.numpy()[:n_new]
n_broken = contacts.rigid_contact_broken_count.numpy()[0]
broken_indices = contacts.rigid_contact_broken_indices.numpy()[:n_broken]
rigid_contact_new_indices holds indices into the current frame's sorted
contact buffer for contacts without an accepted previous-frame match.
rigid_contact_broken_indices holds indices into the previous frame's
sorted buffer for contacts that no current contact matched.
.. _Performance:
Performance
-----------
- Use EXPLICIT (default) when collision pairs are limited (<100 shapes with most pairs filtered)
- Use SAP for >100 shapes with spatial coherence
- Use NxN for small scenes (<100 shapes) or uniform spatial distribution
- Minimize global entities (world=-1) as they interact with all worlds
- Use positive collision groups to reduce candidate pairs
- Use world indices for parallel simulations (essential for RL with many environments)
- Contact reduction is enabled by default for mesh-heavy scenes
- Pass rigid_contact_max to :class:~CollisionPipeline to limit memory in complex scenes~ModelBuilder.approximate_meshes
- Use :meth: to replace detailed visual meshes with convex hulls for collision
- Use viewer.log_contacts(contacts, state) in the render loop to visualize contact points and normals for debugging
Troubleshooting
- No contacts generated? Check that both shapes have compatible collision_group values (group 0 disables collision) and belong to the same world index.
- Mesh-mesh contacts slow? Attach an SDF with mesh.build_sdf(...) โ without it, Newton falls back to O(N) BVH vertex queries.
- Objects tunneling through each other? Increase gap to detect contacts earlier, or increase substep count (decrease simulation dt).
- Hydroelastic buffer overflow warnings? Increase buffer_fraction in :class:~geometry.HydroelasticSDF.Config.
Graph capture
The simulation loop (including collide and solver.step) can be captured withwp.ScopedCapture for reduced launch overhead. Place collide inside the
captured region so it is replayed each frame:
.. code-block:: python
if wp.get_device().is_cuda:
with wp.ScopedCapture() as capture:
pipeline.collide(state_0, contacts)
for _ in range(sim_substeps):
solver.step(state_0, state_1, control, contacts, dt)
state_0, state_1 = state_1, state_0
graph = capture.graph
# Each frame:
wp.capture_launch(graph)
.. _Solver Integration:
Solver Integration
------------------
Newton's collision pipeline works with all built-in solvers
(:class:~solvers.SolverXPBD, :class:~solvers.SolverVBD,~solvers.SolverSemiImplicit
:class:, :class:~solvers.SolverFeatherstone,~solvers.SolverMuJoCo
:class:). Pass the :class:~Contacts~solvers.SolverBase.step
object to :meth::
.. code-block:: python
solver.step(state_0, state_1, control, contacts, dt)
MuJoCo solver (see also :ref:MuJoCo Warp Integration)
By default (use_mujoco_contacts=True), :class:~solvers.SolverMuJoCo runs its own
contact generation and the contacts argument to step should be None.
To replace MuJoCo's contact generation with Newton's pipeline โ enabling advanced contact models
(SDF, hydroelastic) โ set use_mujoco_contacts=False and pass a populated~Contacts
:class: object to :meth:~solvers.SolverMuJoCo.step:
.. testsetup:: mujoco-solver
import warp as wp
import newton
builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
state_0 = model.state()
state_1 = model.state()
control = model.control()
num_steps = 2
.. testcode:: mujoco-solver
pipeline = newton.CollisionPipeline(model, broad_phase="sap")
solver = newton.solvers.SolverMuJoCo(
model,
use_mujoco_contacts=False,
)
contacts = pipeline.contacts()
for step in range(num_steps):
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, dt=1.0/60.0)
state_0, state_1 = state_1, state_0
.. _Advanced Customization:
Advanced Customization
----------------------
:class:~CollisionPipeline covers the vast majority of use cases, but Newton also
exposes the underlying broad phase, narrow phase, and primitive collision building blocks
for users who need full control โ for example, writing contacts in a custom format,
implementing a domain-specific culling strategy, or integrating Newton's collision
detection into an external solver.
Pipeline stages
The standard pipeline runs three stages:
1. AABB computation โ shape bounding boxes in world space.
2. Broad phase โ identifies candidate shape pairs whose AABBs overlap.
3. Narrow phase โ generates contacts for each candidate pair.
You can replace or compose these stages independently.
Broad phase classes
All broad phase classes expose a launch method that writes candidate pairs
(wp.array[wp.vec2i]) and a pair count:
.. list-table::
:header-rows: 1
:widths: 25 75
* - Class
- Description
* - :class:~geometry.BroadPhaseAllPairs
- All-pairs O(Nยฒ) AABB test. Accepts shape_world and optional shape_flags.~geometry.BroadPhaseSAP
* - :class:
- Sweep-and-prune. Same interface, with optional sweep_thread_count_multiplier
and sort_type tuning parameters.~geometry.BroadPhaseExplicit
* - :class:
- Tests precomputed shape_pairs against AABBs. No constructor arguments.
.. code-block:: python
from newton.geometry import BroadPhaseSAP
bp = BroadPhaseSAP(model.shape_world, model.shape_flags)
bp.launch(
shape_lower=shape_aabb_lower,
shape_upper=shape_aabb_upper,
shape_gap=model.shape_gap,
shape_collision_group=model.shape_collision_group,
shape_world=model.shape_world,
shape_count=model.shape_count,
candidate_pair=candidate_pair_buffer,
candidate_pair_count=candidate_pair_count,
device=device,
)
Narrow phase
:class:~geometry.NarrowPhase accepts the candidate pairs from any broad phase and
generates contacts:
.. code-block:: python
from newton.geometry import NarrowPhase
narrow_phase = NarrowPhase(
max_candidate_pairs=10000,
reduce_contacts=True,
device=device,
)
narrow_phase.launch(
candidate_pair=candidate_pairs,
candidate_pair_count=pair_count,
shape_types=...,
shape_data=...,
shape_transform=...,
# ... remaining geometry arrays from Model
contact_pair=out_pairs,
contact_position=out_positions,
contact_normal=out_normals,
contact_penetration=out_depths,
contact_count=out_count,
device=device,
)
To write contacts in a custom format, pass a contact_writer_warp_func (a Warp@wp.func) to the constructor to define the per-contact write logic, then calllaunch_custom_write instead of launch, providing a writer_data struct that
matches your writer function. Together these give full control over how and where contacts
are stored.
Primitive collision functions
For per-pair queries outside the pipeline, newton.geometry exports Warp device
functions (@wp.func) for specific shape combinations:
- collide_sphere_sphere, collide_sphere_capsule, collide_sphere_box,
collide_sphere_cylinder
- collide_capsule_capsule, collide_capsule_box
- collide_box_box
- collide_plane_sphere, collide_plane_capsule, collide_plane_box,
collide_plane_cylinder, collide_plane_ellipsoid
These return signed distance (negative = penetration), contact position, and contact
normal. Multi-contact variants (e.g., collide_box_box) return fixed-size vectors with
unused slots set to MAXVAL. Because they are @wp.func, they must be called from
within Warp kernels.
GJK, MPR, and multi-contact generators
For convex shapes that lack a dedicated collide_* function, Newton provides
factory functions that create Warp device functions from a support-map interface:
- create_solve_mpr(support_func) โ Minkowski Portal Refinement for boolean
collision and signed distance.
- create_solve_closest_distance(support_func) โ GJK closest-point query.
- create_solve_convex_multi_contact(support_func, writer_func, post_process_contact)
โ generates a stable multi-contact manifold and writes results through a callback.
.. note::
These factory functions are internal building blocks available from
newton._src.geometry. They are not part of the public API and may change between
releases, but are accessible for advanced users building custom narrow-phase routines.
See Also
--------
Imports:
.. testcode:: see-also-imports
import newton
from newton import (
CollisionPipeline,
Contacts,
GeoType,
)
from newton.geometry import (
BroadPhaseAllPairs,
BroadPhaseExplicit,
BroadPhaseSAP,
HydroelasticSDF,
NarrowPhase,
)
API Reference:
- :meth:~CollisionPipeline.contacts - Create a compatible contacts buffer~CollisionPipeline.collide
- :meth: - Run collision detection~CollisionPipeline
- :class: - Collision pipeline with configurable broad phase
- broad_phase - Broad phase algorithm: "nxn", "sap", or "explicit"~Contacts
- :class: - Contact data container~GeoType
- :class: - Shape geometry types~ModelBuilder.ShapeConfig
- :class: - Shape configuration options~ModelBuilder.ShapeConfig.configure_sdf
- :meth: - Set SDF and hydroelastic options in one call~geometry.HydroelasticSDF.Config
- :class: - Hydroelastic contact configuration~Mesh.build_sdf
- :meth: - Precompute SDF for a mesh~ModelBuilder.approximate_meshes
- :meth: - Replace mesh collision shapes with simpler geometry~ModelBuilder.replicate
- :meth: - Stamp out multi-world copies of a template builder~geometry.BroadPhaseAllPairs
- :class:, :class:~geometry.BroadPhaseSAP, :class:~geometry.BroadPhaseExplicit - Broad phase implementations~geometry.NarrowPhase
- :class: - Narrow phase contact generation
Model attributes:
- :attr:~Model.shape_collision_group - Per-shape collision groups~Model.shape_world
- :attr: - Per-shape world indices~Model.shape_gap
- :attr: - Per-shape contact gaps (detection threshold)~Model.shape_margin
- :attr: - Per-shape margin values (signed distance padding)
Related documentation:
- :ref:Contact material support - Material property behavior by solvercustom_attributes
- :doc: - USD custom attributes for collision propertiesusd_parsing
- :doc: - USD import options including collision settingssites
- :doc: - Non-colliding reference points
---
Concepts/Conventions
.. SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
.. SPDX-License-Identifier: CC-BY-4.0
Conventions
===========
This document covers the various conventions used across physics engines and graphics frameworks when working with Newton and other simulation systems.
Primer: Reference Points for Rigid-Body Spatial Force and Velocity
------------------------------------------------------------------
Newton uses rigid-body spatial forces (wrenches) and velocities (twists) in its API. These spatial vectors are defined with respect to a reference point.
When shifting the reference point, the force and velocity are updated in order to preserve the effect of the wrench, and the velocity field described by the twist, with respect to the new reference point.
The 6D wrench and twist are composed of a linear and an angular 3D-vector component, and in the context of these spatial vectors, their reference-point dependence is as follows:
- Point-independent components: linear force :math:\mathbf{f}, angular velocity :math:\boldsymbol{\omega}.\boldsymbol{\tau}
- Point-dependent components: angular torque (moment) :math:, linear velocity :math:\mathbf{v}.
Shifting the reference point by :math:\mathbf{r} = (\mathbf{p}_{\text{new}} - \mathbf{p}_{\text{old}}) changes the point-dependent vector components as follows:
.. math::
\boldsymbol{\tau}_{\text{new}} = \boldsymbol{\tau} + \mathbf{r} \times \mathbf{f}, \qquad
\mathbf{v}_{\text{new}} = \mathbf{v} + \boldsymbol{\omega} \times \mathbf{r}.
Keep this distinction in mind below: In addition to the coordinate frame that wrenches and twists are expressed in,
Newton documentation states the reference point that it expects. If you compute e.g. a wrench with respect to a different reference point, you must shift it to the expected reference point.
Spatial Twist Conventions
--------------------------
Twists in Modern Robotics
~~~~~~~~~~~~~~~~~~~~~~~~~~
In robotics, a twist is a 6-dimensional velocity vector combining angular
and linear velocity. Modern Robotics (Lynch & Park) defines two equivalent
representations of a rigid body's twist, depending on the coordinate frame
used:
* Body twist (:math:V_b):\omega_b
uses the body's body frame (often at the body's center of mass).
Here :math: is the angular velocity expressed in the body frame,v_b
and :math: is the linear velocity of a point at the body originV_b = (\omega_b,\;v_b)
(e.g. the COM) expressed in the body frame.
Thus :math: gives the body's own-frame view of its
motion.
* Spatial twist (:math:V_s):v_s
uses the fixed space frame (world/inertial frame).
:math: represents the linear velocity of a hypothetical point on the\omega_s
moving body that is instantaneously at the world origin, and
:math: is the angular velocity expressed in world coordinates. Equivalently,
.. math::
v_s \;=\; \dot p \;-\; \omega_s \times p,
where :math:p is the vector from the world origin to the body origin.V_s = (v_s,\;\omega_s)
Hence :math: is called the spatial twist.v_s
Note: :math: is not simply the COM velocity\dot p
(that would be :math:); it is the velocity of the world origin as
if rigidly attached to the body.
In summary, Modern Robotics lets us express the same physical motion either
in the body frame or in the world frame. The angular velocity is identical
up to coordinate rotation; the linear component depends on the chosen
reference point (world origin vs. body origin).
Physics-Engine Conventions (Drake, MuJoCo, Isaac)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Most physics engines store the COM linear velocity together with the
angular velocity of the body, typically both in world coordinates. This
corresponds conceptually to a twist taken at the COM and expressed in the
world frame, though details vary:
* Drake
Drake's multibody library uses full spatial vectors with explicit frame
names. The default, :math:V_{MB}^{E}, reads "velocity of frame BV_{WB}^{W}
measured in frame M, expressed in frame E." In normal use
:math: (body B in world W, expressed in W) contains(\omega_{WB}^{W},\;v_{WB_o}^{W})
:math:, i.e. both components in the world
frame. This aligns with the usual physics-engine convention.
* MuJoCo
MuJoCo employs a mixed-frame format for free bodies:
the linear part :math:(v_x,v_y,v_z) is the velocity of the body frame
origin (i.e., where qpos[0:3] is located) in the world frame, while the(\omega_x,\omega_y,\omega_z)
angular part :math: is expressed in the body
frame. The choice follows from quaternion integration (angular velocities
"live" in the quaternion's tangent space, a local frame). Note that when the
body's center of mass (body_ipos) is offset from the body frame origin,MuJoCo conversion <MuJoCo conversion>
the linear velocity is not the CoM velocityโsee :ref:
below for the relationship.
* Isaac Lab / Isaac Gym
NVIDIA's Isaac tools provide both linear and angular velocities in the
world frame. The root-state tensor returns
:math:(v_x,v_y,v_z,\;\omega_x,\omega_y,\omega_z) all expressed globally.
This matches Bullet/ODE/PhysX practice.
.. _Twist conventions:
Newton Conventions
~~~~~~~~~~~~~~~~~~
Newton follows the standard physics engine convention for most solvers,
aligning with Isaac Lab's approach.
Newton's public spatial_vector arrays use (linear, angular) ordering,
unlike Warp's native (angular, linear) convention. This applies to arraysnewton.State.body_qd
such as :attr: and :attr:newton.State.body_f.State.body_qd <newton.State.body_qd>
Newton's :attr: stores both linear and angular velocities
in the world frame.
.. code-block:: python
@wp.kernel
def get_body_twist(body_qd: wp.array[wp.spatial_vector]):
body_id = wp.tid()
# body_qd is a 6D wp.spatial_vector in world frame
twist = body_qd[body_id]
# linear velocity is the velocity of the body's center of mass in world frame
linear_velocity = twist[0:3]
# angular velocity is the angular velocity of the body in world frame
angular_velocity = twist[3:6]
wp.launch(get_body_twist, dim=model.body_count, inputs=[state.body_qd])
The linear velocity represents the COM velocity in world
coordinates, while the angular velocity is also expressed in world coordinates.
This matches the Isaac Lab convention exactly. Note that Newton will automatically
convert from this convention to MuJoCo's mixed-frame format when using the
SolverMuJoCo, including both the angular velocity frame conversion (world โ body)
and the linear velocity reference point conversion (CoM โ body frame origin).
If you need the velocity of the body-frame origin rather than the COM, shift the
linear term by the body's COM offset in world coordinates:
.. math::
v_{\text{origin}}^W = v_{\text{com}}^W - \omega^W \times r_{\text{com}}^W.
Summary of Conventions
~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 28 27 27 18
* - System
- Linear velocity (translation)
- Angular velocity (rotation)
- Twist term
- Modern Robotics* โ Body twist
- Body origin (chosen point; often COM), body frame
- Body frame
- "Body twist" (:math:V_b)V_s
- Modern Robotics* โ Spatial twist
- World origin, world frame
- World frame
- "Spatial twist" (:math:)B_o
* - Drake
- Body-frame origin :math: (not necessarily COM), world frameV_{WB}^{W}
- World frame
- Spatial velocity :math:[\mathbf{v}_{com}^W;\ \boldsymbol{\omega}^W]
* - MuJoCo
- Body-frame origin, world frame
- Body frame
- Mixed-frame 6-vector
* - Isaac Gym / Sim
- COM, world frame
- World frame
- "Root" linear/angular velocity
* - PhysX
- COM, world frame
- World frame
- Not named "twist"; typically treated as :math:~newton.State.body_qd
* - Newton
- COM, world frame
- World frame
- :attr:
Mapping Between Representations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Body โ Spatial (Modern Robotics)
For body pose :math:T_{sb}=(R,p):
.. math::
\omega_s \;=\; R\,\omega_b,
\qquad
v_s \;=\; R\,v_b \;+\; \omega_s \times p.
This is :math:V_s = \mathrm{Ad}_{(R,p)}\,V_b;R^{\mathsf T}
the inverse uses :math: and :math:-R^{\mathsf T}p.
Physics engine โ MR
Given engine values :math:(v_{\text{com}}^{W},\;\omega^{W})
(world-frame COM velocity and angular velocity):
1. Spatial twist at COM
:math:V_{WB}^{W} = (v_{\text{com}}^{W},\;\omega^{W})
2. Body-frame twist
:math:\omega_b = R^{\mathsf T}\omega^{W},v_b = R^{\mathsf T}v_{\text{com}}^{W}
:math:.
3. Shift to another origin offset :math:r from COM:
:math:v_{\text{origin}}^{W} = v_{\text{com}}^{W} + \omega^{W}\times r^{W},r^{W}=R\,r
where :math:.
.. _MuJoCo conversion:
MuJoCo conversion
Two conversions are needed between Newton and MuJoCo:
1. Angular velocity frame: Rotate MuJoCo's body-frame angular velocity by
:math:R to obtain the world-frame angular velocity (or vice versa):
.. math::
\omega^{W} = R\,\omega^{B}, \qquad \omega^{B} = R^{\mathsf T}\omega^{W}
2. Linear velocity reference point: MuJoCo's linear velocity is at the
body frame origin, while Newton uses the CoM velocity. When the body has a
non-zero CoM offset :math:r (body_ipos in MuJoCo, body_com in
Newton), convert using:
.. math::
v_{\text{origin}}^{W} = v_{\text{com}}^{W} - \omega^{W} \times r^{W},
\qquad
v_{\text{com}}^{W} = v_{\text{origin}}^{W} + \omega^{W} \times r^{W}
where :math:r^{W} = R\,r^{B} is the CoM offset expressed in world coordinates.
In all cases the conversion boils down to the reference point
(COM vs. another point) and the frame (world vs. body) used for each
component. Physics is unchanged; any linear velocity at one point follows
:math:v_{\text{new}} = v + \omega\times r.
Spatial Wrench Conventions
--------------------------
Newton represents external rigid-body forces as spatial wrenches in
:attr:State.body_f <newton.State.body_f>. The 6D wrench is stored in world
frame as:
.. math::
\mathbf{w} = \begin{bmatrix} \mathbf{f} \\ \boldsymbol{\tau} \end{bmatrix},
where :math:\mathbf{f} is the linear force and :math:\boldsymbol{\tau}\mathbf{r}
is the moment about the body's center of mass (COM), both expressed in
world coordinates. The reference point matters for the moment term, so shifting
the wrench to a point offset by :math: changes the torque as:
.. math::
\boldsymbol{\tau}_{\text{new}} = \boldsymbol{\tau} + \mathbf{r} \times \mathbf{f}.
This convention is used in all Newton solvers.
The array of joint forces (torques) in generalized coordinates is stored in :attr:Control.joint_f <newton.Control.joint_f>.
For FREE and DISTANCE joints, the corresponding 6 dimensions in this
array are the physical wrench in world coordinates, with the force and torque
referenced at the child body's center of mass (COM).
.. note::
MuJoCo represents root free-joint generalized forces in a mixed-frame convention in qfrc_applied. To preserve Newton's~newton.solvers.SolverMuJoCo
COM-wrench semantics for that root-free-joint case, :class: applies free-jointControl.joint_f <newton.Control.joint_f>
:attr: through xfrc_applied (world-frame wrench at the COM) and
uses qfrc_applied only for non-free joints. This keeps free-joint joint_f behavior aligned withState.body_f <newton.State.body_f>
:attr:.
We avoid converting free-joint wrenches into qfrc_applied directly because qfrc_applied is generalized-force
space, not a physical wrench. For free joints the 6-DOF basis depends on the current cdof (subtree COM frame),
and the rotational components are expressed in the body frame. A naive world-to-body rotation is insufficient because
the correct mapping is the Jacobian-transpose operation used internally by MuJoCo (the same path as xfrc_applied).
Routing through xfrc_applied ensures the wrench is interpreted at the COM in world coordinates and then mapped to
generalized forces consistently with MuJoCo's own dynamics.
Quaternion Ordering Conventions
--------------------------------
Different physics engines and graphics frameworks use different conventions
for storing quaternion components. This can cause significant confusion when
transferring data between systems or when interfacing with external libraries.
The quaternion :math:q = w + xi + yj + zk where :math:w is the scalar (x, y, z)
(real) part and :math: is the vector (imaginary) part, can be
stored in memory using different orderings:
.. list-table:: Quaternion Component Ordering
:header-rows: 1
:widths: 30 35 35
* - System
- Storage Order
- Description
* - Newton / Warp
- (x, y, z, w)
- Vector part first, scalar last
* - Isaac Lab / Isaac Sim
- (w, x, y, z)
- Scalar first, vector part last
* - MuJoCo
- (w, x, y, z)
- Scalar first, vector part last
* - USD (Universal Scene Description)
- (x, y, z, w)
- Vector part first, scalar last
Important Notes:
* Mathematical notation typically writes quaternions as :math:q = w + xi + yj + zk q = (w, x, y, z)
or :math:, but this doesn't dictate storage order.
* Conversion between systems requires careful attention to component ordering.
For example, converting from Isaac Lab to Newton requires reordering:
newton_quat = (isaac_quat[1], isaac_quat[2], isaac_quat[3], isaac_quat[0])
* Rotation semantics remain the same regardless of storage orderโonly the
memory layout differs.
* Warp's quat type uses (x, y, z, w) ordering, accessible via:
quat[0] (x), quat[1] (y), quat[2] (z), quat[3] (w).
When working with multiple systems, always verify quaternion ordering in your
data pipeline to avoid unexpected rotations or orientations.
Coordinate System and Up Axis Conventions
------------------------------------------
Different physics engines, graphics frameworks, and content creation tools use
different conventions for coordinate systems and up axis orientation. This can
cause significant confusion when transferring assets between systems or when
setting up physics simulations from existing content.
The up axis determines which coordinate axis points "upward" in the world,
affecting gravity direction, object placement, and overall scene orientation.
.. list-table:: Coordinate System and Up Axis Conventions
:header-rows: 1
:widths: 30 20 25 25
* - System
- Up Axis
- Handedness
- Notes
* - Newton
- Z (default)
- Right-handed
- Configurable via Axis.X/Y/Z
* - MuJoCo
- Z (default)
- Right-handed
- Standard robotics convention
* - USD
- Y (default)
- Right-handed
- Configurable as Y or Z
* - Isaac Lab / Isaac Sim
- Z (default)
- Right-handed
- Follows robotics conventions
Important Design Principle:
Newton itself is coordinate system agnostic and can work with any choice
of up axis. The physics calculations and algorithms do not depend on a specific
coordinate system orientation. However, it becomes essential to track the
conventions used by various assets and data sources to enable proper conversion
and integration at runtime.
Common Integration Scenarios:
* USD to Newton: Convert from USD's Y-up (or Z-up) to Newton's configured up axis
* MuJoCo to Newton: Convert from MuJoCo's Z-up to Newton's configured up axis
* Mixed asset pipelines: Track up axis per asset and apply appropriate transforms
Conversion Between Systems:
When converting assets between coordinate systems with different up axes,
apply the appropriate rotation transforms:
* Y-up โ Z-up: 90ยฐ rotation around the X-axis
* Maintain right-handedness: Ensure coordinate system handedness is preserved
Example Configuration:
.. code-block:: python
import newton
# Configure Newton for Z-up coordinate system (robotics convention)
builder = newton.ModelBuilder(up_axis=newton.Axis.Z, gravity=(0.0, 0.0, -9.81))
# Or use Y-up (graphics/animation convention)
builder = newton.ModelBuilder(up_axis=newton.Axis.Y, gravity=(0.0, -9.81, 0.0))
The up axis controls geometry conventions but does not constrain an explicit
gravity vector. Omitting gravity defaults to -9.81 along the configured
up axis. Passing a scalar gravity value is deprecated.
Color Space Handling
--------------------
Newton treats authored surface colors as display/sRGB RGB values by default.
Public color inputs such as :attr:newton.Model.shape_color,newton.Mesh.color
:attr:, and the color arguments onnewton.ModelBuilder
:class: shape helpers should be passed as the values you
want to see on screen, with components in [0, 1].
Rendering backends convert authored display colors to linear light for shading.
In other words, do not pre-linearize shape or mesh colors before assigning them
to Newton. When you need linear-light math explicitly, convert at the boundary
with :func:newton.utils.color_srgb_to_linear andnewton.utils.color_linear_to_srgb
:func:.
.. code-block:: python
import newton
display_color = (0.125, 0.125, 0.15)
builder = newton.ModelBuilder()
builder.add_ground_plane(color=display_color)
linear_color = newton.utils.color_srgb_to_linear(display_color)
Base-color textures stored on Newton models follow the same convention and are
kept display/sRGB-encoded.
Packed color and albedo outputs from :class:newton.sensors.SensorTiledCamera
use display/sRGB encoding by default. SetSensorTiledCamera.RenderConfig(output_color_space=newton.utils.ColorSpace.LINEAR)
when linear RGB bytes are required for downstream processing. Clear colors are
specified as display/sRGB packed RGBA values and are converted to linear when
linear output is requested.
Collision Primitive Conventions
-------------------------------
This section documents the conventions used for collision primitive shapes in Newton and compares them with other physics engines and formats. Understanding these conventions is essential when:
* Creating collision geometry programmatically with ModelBuilder
* Debugging unexpected collision behavior after asset import
* Understanding center of mass calculations for asymmetric shapes
Newton Collision Primitives
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Newton defines collision primitives with consistent conventions across all shape types. The following table summarizes the key parameters and properties for each primitive:
.. list-table:: Newton Collision Primitive Specifications
:header-rows: 1
:widths: 15 20 35 30
* - Shape
- Origin
- Parameters
- Notes
* - Box
- Geometric center
- hx, hy, hz (half-extents)
- Edges aligned with local axes
* - Sphere
- Center
- radius
- Uniform in all directions
* - Capsule
- Geometric center
- radius, half_height
- Extends along Z-axis; half_height excludes hemispherical caps
* - Cylinder
- Geometric center
- radius, half_height, optional barrel_radius
- Extends along Z-axis; barrel_radius curves the side as a symmetric circular arc
* - Cone
- Geometric center
- radius (base), half_height
- Extends along Z-axis; base at -half_height, apex at +half_height
* - Plane
- Shape frame origin
- width, length (or 0,0 for infinite)
- Normal along +Z of shape frame
* - Mesh
- User-defined
- Vertex and triangle arrays
- General triangle mesh (can be non-convex); CCW winding defines outward face normal
Shape Orientation and Alignment
All Newton primitives that have a primary axis (capsule, cylinder, cone) are aligned along the Z-axis in their local coordinate frame. The shape's transform determines its final position and orientation in the world or parent body frame.
For a cylinder, radius is the radius at both ends. Setting barrel_radius to a nonzero value replaces the
straight side profile with a symmetric circular arc of that radius before revolving it around the Z-axis.barrel_radius must then be at least half_height. Its default value of zero selects a regular cylinder.
Center of Mass Considerations
For most primitives, the center of mass coincides with the geometric origin. The cone is a notable exception:
* Cone COM: Located at (0, 0, -half_height/2) in the shape's local frame, which is 1/4 of the total height from the base toward the apex.
Collision Primitive Conventions Across Engines
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following tables compare how different engines and formats define common collision primitives:
Sphere Primitives
.. list-table::
:header-rows: 1
:widths: 25 35 40
* - System
- Parameter Convention
- Notes
* - Newton
- radius
- Origin at center
* - MuJoCo
- size[0] = radius
- Origin at center
* - USD (UsdGeomSphere)
- radius attribute
- Origin at center
* - USD Physics
- radius attribute
- Origin at center
Box Primitives
.. list-table::
:header-rows: 1
:widths: 25 35 40
* - System
- Parameter Convention
- Notes
* - Newton
- Half-extents (hx, hy, hz)
- Distance from center to face
* - MuJoCo
- Half-sizes in size attribute
- Can use fromto (Newton importer doesn't support)
* - USD (UsdGeomCube)
- size attribute (full dimensions)
- Edge length, not half-extent
* - USD Physics
- halfExtents attribute
- Matches Newton convention
Capsule Primitives
.. list-table::
:header-rows: 1
:widths: 25 35 40
* - System
- Parameter Convention
- Notes
* - Newton
- radius, half_height (excludes caps)
- Total length = 2*(radius + half_height)
* - MuJoCo
- size[0] = radius, size[1] = half-length (excludes caps)
- Can also use fromto for endpoints
* - USD (UsdGeomCapsule)
- radius, height (excludes caps)
- Full height of cylindrical portion
* - USD Physics
- radius, halfHeight (excludes caps)
- Similar to Newton
Cylinder Primitives
.. list-table::
:header-rows: 1
:widths: 25 35 40
* - System
- Parameter Convention
- Notes
* - Newton
- radius, half_height, optional barrel_radius
- Extends along Z-axis
* - MuJoCo
- size[0] = radius, size[1] = half-length
- Can use fromto; Newton's MJCF importer maps to capsule
* - USD (UsdGeomCylinder)
- radius, height (full height)
- Visual shape
* - USD Physics
- radius, halfHeight
- Newton's USD importer creates actual cylinders
Cone Primitives
.. list-table::
:header-rows: 1
:widths: 25 35 40
* - System
- Parameter Convention
- Notes
* - Newton
- radius (base), half_height
- COM offset at -half_height/2
* - MuJoCo
- Not supported
- N/A
* - USD (UsdGeomCone)
- radius, height (full height)
- Visual representation
* - USD Physics
- radius, halfHeight
- Physics representation
Plane Primitives
.. list-table::
:header-rows: 1
:widths: 25 35 40
* - System
- Definition Method
- Normal Direction
* - Newton
- Transform-based or plane equation
- +Z of shape frame
* - MuJoCo
- Size and orientation in body frame
- +Z of geom frame
* - USD
- No standard plane primitive
- Implementation-specific
Mesh Primitives
.. list-table::
:header-rows: 1
:widths: 25 35 40
* - System
- Mesh Type
- Notes
* - Newton
- General triangle mesh
- Can be non-convex
* - MuJoCo
- Convex hull only for collision
- Visual mesh can be non-convex
* - USD (UsdGeomMesh)
- General polygon mesh
- Visual representation
* - USD Physics
- Implementation-dependent
- May use convex approximation
Import Handling
~~~~~~~~~~~~~~~
Newton's importers automatically handle convention differences when loading assets. No manual conversion is required when using these importersโthey automatically transform shapes to Newton's conventions.
---
Concepts/Coupling
.. SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
.. SPDX-License-Identifier: CC-BY-4.0
.. currentmodule:: newton
Coupled Solvers
===============
.. experimental::
Newton's coupled-solver framework lets one simulation step be split across
multiple solver backends while those backends still exchange forces, poses, and
constraint information through a shared :class:Model. This is useful when a
scene combines material models or algorithms that are best handled by different
solvers: for example a MuJoCo or Kamino rigid mechanism coupled to VBD cloth,
XPBD particles coupled to MPM material, or rigid bodies connected to particles
through an ADMM constraint.
The framework is exposed as an experimental namespace rather than as flat
symbols on :mod:newton.solvers. Import the coupled solver types directly from
that namespace:
.. code-block:: python
from newton.solvers import SolverMuJoCo, SolverVBD
from newton.solvers.experimental.coupled import (
SolverCoupledADMM,
SolverCoupled,
SolverCoupledProxy,
)
The main public types are:
- :class:newton.solvers.experimental.coupled.ModelView: a view-local overlayModel
on a shared :class:.newton.solvers.experimental.coupled.CouplingInterface
- :class:: the hooknewton.solvers.experimental.coupled.SolverCoupled
protocol implemented by solvers that need custom coupled behavior.
- :class:: the shared basenewton.solvers.experimental.coupled.SolverCoupledProxy
for partitioning models, distributing state, stepping entries, and
reconciling results.
- :class:: a lagged ornewton.solvers.experimental.coupled.SolverCoupledADMM
staggered proxy coupling wrapper.
- :class:: a fixed
iteration ADMM coupling wrapper for model-derived joints, attachments, and
contacts.
Shared Model, Entry Views, and Ownership
----------------------------------------
Coupled simulations start from a single :class:Model. Each sub-solver receives~newton.solvers.experimental.coupled.ModelView
a :class: rather than the raw
model. A view delegates reads to the parent model until the coupler or user
applies a view-local override. The important idea is that sub-solvers can see
the same model topology while owning only the bodies, particles, joints, or
shapes assigned to their entry.
A :class:~newton.solvers.experimental.coupled.SolverCoupled.Entry describes
one sub-solver:
.. code-block:: python
entry = SolverCoupled.Entry(
name="soft",
solver=lambda view: SolverVBD(model=view, iterations=20, rigid_compliant_alm=True),
bodies=soft_body_ids,
particles=cloth_particle_ids,
shapes=cloth_shape_ids,
substeps=2,
)
The entry lists the objects the sub-solver owns. During construction,SolverCoupled creates a model view for every entry, deactivates non-owned
dynamic endpoints where appropriate, constructs the sub-solver by calling the
entry's solver(view) factory, and keeps per-entry input and output states.
Bind any extra solver constructor arguments in the factory itself. After a
top-level step, only owned outputs are reconciled back into the caller's sharedstate_out. This prevents two sub-solvers from overwriting the same body or
particle unless an explicit coupling algorithm is responsible for arbitration.
The shared base also manages:
- per-entry substeps, so one solver can take smaller time intervals than
another;
- copying public force input from State and Control into entry-local
state;
- entry-local collision visibility and shape ownership;
- input-state notifications for solvers with private history buffers;
- fallback effective-mass estimates from public model mass and inertia arrays.
ModelView applies view-local changes with copy-on-write semantics so the
coupler can hide, immobilize, or rescale endpoints without changing the parent
model. Parent-derived view masks are refreshed when relevant model-change
notifications arrive. Direct writes through returned Warp arrays are not
intercepted, so view-local edits should go through the coupled-solver API that
owns the view.
Coupling Hooks
--------------
Some solvers keep important state outside the public :class:State arrays ornewton.solvers.experimental.coupled.CouplingInterface
can report interface forces more accurately than a generic momentum fallback.
Those solvers implement
:class: hooks. TheNotImplementedError
hooks are ordinary methods with default mixin implementations. Solvers override
only the methods that need solver-specific behavior. A solver that cannot
support a hook raises :class: from that hook instead of
silently using an invalid path.
The protocol currently covers these concepts:
- coupling_notify_input_state_update() tells a solver that public state
arrays or force-input buffers were changed by the coupler. Its flagsnewton.StateFlags
argument uses :class:. VBD uses this to realign private
previous-pose state after proxy synchronization or ADMM iteration restarts.
MPM uses it to keep collider caches consistent.
- coupling_eval_gravity_acceleration() lets a solver report the body and
particle acceleration that it applies internally for gravity-like forces.
Proxy and ADMM couplers pass these acceleration arrays explicitly to rewind
and harvest hooks so solvers that scale or compensate gravity can avoid
double-applying it.
- coupling_rewind_proxy_body() and
coupling_rewind_proxy_particle() let a
destination solver prepare proxy velocities before a lagged proxy pass.
- coupling_harvest_proxy_wrenches() and
coupling_harvest_proxy_particle_forces() let a destination solver
report feedback forces from solver-native contact or transfer data.
- coupling_prepare_proxy_contacts() lets a destination solver filter or prepare
proxy-local contacts before its step.
- coupling_eval_effective_mass() and
coupling_eval_effective_mass_block() let a solver provide endpoint
effective mass instead of using raw model mass and inertia.
Force injection itself is not a hook. Couplers write into publicstate.body_f, state.particle_f, and control.joint_f buffers, then
call the normal solver step. Likewise, virtual and proximal mass changes are
applied to a ModelView and refreshed through the usualnotify_model_changed() path when a solver must rebuild private caches.
Proxy Coupling
--------------
Proxy coupling represents an endpoint owned by one solver as a proxy endpoint in
another solver. The source solver owns the real object. The destination solver
receives a proxy body or proxy particle with scaled virtual inertia, solves its
own local problem against that proxy, then returns feedback to the source on a
later pass or iteration.
This is a good match for coupling algorithms that are naturally one-way within a
substep but can converge through repeated lagged iterations. Examples include a
rigid body acting as a proxy collider inside a soft-body solve, XPBD particles
driving MPM transfer particles, or VBD reporting contact forces back to a rigid
source body.
A proxy pair is declared with
:class:newton.solvers.experimental.coupled.SolverCoupledProxy.Proxy:
.. code-block:: python
solver = SolverCoupledProxy(
model,
entries=[rigid_entry, soft_entry],
coupling=SolverCoupledProxy.Config(
proxies=[
SolverCoupledProxy.Proxy(
source="rigid",
destination="soft",
bodies=robot_body_ids,
proxy_bodies=robot_proxy_body_ids,
particles=(),
proxy_particles=(),
mass_scale=0.25,
mode="lagged",
proxy_relaxation=0.5,
)
],
iterations=4,
),
)
source and destination name entries. bodies and particles are
source endpoints. proxy_bodies and proxy_particles name the
corresponding destination endpoints. If a proxy list is None, the source
indices are reused in the destination view. mass_scale scales proxy body
mass/inertia and proxy particle mass in that destination view.proxy_relaxation blends harvested feedback with the previously lagged
feedback buffer after each destination solve. Values below 1 underrelax the
update, 1 keeps the harvested feedback unchanged, and values above 1
overrelax it.
Two proxy modes are available through the mode string:
- LAGGED synchronizes the source begin pose and end velocity into the
destination proxy, rewinds destination proxy velocity by previously applied
feedback, public force input, and gravity, then steps the destination. This is
the most common mode for relaxed fixed-point coupling.
- STAGGERED synchronizes the source end pose and velocity into the
destination and skips the generic lagged rewind. This is useful when the
scheduling already gives the destination a current source state.
After the destination step, the coupler harvests feedback. If the destination
solver implements a body or particle harvest hook, that hook can report
contact-native forces or transfer impulses. Otherwise the shared fallback
estimates feedback from proxy momentum change. The fallback is convenient for
simple particle proxy cases, but contact-rich or solver-private interactions are
usually better served by a custom harvest hook.
Proxy-local collision detection is optional. A proxy can provide acollision_pipeline factory that receives the destination ModelView. If
the factory returns a pipeline, the coupler owns a persistent contact buffer and
refreshes it at collide_interval; inner proxy iterations reuse that result.
Masked resets preserve both this cadence and cached contacts, and mark matching
history for entities selected by the mask, including global entities when the
final mask entry is set, for invalidation on the next refresh. If the factory
returns None or no factory is supplied, the destination solve uses contacts
passed to the outer step() call.
The generic proxy loop currently supports at most two solver entries. Within
that limit, body and particle mappings are grouped by (source,
destination). One source step and one destination step are performed for each
solver pair and proxy iteration, so a single proxy declaration can carry both
body and particle mappings around the same destination solve.
ADMM Coupling
-------------
ADMM coupling is the symmetric coupling path. Instead of placing a virtual proxy
inside another solver, it constructs interface rows between endpoints owned by
different entries. Each iteration restores entry states, applies a proximal
velocity target when configured, lets sub-solvers advance, solves local
interface rows, updates dual variables, and splats equal and opposite coupling
forces back to endpoint force buffers.
Compared with proxy coupling, ADMM is less invasive for sub-solvers: entries do
not need to represent proxy bodies or particles, filter proxy contacts, or
harvest proxy-native feedback. The tradeoff is that the coupler must implement
each supported interface row explicitly, so every cross-solver joint, attachment,
and contact type needs ADMM row support. Transient contacts and stiff
attachments also generally need several coupling iterations per step, while the
proxy path is often useful with a single lagged or staggered pass.
The implemented ADMM wrapper discovers constraint rows from the shared model and
enables contact rows through explicit
:class:newton.solvers.experimental.coupled.SolverCoupledADMM.ContactPair
objects. It does not currently accept arbitrary user-authored endpoint records
as public API. Supported row sources are:
- cross-solver model joints;
- custom body-particle attachment attributes;
- internally detected rigid-rigid, rigid-particle, and particle-particle
contacts.
Cross-solver model joints are owned by the coupler only when the two connected
bodies belong to different entries and the joint itself is not owned by either
sub-solver. This avoids solving the same constraint twice. The current generic
ADMM path supports BALL, FIXED, and REVOLUTE joints. Ball joints
create translational anchor-coincidence rows. Fixed joints add angular rows.
Revolute joints preserve the hinge axis and can add a dry-friction row from
model joint friction. Prismatic, distance, and D6 joint rows are not yet part of
the experimental API.
Body-particle attachments cover interfaces that cannot be represented by a
model joint because one endpoint is a particle. The helperSolverCoupledADMM.add_body_particle_attachment() registers and fills custom
attributes under coupling:body_particle_attachment with body id, particle id,
body-local point, stiffness, damping, and enabled state. Importers can author the
same custom attributes directly. Rows whose endpoints are unowned or owned by
the same entry are ignored; only cross-solver attachments are coupled by ADMM.
Contact coupling is enabled by adding one or more ContactPair values toSolverCoupledADMM.Config.contact_pairs. A contact pair names two entries.SolverCoupledADMM.auto_detect_contact_pairs(entries) can build the complete
pair list for every distinct entry combination.
For enabled contact pairs, the coupler owns private detection data and builds
rows from solver ownership: particle-shape rows between particle entries and
shapes on bodies owned by other entries, rigid-rigid rows from cross-entry shape
pairs, and particle-particle rows from cross-entry particle sets through a
private hash-grid stream. Friction is read from model material properties such
as shape_material_mu and Model.particle_mu at row-fill time; it is not aContactPair field. Contact rows use an isotropic Coulomb
maximum-dissipation projection. They do not solve cone complementarity directly.
ADMM contact buffers are fixed-capacity device arrays. Persistent contacts
warm-start local variables and dual variables by stable contact keys across
steps. The particle-particle stream is contacts-like and hash-grid based, but it
is internal to the ADMM coupler and should not be treated as a public contact
stream.
The main ADMM parameters are:
- iterations: fixed iteration count, chosen to be graph-capture friendly;
- rho: penalty weight for interface rows;
- gamma: proximal inertia and velocity weight;
- baumgarte: positional error stabilization for attachment/contact rows;
- stiffness and damping values for model-joint and body-particle attachment
rows;
- rigid contact matching mode, thresholds, and warm-start force scale.
When gamma is positive, the coupler scales owned body and particle masses in
each entry ModelView, asks sub-solvers to refresh model-derived caches, and
shifts entry input velocities toward the previous ADMM iterate. Endpoint
effective mass uses solver hooks when available and model fallbacks otherwise.
This keeps the implementation compatible with solvers that can provide an
articulated mass estimate, such as MuJoCo Warp, while still allowing simpler
solvers to participate.
Choosing Proxy or ADMM Coupling
-------------------------------
Use proxy coupling when one solver can reasonably treat the other solver's
endpoint as an obstacle, transfer participant, or virtual body over a substep.
Proxy coupling is often easier to tune for collider-style interactions and can
reuse destination solver contact machinery. It is also the path that currently
supports MPM transfer-active proxy particles and deformable collider particles.
Use ADMM coupling when the interface should be represented as a symmetric
constraint or frictional contact between entries. ADMM is better suited for
cross-solver joints, body-particle attachments, and contact rows that need equal
and opposite forces. It is more structured, but it also has more tuning
parameters and a narrower set of supported row types.
The two approaches share the same base concepts: model views, ownership,
entry-local state, force-buffer injection, input-state notifications, and
effective-mass hooks. A scene can often be formulated either way, but the
numerical behavior will differ. Proxy coupling behaves like a relaxed
fixed-point iteration over solver-specific dynamics. ADMM behaves like a fixed
iteration constrained optimization split over the entry solvers and interface
rows.
Solver-Specific Behavior
------------------------
Coupled solvers rely on solver-specific hooks only where generic public
model/state behavior is insufficient.
VBD uses proxy contact preparation, body-proxy harvesting, and input-state
notifications. The notification hook keeps private previous-body state aligned
when proxy poses are synchronized or ADMM iterations restart. The harvest path
reduces final rigid-rigid and body-particle contact forces onto proxy bodies
instead of relying on aggregate momentum differences. VBD also supports proxy
joints: :class:~newton.solvers.experimental.coupled.SolverCoupledProxy keeps
configured fixed, prismatic, or revolute joints (or their proxy_joints
aliases) enabled in the destination view so their constraints continue to act
between proxy bodies. For one-DoF drives, the coupler remaps source targets to
destination-local indices and copies joint_target_q and joint_target_qd
before each destination solve.
Implicit MPM supports proxy body and proxy particle rewind/harvest hooks.
Transfer-active proxy particles can participate in P2G/G2P momentum transfer
while being excluded from material volume, stress, strain, and constitutive
updates. Deformable collider particles registered through collider-particle ids
use collider impulse collection rather than material transfer.
XPBD understands proxy particles and proxy bodies in particle contact kernels.
Owned particles may collide with destination proxy particles, but proxy-proxy,
proxy-static, and proxy-particle versus proxy-body contacts are filtered so the
destination solve does not create feedback between two proxy endpoints or
against immovable particles.
MuJoCo provides GPU effective-mass hooks from MuJoCo Warp data so proxy virtual
inertia and ADMM endpoint weights can use articulated mass estimates rather than
raw body mass.
Current Limitations
-------------------
The coupled-solver framework is useful today, but it is still experimental:
- Proxy stability is tuning-sensitive. Virtual inertia scale, contact
stiffness, solver iterations, and lagged versus staggered scheduling strongly
affect damping and convergence.
- Generic momentum harvesting is only a fallback. Solver-private contact modes
should expose custom harvest hooks where possible.
- ADMM contact detection is internal and does not consume arbitrary caller
:class:Contacts rows as a public interface stream.~newton.solvers.SolverVBD
- ADMM joint support is limited to ball, fixed, and revolute rows.
- Particle-particle ADMM contacts use a private stream, not a public contact API.
- Effective-mass weighting falls back to simple model mass/inertia where no
custom hook is available.
- USD ownership, automatic coupled-solver construction, and high-level tuning
guidance are not part of the experimental public API yet.
- Full-surface (edge/face) rigid-soft contacts are consumed by
:class: only. The per-entry contact filter drops
them for other sub-solvers while preserving particle contacts, and keeps them
for VBD only when the entry owns every referenced corner. A record spanning
two entries is dropped by both. Proxy particles do not support them at all.
Treat coupled solvers as an advanced feature for controlled experiments and
solver integration work. Prefer focused regression tests and explicit scene
tuning when using them in new examples.
---
Concepts/Custom Attributes
.. SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
.. SPDX-License-Identifier: CC-BY-4.0
.. currentmodule:: newton
.. _custom_attributes:
Custom Attributes
=================
Newton's simulation model uses flat buffer arrays to represent physical properties and simulation state. These arrays can be extended with user-defined custom attributes to store application-specific data alongside the standard physics quantities.
Use Cases
---------
Custom attributes enable a wide range of simulation extensions:
* Per-body properties: Store thermal properties, material composition, sensor IDs, or hardware specifications
* Advanced control: Store PD gains, velocity limits, control modes, or actuator parameters per-joint or per-DOF
* Visualization: Attach colors, labels, rendering properties, or UI metadata to simulation entities
* Multi-physics coupling: Store quantities like surface stress, temperature fields, or electromagnetic properties
* Reinforcement learning: Store observation buffers, reward weights, optimization parameters, or policy-specific data directly on entities
* Solver-specific data: Store contact pair parameters, tendon properties, or other solver-specific entity types
Custom attributes follow Newton's flat array indexing scheme, enabling efficient GPU-parallel access while maintaining flexibility for domain-specific extensions.
Overview
--------
Newton organizes simulation data into four primary objects, each containing flat arrays indexed by simulation entities:
* Model Object (:class:~newton.Model) - Static configuration and physical properties that remain constant during simulation~newton.State
* State Object (:class:) - Dynamic quantities that evolve during simulation~newton.Control
* Control Object (:class:) - Control inputs and actuator commands~newton.Contacts
* Contact Object (:class:) - Contact-specific properties
Custom attributes extend these objects with user-defined arrays that follow the same indexing scheme as Newton's built-in attributes. The CONTACT assignment attaches attributes to the :class:~newton.Contacts object created during collision detection.
Declaring Custom Attributes
----------------------------
Custom attributes must be declared before use via the :meth:newton.ModelBuilder.add_custom_attribute method. Each declaration specifies:
* name: Attribute name
* frequency: Determines array size and indexingโeither a :class:~newton.Model.AttributeFrequency enum value (e.g., BODY, SHAPE, JOINT, JOINT_DOF, JOINT_COORD, ARTICULATION, ONCE) or a string for custom frequencies
* dtype: Warp data type (wp.float32, wp.vec3, wp.quat, etc.) or str for string attributes stored as Python lists
* assignment: Which simulation object owns the attribute (MODEL, STATE, CONTROL, CONTACT)
* default (optional): Default value for unspecified entities. When omitted, a sensible zero-value is derived from the dtype (0 for scalars, identity for quaternions, False for booleans, "" for strings)
* namespace (optional): Hierarchical organization for grouping related attributes
* references (optional): For multi-world merging, specifies how values are transformed (e.g., "body", "shape", "world", or a custom frequency key)
* values (optional): Pre-populated values โ dict[int, Any] for enum frequencies or list[Any] for custom string frequencies
When no namespace is specified, attributes are added directly to their assignment object (e.g., model.temperature). When a namespace is provided, Newton creates a namespace container (e.g., model.mujoco.damping).
.. testcode::
from newton import Model, ModelBuilder
import warp as wp
builder = ModelBuilder()
# Default namespace attributes - added directly to assignment objects
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="temperature",
frequency=Model.AttributeFrequency.BODY,
dtype=wp.float32,
default=20.0, # Explicit default value
assignment=Model.AttributeAssignment.MODEL
)
)
# โ Accessible as: model.temperature
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="velocity_limit",
frequency=Model.AttributeFrequency.BODY,
dtype=wp.vec3,
default=(1.0, 1.0, 1.0), # Default vector value
assignment=Model.AttributeAssignment.STATE
)
)
# โ Accessible as: state.velocity_limit
# Namespaced attributes - organized under namespace containers
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="float_attr",
frequency=Model.AttributeFrequency.BODY,
dtype=wp.float32,
default=0.5,
assignment=Model.AttributeAssignment.MODEL,
namespace="namespace_a"
)
)
# โ Accessible as: model.namespace_a.float_attr
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="bool_attr",
frequency=Model.AttributeFrequency.SHAPE,
dtype=wp.bool,
default=False,
assignment=Model.AttributeAssignment.MODEL,
namespace="namespace_a"
)
)
# โ Accessible as: model.namespace_a.bool_attr
# Articulation frequency attributes - one value per articulation
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="articulation_stiffness",
frequency=Model.AttributeFrequency.ARTICULATION,
dtype=wp.float32,
default=100.0,
assignment=Model.AttributeAssignment.MODEL
)
)
# โ Accessible as: model.articulation_stiffness
# ONCE frequency attributes - a single global value
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="gravity_scale",
frequency=Model.AttributeFrequency.ONCE,
dtype=wp.float32,
default=1.0,
assignment=Model.AttributeAssignment.MODEL
)
)
# โ Accessible as: model.gravity_scale (array of length 1)
# String dtype attributes - stored as Python lists, not Warp arrays
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="body_description",
frequency=Model.AttributeFrequency.BODY,
dtype=str,
default="unnamed",
assignment=Model.AttributeAssignment.MODEL
)
)
# โ Accessible as: model.body_description (Python list[str])
Default Value Behavior:
When entities don't explicitly specify custom attribute values, the default value is used:
.. testcode::
# First body uses the default value (20.0)
body1 = builder.add_body(mass=1.0)
# Second body overrides with explicit value
body2 = builder.add_body(
mass=1.0,
custom_attributes={"temperature": 37.5}
)
# Articulation attributes: create articulations with custom values
# Each add_articulation creates one articulation at the next index
for i in range(3):
base = builder.add_link(mass=1.0)
joint = builder.add_joint_free(child=base)
builder.add_articulation(
joints=[joint],
custom_attributes={
"articulation_stiffness": 100.0 + float(i) * 50.0 # 100, 150, 200
}
)
# After finalization, access attributes
model = builder.finalize()
temps = model.temperature.numpy()
arctic_stiff = model.articulation_stiffness.numpy()
print(f"Body 1: {temps[body1]}") # 20.0 (default)
print(f"Body 2: {temps[body2]}") # 37.5 (authored)
# Articulation indices reflect all articulations in the model
# (including any implicit ones from add_body)
print(f"Articulations: {len(arctic_stiff)}")
print(f"Last articulation stiffness: {arctic_stiff[-1]}") # 200.0
.. testoutput::
Body 1: 20.0
Body 2: 37.5
Articulations: 5
Last articulation stiffness: 200.0
.. note::
Uniqueness is determined by the full identifier (namespace + name):
- model.float_attr (key: "float_attr") and model.namespace_a.float_attr (key: "namespace_a:float_attr") can coexist
- model.float_attr (key: "float_attr") and state.namespace_a.float_attr (key: "namespace_a:float_attr") can coexist
- model.float_attr (key: "float_attr") and state.float_attr (key: "float_attr") cannot coexist - same key
- model.namespace_a.float_attr and state.namespace_a.float_attr cannot coexist - same key "namespace_a:float_attr"
Registering Solver Attributes:
Before loading assets, register solver-specific attributes:
.. testcode:: custom-attrs-solver
from newton import ModelBuilder
from newton.solvers import SolverMuJoCo
builder_mujoco = ModelBuilder()
SolverMuJoCo.register_custom_attributes(builder_mujoco)
# Now build your scene...
body = builder_mujoco.add_link()
joint = builder_mujoco.add_joint_free(body)
builder_mujoco.add_articulation([joint])
shape = builder_mujoco.add_shape_box(body=body, hx=0.1, hy=0.1, hz=0.1)
model_mujoco = builder_mujoco.finalize()
assert hasattr(model_mujoco, "mujoco")
assert hasattr(model_mujoco.mujoco, "condim")
MuJoCo boolean custom attributes use a parse_bool transformer (registered by :meth:~newton.solvers.SolverMuJoCo.register_custom_attributes) that handles strings ("true"/"false"), integers, and native booleans.
Authoring Custom Attributes
----------------------------
After declaration, values are assigned through the standard entity creation API (add_body, add_shape, add_joint). For default namespace attributes, use the attribute name directly. For namespaced attributes, use the format "namespace:attr_name".
.. testcode::
# Create a body with both default and namespaced attributes
body_id = builder.add_body(
mass=1.0,
custom_attributes={
"temperature": 37.5, # default โ model.temperature
"velocity_limit": [2.0, 2.0, 2.0], # default โ state.velocity_limit
"namespace_a:float_attr": 0.5, # namespaced โ model.namespace_a.float_attr
}
)
# Create a shape with a namespaced attribute
shape_id = builder.add_shape_box(
body=body_id,
hx=0.1, hy=0.1, hz=0.1,
custom_attributes={
"namespace_a:bool_attr": True, # โ model.namespace_a.bool_attr
}
)
Joint Frequency Types:
For joints, Newton provides three frequency types to store different granularities of data:
* JOINT frequency โ One value per joint
* JOINT_DOF frequency โ Values per degree of freedom (list, dict, or scalar for single-DOF joints)
* JOINT_COORD frequency โ Values per position coordinate (list, dict, or scalar for single-coordinate joints)
For JOINT_DOF and JOINT_COORD frequencies, values can be provided in three formats:
1. List format: Explicit values for all DOFs/coordinates (e.g., [100.0, 200.0] for 2-DOF joint)
2. Dict format: Sparse specification mapping indices to values (e.g., {0: 100.0, 2: 300.0} sets only DOF 0 and 2)
3. Scalar format: Single value for single-DOF/single-coordinate joints, automatically expanded to a list
.. testcode::
# Declare joint attributes with different frequencies
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="int_attr",
frequency=Model.AttributeFrequency.JOINT,
dtype=wp.int32
)
)
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="float_attr_dof",
frequency=Model.AttributeFrequency.JOINT_DOF,
dtype=wp.float32
)
)
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="float_attr_coord",
frequency=Model.AttributeFrequency.JOINT_COORD,
dtype=wp.float32
)
)
# Create a D6 joint with 2 DOFs (1 linear + 1 angular) and 2 coordinates
parent = builder.add_link(mass=1.0)
child = builder.add_link(mass=1.0)
cfg = ModelBuilder.JointDofConfig
joint_id = builder.add_joint_d6(
parent=parent,
child=child,
linear_axes=[cfg(axis=[1, 0, 0])], # 1 linear DOF
angular_axes=[cfg(axis=[0, 0, 1])], # 1 angular DOF
custom_attributes={
"int_attr": 5, # JOINT frequency: single value
"float_attr_dof": [100.0, 200.0], # JOINT_DOF frequency: list with 2 values (one per DOF)
"float_attr_coord": [0.5, 0.7], # JOINT_COORD frequency: list with 2 values (one per coordinate)
}
)
builder.add_articulation([joint_id])
# Scalar format for single-DOF joints (automatically expanded to list)
parent2 = builder.add_link(mass=1.0)
child2 = builder.add_link(mass=1.0)
revolute_joint = builder.add_joint_revolute(
parent=parent2,
child=child2,
axis=[0, 0, 1],
custom_attributes={
"float_attr_dof": 150.0, # Scalar for 1-DOF joint (expanded to [150.0])
"float_attr_coord": 0.8, # Scalar for 1-coord joint (expanded to [0.8])
}
)
builder.add_articulation([revolute_joint])
# Dict format for sparse specification (only set specific DOF/coord indices)
parent3 = builder.add_link(mass=1.0)
child3 = builder.add_link(mass=1.0)
d6_joint = builder.add_joint_d6(
parent=parent3,
child=child3,
linear_axes=[cfg(axis=[1, 0, 0]), cfg(axis=[0, 1, 0])], # 2 linear DOFs
angular_axes=[cfg(axis=[0, 0, 1])], # 1 angular DOF
custom_attributes={
"float_attr_dof": {0: 100.0, 2: 300.0}, # Dict: only DOF 0 and 2 specified
}
)
builder.add_articulation([d6_joint])
Accessing Custom Attributes
----------------------------
After finalization, custom attributes become accessible as Warp arrays. Default namespace attributes are accessed directly on their assignment object, while namespaced attributes are accessed through their namespace container.
.. testcode::
# Finalize the model
model = builder.finalize()
state = model.state()
# Access default namespace attributes (direct access on assignment objects)
temperatures = model.temperature.numpy()
velocity_limits = state.velocity_limit.numpy()
print(f"Temperature: {temperatures[body_id]}")
print(f"Velocity limit: {velocity_limits[body_id]}")
# Access namespaced attributes (via namespace containers)
namespace_a_body_floats = model.namespace_a.float_attr.numpy()
namespace_a_shape_bools = model.namespace_a.bool_attr.numpy()
print(f"Namespace A body float: {namespace_a_body_floats[body_id]}")
print(f"Namespace A shape bool: {bool(namespace_a_shape_bools[shape_id])}")
.. testoutput::
Temperature: 37.5
Velocity limit: [2. 2. 2.]
Namespace A body float: 0.5
Namespace A shape bool: True
Custom attributes follow the same GPU/CPU synchronization rules as built-in attributes and can be modified during simulation.
USD Integration
---------------
Custom attributes can be authored in USD files using a declaration-first pattern, similar to the Python API. Declarations are placed on the PhysicsScene prim, and individual prims can then assign values to these attributes.
Declaration Format (on PhysicsScene prim):
.. code-block:: usda
def PhysicsScene "physicsScene" {
# Default namespace attributes
custom float newton:float_attr = 0.0 (
customData = {
string assignment = "model"
string frequency = "body"
}
)
custom float3 newton:vec3_attr = (0.0, 0.0, 0.0) (
customData = {
string assignment = "state"
string frequency = "body"
}
)
# ARTICULATION frequency attribute
custom float newton:articulation_stiffness = 100.0 (
customData = {
string assignment = "model"
string frequency = "articulation"
}
)
# Custom namespace attributes
custom float newton:namespace_a:some_attrib = 150.0 (
customData = {
string assignment = "control"
string frequency = "joint_dof"
}
)
custom bool newton:namespace_a:bool_attr = false (
customData = {
string assignment = "model"
string frequency = "shape"
}
)
}
Assignment Format (on individual prims):
.. code-block:: usda
def Xform "robot_arm" (
prepend apiSchemas = ["PhysicsRigidBodyAPI"]
) {
# Override declared attributes with custom values
custom float newton:float_attr = 850.0
custom float3 newton:vec3_attr = (1.0, 0.5, 0.3)
custom float newton:namespace_a:some_attrib = 250.0
}
def Mesh "gripper" (
prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsCollisionAPI"]
) {
custom bool newton:namespace_a:bool_attr = true
}
After importing the USD file, attributes are accessible following the same patterns as programmatically declared attributes:
.. testcode::
:skipif: True
from newton import ModelBuilder
builder_usd = ModelBuilder()
builder_usd.add_usd("robot_arm.usda")
model = builder_usd.finalize()
state = model.state()
control = model.control()
# Access default namespace attributes
float_values = model.float_attr.numpy()
vec3_values = state.vec3_attr.numpy()
# Access namespaced attributes
namespace_a_floats = control.namespace_a.some_attrib.numpy()
namespace_a_bools = model.namespace_a.bool_attr.numpy()
For more information about USD integration and the schema resolver system, see :doc:usd_parsing.
MJCF and URDF Integration
--------------------------
Custom attributes can also be parsed from MJCF and URDF files. Each :class:~newton.ModelBuilder.CustomAttribute has optional fields for controlling how values are extracted from these formats:
* :attr:~newton.ModelBuilder.CustomAttribute.mjcf_attribute_name โ name of the XML attribute to read (defaults to the attribute name)~newton.ModelBuilder.CustomAttribute.mjcf_value_transformer
* :attr: โ callable that converts the XML string value to the target dtype~newton.ModelBuilder.CustomAttribute.urdf_attribute_name
* :attr: โ name of the XML attribute to read (defaults to the attribute name)~newton.ModelBuilder.CustomAttribute.urdf_value_transformer
* :attr: โ callable that converts the XML string value to the target dtype
These are primarily used by solver integrations (e.g., :meth:~newton.solvers.SolverMuJoCo.register_custom_attributes registers MJCF transformers for MuJoCo-specific attributes like condim, priority, and solref). When no transformer is provided, values are parsed using a generic string-to-Warp converter.
.. code-block:: python
# Example: register an attribute that reads "damping" from MJCF joint elements
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="custom_damping",
frequency=Model.AttributeFrequency.JOINT_DOF,
dtype=wp.float32,
default=0.0,
namespace="myns",
mjcf_attribute_name="damping", # reads <joint damping="..."/>
)
)
Validation and Constraints
---------------------------
The custom attribute system enforces several constraints to ensure correctness:
* Attributes must be declared via add_custom_attribute() before use (raises AttributeError otherwise)
* Each attribute must be used with entities matching its declared frequency (raises ValueError otherwise)
* Each full attribute identifier (namespace + name) can only be declared once with a specific assignment, frequency, and dtype
* The same attribute name can exist in different namespaces because they create different full identifiers
Custom Frequencies
==================
While enum frequencies (BODY, SHAPE, JOINT, etc.) cover most use cases, some data structures have counts independent of built-in entity types. Custom frequencies address this by allowing a string instead of an enum for the :attr:~newton.ModelBuilder.CustomAttribute.frequency parameter.
Example use case: MuJoCo's <contact><pair> elements define contact pairs between geometries. These pairs have their own count independent of bodies or shapes, and their indices must be remapped when merging worlds.
Registering Custom Frequencies
------------------------------
Custom frequencies must be registered before use via :meth:~newton.ModelBuilder.add_custom_frequency using a :class:~newton.ModelBuilder.CustomFrequency object. This explicit registration ensures clarity about which entity types exist and enables optional USD parsing support.
.. testsetup:: custom-freqs
from newton import Model, ModelBuilder
builder = ModelBuilder()
.. testcode:: custom-freqs
# Register a custom frequency
builder.add_custom_frequency(
ModelBuilder.CustomFrequency(
name="item",
namespace="myns",
)
)
The frequency key follows the same namespace rules as attribute keys: if a namespace is provided, it is prepended to the name (e.g., "mujoco:pair"). When declaring a custom attribute, the :attr:~newton.ModelBuilder.CustomAttribute.frequency string must match this full key.
Declaring Custom Frequency Attributes
-------------------------------------
Once a custom frequency is registered, pass a string instead of an enum for the :attr:~newton.ModelBuilder.CustomAttribute.frequency parameter when adding attributes:
.. testcode:: custom-freqs
# First register the custom frequency
builder.add_custom_frequency(
ModelBuilder.CustomFrequency(name="pair", namespace="mujoco")
)
# Then add attributes using that frequency
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="pair_geom1",
frequency="mujoco:pair", # Custom frequency (string)
dtype=wp.int32,
namespace="mujoco",
)
)
.. note::
Attempting to add an attribute with an unregistered custom frequency will raise a ValueError.
Adding Values
-------------
Custom frequency values are appended using :meth:~newton.ModelBuilder.add_custom_values:
.. testcode:: custom-freqs
# Declare attributes sharing the "myns:item" frequency
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(name="item_id", frequency="myns:item", dtype=wp.int32, namespace="myns")
)
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(name="item_value", frequency="myns:item", dtype=wp.float32, namespace="myns")
)
# Append values together
builder.add_custom_values({
"myns:item_id": 100,
"myns:item_value": 2.5,
})
builder.add_custom_values({
"myns:item_id": 101,
"myns:item_value": 3.0,
})
# Finalize (requires at least one articulation)
_body = builder.add_link()
_joint = builder.add_joint_free(_body)
builder.add_articulation([_joint])
model = builder.finalize()
print(model.myns.item_id.numpy())
print(model.myns.item_value.numpy())
.. testoutput:: custom-freqs
[100 101]
[2.5 3. ]
For convenience, :meth:~newton.ModelBuilder.add_custom_values_batch appends multiple rows in a single call:
.. code-block:: python
builder.add_custom_values_batch([
{"myns:item_id": 100, "myns:item_value": 2.5},
{"myns:item_id": 101, "myns:item_value": 3.0},
])
Validation: All attributes sharing a custom frequency must have the same count at finalize() time. This catches synchronization bugs early.
USD Parsing Support
-------------------
Custom frequencies can support automatic USD parsing:
In this section, a row means one appended set of values for a custom frequency
(that is, one index entry across all attributes in that frequency, equivalent to
one call to :meth:~newton.ModelBuilder.add_custom_values).
* :attr:~newton.ModelBuilder.CustomFrequency.usd_prim_filter selects which prims should emit rows.~newton.ModelBuilder.CustomFrequency.usd_entry_expander
* :attr: (optional) expands one prim into multiple rows.
.. code-block:: python
def is_actuator_prim(prim, context):
"""Return True for prims with type name MjcActuator."""
return prim.GetTypeName() == "MjcActuator"
builder.add_custom_frequency(
ModelBuilder.CustomFrequency(
name="actuator",
namespace="mujoco",
usd_prim_filter=is_actuator_prim,
)
)
For one-to-many mappings (one prim -> many rows):
.. code-block:: python
def is_tendon_prim(prim, context):
return prim.GetTypeName() == "MjcTendon"
def expand_joint_rows(prim, context):
return [
{"mujoco:tendon_joint": 4, "mujoco:tendon_coef": 0.5},
{"mujoco:tendon_joint": 8, "mujoco:tendon_coef": 0.5},
]
builder.add_custom_frequency(
ModelBuilder.CustomFrequency(
name="tendon_joint",
namespace="mujoco",
usd_prim_filter=is_tendon_prim,
usd_entry_expander=expand_joint_rows,
)
)
When :meth:~newton.ModelBuilder.add_usd runs:
1. Parses standard entities (bodies, shapes, joints, etc.).
2. Collects custom frequencies that define :attr:~newton.ModelBuilder.CustomFrequency.usd_prim_filter.
3. Traverses prims under the requested root_path once (including instance proxies via
Usd.TraverseInstanceProxies()).~newton.ModelBuilder.CustomFrequency.usd_entry_expander
4. For each prim, evaluate matching frequencies in registration order:
- If :attr: is set, one row is appended per emitted dictionary,
and default per-attribute USD extraction for that frequency is skipped for that prim.
- Otherwise, one row is appended from the frequency's declared attributes.
Callback inputs:
* usd_prim_filter(prim, context) and usd_entry_expander(prim, context) receive
the same context shape.
* context is a small dictionary:
- prim: current USD prim (same object as the prim argument)
- builder: current :class:~newton.ModelBuilder instance
- result: dictionary returned by :meth:~newton.ModelBuilder.add_usd
.. note::
Important behavior:
- Frequency callbacks are evaluated in deterministic registration order for each visited prim.
- If a frequency defines :attr:~newton.ModelBuilder.CustomFrequency.usd_entry_expander, then for every matched~newton.ModelBuilder.CustomAttribute
prim in that frequency, the expander output is the only source of row values.
- In that expander code path, the normal :class: USD parsing path is skipped~newton.ModelBuilder.CustomAttribute.usd_attribute_name
for that frequency/prim. In other words,
:attr: and~newton.ModelBuilder.CustomAttribute.usd_value_transformer
:attr: are not evaluated for those rows.
- Example: if frequency "mujoco:tendon_joint" has an expander and attribute
CustomAttribute(name="tendon_coef", frequency="mujoco:tendon_joint", ...), then tendon_coef is populated
only from keys returned by the expander rows. If a row omits "mujoco:tendon_coef", the value is treated as
None and the attribute default is applied at finalize time.
This mechanism lets solvers such as MuJoCo define USD-native schemas and parse them automatically
during model import.
Deriving Values from Prim Data (Wildcard Attribute)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, each custom attribute reads its value from a specific USD attribute on the prim (e.g., newton:myns:my_attr). Sometimes, however, you want to compute an attribute value from arbitrary prim data rather than reading a single named attribute. This is what setting :attr:~newton.ModelBuilder.CustomAttribute.usd_attribute_name to "*" is for.
When :attr:~newton.ModelBuilder.CustomAttribute.usd_attribute_name is set to "*", the attribute's :attr:~newton.ModelBuilder.CustomAttribute.usd_value_transformer is called for every prim matching the attribute's frequency โ regardless of which USD attributes exist on that prim. The transformer receives None as the value (since there is no specific attribute to read) and a context dictionary containing the prim and the attribute definition.
A :attr:~newton.ModelBuilder.CustomAttribute.usd_value_transformer must be provided when using "*"; omitting it raises a :class:ValueError.
Example: Suppose your USD stage contains "sensor" prims, each with an arbitrary sensor:position attribute. You want to store the distance from the origin as a custom attribute, computed at parse time:
.. code-block:: python
import warp as wp
import numpy as np
# 1. Register the custom frequency with a filter that selects sensor prims
def is_sensor(prim, context):
return prim.GetName().startswith("Sensor")
builder.add_custom_frequency(
ModelBuilder.CustomFrequency(
name="sensor",
namespace="myns",
usd_prim_filter=is_sensor,
)
)
# 2. Define a transformer that computes the distance from prim data
def compute_distance(value, context):
pos = context["prim"].GetAttribute("sensor:position").Get()
return wp.float32(float(np.linalg.norm(pos)))
# 3. Register the attribute with usd_attribute_name="*"
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="distance",
frequency="myns:sensor",
dtype=wp.float32,
default=0.0,
namespace="myns",
usd_attribute_name="*",
usd_value_transformer=compute_distance,
)
)
# 4. Parse the USD stage (assuming stage is an existing Usd.Stage)
builder.add_usd(stage)
model = builder.finalize()
# Access the computed values
distances = model.myns.distance.numpy()
The transformer context dictionary contains:
* "prim": The current USD prim.
* "attr": The :class:~newton.ModelBuilder.CustomAttribute being evaluated.~newton.ModelBuilder.add_usd
* When called from :meth: custom-frequency parsing,
context also includes "result" (the add_usd return dictionary) and
"builder" (the current :class:~newton.ModelBuilder).
This pattern is useful when:
* The value you need doesn't exist as a single USD attribute (it must be derived from multiple attributes, prim metadata, or relationships).
* You want to run the same computation for every prim of a given frequency without requiring an authored attribute on each prim.
* You need to look up related entities (for example, resolving a prim relationship
to a body index through context["result"]["path_body_map"]).
Multi-World Merging
-------------------
When using add_builder(), add_world(), or replicate() in multi-world simulations, the :attr:~newton.ModelBuilder.CustomAttribute.references field specifies how attribute values should be transformed:
.. testcode:: custom-merge
from newton import Model, ModelBuilder
builder = ModelBuilder()
builder.add_custom_frequency(
ModelBuilder.CustomFrequency(name="pair", namespace="mujoco")
)
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="pair_world",
frequency="mujoco:pair",
dtype=wp.int32,
namespace="mujoco",
references="world", # Replaced with the builder-managed current world during merge
)
)
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="pair_geom1",
frequency="mujoco:pair",
dtype=wp.int32,
namespace="mujoco",
references="shape", # Offset by shape count during merge
)
)
Supported reference types:
* Any built-in entity type (e.g., "body", "shape", "joint", "joint_dof", "joint_coord", "articulation") โ offset by entity count
* "world" โ replaced with the builder-managed current_world for the active merge context
* Custom frequency keys (e.g., "mujoco:pair") โ offset by that frequency's count
Querying Counts
---------------
Use :meth:~newton.Model.get_custom_frequency_count to get the count for a custom frequency (raises KeyError if unknown):
.. testcode:: custom-merge
# Finalize (requires at least one articulation)
_body = builder.add_link()
_joint = builder.add_joint_free(_body)
builder.add_articulation([_joint])
model = builder.finalize()
pair_count = model.get_custom_frequency_count("mujoco:pair")
# Or check directly without raising:
pair_count = model.custom_frequency_counts.get("mujoco:pair", 0)
.. note::
When querying, use the full frequency key with namespace prefix (e.g., "mujoco:pair"). This matches how attribute keys work: model.get_attribute_frequency("mujoco:condim") for a namespaced attribute.
ArticulationView Limitations
----------------------------
Custom frequency attributes are generally not accessible via :class:~newton.selection.ArticulationView because they represent entity types that aren't tied to articulation structure. The one exception is the mujoco:tendon frequency, which is supported. For per-articulation data, use enum frequencies like ARTICULATION, JOINT, or BODY`.
---