### 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 exposing
``joint_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 an
integral accumulator, or :class:`ControllerNeuralLSTM` with hidden/cell state) and
actuators with a :class:`Delay` require explicit double-buffered state
management.  Create two state objects with :meth:`Actuator.state` 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`,
:class:`ControllerNeuralLSTM`) 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 fully
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:`Actuator.is_graphable` 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).
* :class:`ControllerPID` — proportional-integral-derivative control law
  (stateful: integral accumulator with anti-windup clamp).
* :class:`ControllerNeuralMLP` — MLP neural-network controller
  (stateful: position/velocity history buffers).
* :class:`ControllerNeuralLSTM` — 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.
* :class:`ClampingDCMotor` — velocity-dependent effort saturation using the DC
  motor effort-speed characteristic.
* :class:`ClampingPositionBased` — 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`,
:meth:`~Controller.is_stateful`, 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
:meth:`~newton.ModelBuilder.add_actuator` or USD schemas) to constructor
parameters, filling in defaults where needed.

Similarly, a custom clamping stage subclasses :class:`Clamping` and implements
:meth:`~Clamping.modify_forces` (which reads effort from a source buffer and writes bounded effort to a destination buffer).

See Also
--------

* :mod:`newton.actuators` — full API reference
* :meth:`newton.ModelBuilder.add_actuator` — 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`,
and its velocity by 6 parameters (3D linear and 3D angular) in :attr:`newton.State.body_qd`.
The linear component of :attr:`newton.State.body_qd` is the world-frame velocity
of the body's center of mass. For public ``FREE`` and ``DISTANCE`` joints,
:attr:`newton.State.joint_qd` 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`
use generalized coordinates, while :class:`~newton.solvers.SolverXPBD`,
:class:`~newton.solvers.SolverSemiImplicit`, and :class:`~newton.solvers.SolverVBD`
use maximal coordinates.
Note that collision detection via :meth:`newton.CollisionPipeline.collide` 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
it is not a conventional generalized-coordinate joint. Its four entries are
VBD constraint/material slots defined by
:class:`~newton.solvers.SolverVBD.JointSlot`: stretch (``STRETCH``, slot 0),
shear (``SHEAR``, slot 1), bend (``BEND``, slot 2), and
twist (``TWIST``, slot 3). These slots store independent per-cable stiffness
and damping through
:attr:`newton.Model.joint_target_ke` 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 by
:class:`newton.solvers.SolverVBD`. 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`
or :meth:`~newton.ModelBuilder.add_body`. Only root links (joint parent ``-1``) may be kinematic.
Setting a non-root link to kinematic raises a :class:`ValueError` 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)
     - 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:`Mass and Inertia`)
     - 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`,
  then call :func:`newton.eval_fk` so maximal coordinates (for collisions and body-space consumers) are current.
- For maximal-coordinate workflows, write :attr:`newton.State.body_q` and :attr:`newton.State.body_qd` directly.

Rigid-body solver behavior
^^^^^^^^^^^^^^^^^^^^^^^^^^

The rigid-body solvers (:class:`~newton.solvers.SolverMuJoCo`,
:class:`~newton.solvers.SolverFeatherstone`, :class:`~newton.solvers.SolverXPBD`,
:class:`~newton.solvers.SolverSemiImplicit`, :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`,
  :class:`~newton.solvers.SolverFeatherstone`) treat kinematic motion through prescribed joint state.
- Maximal-coordinate solvers (:class:`~newton.solvers.SolverXPBD`,
  :class:`~newton.solvers.SolverSemiImplicit`, :class:`~newton.solvers.SolverVBD`)
  use prescribed body transforms/twists.
- Contact handling of kinematic bodies is not identical across the solvers. :class:`~newton.solvers.SolverXPBD`,
  :class:`~newton.solvers.SolverVBD`, :class:`~newton.solvers.SolverMuJoCo`, and
  :class:`~newton.solvers.SolverFeatherstone` treat kinematic bodies like
  infinite-mass colliders for contact response, while
  :class:`~newton.solvers.SolverSemiImplicit` 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 a
large internal armature value; see :ref:`Kinematic Links and Fixed Roots <mujoco-kinematic-links-and-fixed-roots>` 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.FREE``
     - Free (floating) joint with 6 degrees of freedom in velocity space
     - 7 (3D position + 4D quaternion)
     - 6 (see :ref:`Twist conventions in Newton <Twist conventions>`)
   * - ``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, not
generalized coordinates or velocity DOFs; see `Cable joints`_.

Definition of ``joint_q``
^^^^^^^^^^^^^^^^^^^^^^^^^

The :attr:`newton.Model.joint_q` array stores the default generalized joint positions
for generalized-coordinate joints and is used to initialize :attr:`newton.State.joint_q`.
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 velocities
for generalized-coordinate joints and is used to initialize :attr:`newton.State.joint_qd`.
The generalized joint forces at :attr:`newton.Control.joint_f` 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`.
This includes :attr:`newton.Model.joint_axis`, joint limits and other per-DOF
properties defined via :class:`newton.ModelBuilder.JointDofConfig`, and the
velocity targets at :attr:`newton.Control.joint_target_qd`.

The position targets at :attr:`newton.Control.joint_target_q` instead match
:attr:`newton.Model.joint_q` (coord layout) when
:attr:`newton.use_coord_layout_targets` is ``True``; index those with
:attr:`newton.Model.joint_q_start`. Under the legacy default
(``use_coord_layout_targets = False``) the array is still DOF-shaped and
indexed via :attr:`newton.Model.joint_qd_start` — see the
:ref:`migration guide <joint-target-layout>` 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-attached
``REVOLUTE`` root), ``set_root_transforms()`` moves the articulation by writing
``Model.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`)
   * - x_wc
     - World transform of the child body (stored at :attr:`State.body_q`)
   * - x_pj
     - Transform from the parent body to the joint parent anchor frame (defined by :attr:`Model.joint_X_p`)
   * - x_cj
     - Transform from the child body to the joint child anchor frame (defined by :attr:`Model.joint_X_c`)
   * - 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`).
Given the parent body's world transform :math:`x_{wp}` 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 that
COM/world convention, and :func:`newton.eval_ik` expects the same convention
when recovering generalized state from maximal body state. For ``FREE`` and
``DISTANCE`` 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:`q`
     - Generalized joint coordinates (:attr:`State.joint_q`).
   * - :math:`\dot{q}`
     - Generalized joint velocities (:attr:`State.joint_qd`).
   * - :math:`\ddot{q}`
     - Generalized joint accelerations (user-supplied ``joint_qdd``).
   * - :math:`\tau`
     - Generalized joint forces / torques, same layout as :attr:`Control.joint_f`.
   * - :math:`M(q)`
     - Joint-space mass matrix, shape ``(articulation_count, max_dofs_per_articulation, max_dofs_per_articulation)``.
   * - :math:`g(q) = \partial U / \partial q`
     - Gravity force, where :math:`U(q) = \sum_i -m_i\, \mathbf{g} \cdot \mathbf{x}_{\text{com},i}` 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.
   * - :math:`C(q, \dot{q})\, \dot{q}`
     - Coriolis + centrifugal force.

:func:`newton.eval_inverse_dynamics_passive` populates any requested
combination of :math:`M(q)`, :math:`g(q)`, and
:math:`C(q, \dot{q})\, \dot{q}` into caller-allocated arrays. An output set to
``None`` is not computed.
:func:`newton.eval_inverse_dynamics_force` then combines them with a
user-supplied :math:`\ddot{q}` to produce :math:`\tau`.

Both functions require ``state.body_q`` to be consistent with
``state.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 = M*joint_qdd + C*qdot + 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, supplying
``gravity_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
:meth:`~newton.selection.ArticulationView.eval_inverse_dynamics_passive`, which
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:`newton.eval_mass_matrix` 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 the
``joint_list`` passed to** :meth:`~newton.ModelBuilder.add_articulation`,
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:`orphan joint <Orphan joints>` (whose child has no articulated path
and which :meth:`~newton.ModelBuilder.finalize` rejects unless
``skip_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
leave it out of the surrounding :meth:`~newton.ModelBuilder.add_articulation`
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
     enforce joints as pairwise body constraints, so the loop-closure joint is
     solved alongside the tree joints with no special-casing. Under
     :class:`~newton.solvers.SolverXPBD` and
     :class:`~newton.solvers.SolverSemiImplicit`, ``j_loop`` keeps its full
     joint behavior — drive (``joint_target_ke``/``joint_target_kd``,
     ``control.joint_f``) and joint limits are applied alongside the
     loop-closure constraint, subject to each solver's general joint-feature
     support (see :ref:`Joint feature support`).
     :class:`~newton.solvers.SolverVBD` and
     :class:`~newton.solvers.SolverKamino` 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
     bilateral coupling at compile time, which restricts the supported
     joint types and drops joint-level features (see the note below).
     :class:`~newton.solvers.SolverFeatherstone` 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`,
   :func:`newton.eval_ik`, and :class:`~newton.selection.ArticulationView` —
   those walk the articulation tree only.

.. note::

   :class:`~newton.solvers.SolverMuJoCo` supports only a subset of joint
   types as loop closures, and the loop-closing joint loses its joint-level
   features (drive, limits, armature, friction). See
   :ref:`mujoco-loop-closures` 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`).
- **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:`~solvers.SolverMuJoCo` for advanced contact models (see
  :ref:`MuJoCo Warp Integration`).

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`).
   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:`Mesh Collisions` 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`` once
   per frame instead of every substep can significantly improve performance. See
   :ref:`Common Patterns` 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.py`
- **SDF mesh contacts** —
  :github:`newton/examples/contacts/example_nut_bolt_sdf.py`
- **Robot manipulation with SDF** —
  :github:`newton/examples/contacts/example_brick_stacking.py`

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 shared
``rigid_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 can
       alternate between the cylinder ends. This is a known discrepancy tracked
       in `mujoco_warp#1555
       <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>` and
``example_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 instead, because the precomputed :attr:`~Model.shape_contact_pairs` 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. Use
``narrow_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
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:`~Mesh.edges`,
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. Set
``edge_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`` and
``edge_box_half_lateral``/``edge_box_half_lateral_rel`` tune the box extents (absolute
metres or fractions of the mesh AABB diagonal); see :meth:`~Mesh.build_sdf` 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_hydroelastic``
     - Whether the shape uses SDF-based hydroelastic contacts. Both shapes in a pair must have this enabled. See :ref:`Hydroelastic Contacts`. 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 planned
:meth:`CollisionPipeline.collide` 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
do not control collision detection performed inside a solver. For example,
:class:`~solvers.SolverMuJoCo` generates contacts internally when
``use_mujoco_contacts=True`` (see :ref:`mujoco-collision-pipeline`), while
:class:`~solvers.SolverVBD` 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 to
``sim_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
and is consumed by the solver :meth:`~solvers.SolverBase.step` 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"``.
       See :ref:`Contact Matching`.
   * - ``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``.
       See :ref:`Contact Reports`.
   * - ``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``.
       See :ref:`Contact Reports`.

**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`
     - Contact spatial forces (used by :class:`~sensors.SensorContact`).
       Populated by :meth:`~solvers.SolverBase.update_contacts`.

.. note::

   :class:`~solvers.SolverXPBD` with ``rigid_contact_con_weighting`` enabled
   (the default) does not conserve momentum at contacts.  The per-contact
   forces written by :meth:`~solvers.SolverXPBD.update_contacts` 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 in
:class:`wp.Tape` 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
world-space normal is already available as
:attr:`~newton.Contacts.rigid_contact_normal`; 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 the
deprecation window.
Allocate only the outputs you need and call
:func:`newton.eval_rigid_contact_kinematics` explicitly to prepare
for their removal. The deprecated ``rigid_contact_diff_normal`` attribute is
already an alias for
:attr:`~newton.Contacts.rigid_contact_normal` and does not allocate a duplicate
array.

.. _Creating Contacts:

Creating and Populating Contacts
--------------------------------

Create a :class:`~CollisionPipeline` explicitly, then allocate and populate a
:class:`~Contacts` 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. Supply
``pressure_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 as
``model.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
:class:`~geometry.HydroelasticSDF.Config`. 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-specific
``buffer_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``
     - Coefficient of friction
     - 1.0
     - :attr:`~ModelBuilder.ShapeConfig.mu`
     - :attr:`~Model.shape_material_mu`
   * - ``ke``
     - Normal contact stiffness
     - 2.5e3
     - :attr:`~ModelBuilder.ShapeConfig.ke`
     - :attr:`~Model.shape_material_ke`
   * - ``kd``
     - Normal contact damping
     - 100.0
     - :attr:`~ModelBuilder.ShapeConfig.kd`
     - :attr:`~Model.shape_material_kd`
   * - ``kf``
     - Contact friction gain
     - 1000.0
     - :attr:`~ModelBuilder.ShapeConfig.kf`
     - :attr:`~Model.shape_material_kf`
   * - ``ka``
     - Adhesion distance
     - 0.0
     - :attr:`~ModelBuilder.ShapeConfig.ka`
     - :attr:`~Model.shape_material_ka`
   * - ``restitution``
     - Bounciness
     - 0.0
     - :attr:`~ModelBuilder.ShapeConfig.restitution`
     - :attr:`~Model.shape_material_restitution`
   * - ``mu_torsional``
     - Resistance to spinning at contact
     - 0.005
     - :attr:`~ModelBuilder.ShapeConfig.mu_torsional`
     - :attr:`~Model.shape_material_mu_torsional`
   * - ``mu_rolling``
     - Resistance to rolling motion
     - 0.0001
     - :attr:`~ModelBuilder.ShapeConfig.mu_rolling`
     - :attr:`~Model.shape_material_mu_rolling`
   * - ``kh``
     - Hydroelastic stiffness coefficient
     - 1.0e10
     - :attr:`~ModelBuilder.ShapeConfig.kh`
     - :attr:`~Model.shape_material_kh`

.. 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. The
``newton:contactMargin`` and ``newton:contactGap`` attributes map to
:attr:`~ModelBuilder.ShapeConfig.margin` 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
attributes. The contact response attributes map to :class:`~ModelBuilder.ShapeConfig`
fields as follows: ``newton:contactStiffness`` → ``ke``,
``newton:contactDamping`` → ``kd``, ``newton:contactFrictionGain`` → ``kf``,
``newton:contactAdhesion`` → ``ka``. A value of ``-inf`` means "use the engine's
default" (see :ref:`Contact Material Properties`).

.. 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``
to :class:`~newton.CollisionPipeline` (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``
argument on :class:`~CollisionPipeline` selects one of three modes:

- ``"disabled"`` (default) — no matching, no extra buffers.
- ``"latest"`` — match current contacts against the previous
  frame and populate :attr:`Contacts.rigid_contact_match_index`, but keep the
  current frame's freshly generated contact geometry in the returned
  :class:`Contacts` 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 midpoint**
``0.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
- Use :meth:`~ModelBuilder.approximate_meshes` 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 with
``wp.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`,
:class:`~solvers.SolverSemiImplicit`, :class:`~solvers.SolverFeatherstone`,
:class:`~solvers.SolverMuJoCo`). Pass the :class:`~Contacts`
object to :meth:`~solvers.SolverBase.step`:

.. 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
:class:`~Contacts` 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``.
   * - :class:`~geometry.BroadPhaseSAP`
     - Sweep-and-prune. Same interface, with optional ``sweep_thread_count_multiplier``
       and ``sort_type`` tuning parameters.
   * - :class:`~geometry.BroadPhaseExplicit`
     - 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 call
``launch_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
- :meth:`~CollisionPipeline.collide` - Run collision detection
- :class:`~CollisionPipeline` - Collision pipeline with configurable broad phase
- ``broad_phase`` - Broad phase algorithm: ``"nxn"``, ``"sap"``, or ``"explicit"``
- :class:`~Contacts` - Contact data container
- :class:`~GeoType` - Shape geometry types
- :class:`~ModelBuilder.ShapeConfig` - Shape configuration options
- :meth:`~ModelBuilder.ShapeConfig.configure_sdf` - Set SDF and hydroelastic options in one call
- :class:`~geometry.HydroelasticSDF.Config` - Hydroelastic contact configuration
- :meth:`~Mesh.build_sdf` - Precompute SDF for a mesh
- :meth:`~ModelBuilder.approximate_meshes` - Replace mesh collision shapes with simpler geometry
- :meth:`~ModelBuilder.replicate` - Stamp out multi-world copies of a template builder
- :class:`~geometry.BroadPhaseAllPairs`, :class:`~geometry.BroadPhaseSAP`, :class:`~geometry.BroadPhaseExplicit` - Broad phase implementations
- :class:`~geometry.NarrowPhase` - Narrow phase contact generation

**Model attributes:**

- :attr:`~Model.shape_collision_group` - Per-shape collision groups
- :attr:`~Model.shape_world` - Per-shape world indices
- :attr:`~Model.shape_gap` - Per-shape contact gaps (detection threshold)
- :attr:`~Model.shape_margin` - Per-shape margin values (signed distance padding)

**Related documentation:**

- :ref:`Contact material support` - Material property behavior by solver
- :doc:`custom_attributes` - USD custom attributes for collision properties
- :doc:`usd_parsing` - USD import options including collision settings
- :doc:`sites` - 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}`.
- **Point-dependent components**: angular torque (moment) :math:`\boldsymbol{\tau}`, 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`):
  uses the body's *body frame* (often at the body's center of mass).
  Here :math:`\omega_b` is the angular velocity expressed in the body frame,
  and :math:`v_b` is the linear velocity of a point at the body origin
  (e.g. the COM) expressed in the body frame.  
  Thus :math:`V_b = (\omega_b,\;v_b)` gives the body's own-frame view of its
  motion.

* **Spatial twist** (:math:`V_s`):
  uses the fixed *space frame* (world/inertial frame).
  :math:`v_s` represents the linear velocity of a hypothetical point on the
  moving body that is instantaneously at the world origin, and
  :math:`\omega_s` 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.
  Hence :math:`V_s = (v_s,\;\omega_s)` is called the **spatial twist**.
  *Note:* :math:`v_s` is **not** simply the COM velocity
  (that would be :math:`\dot p`); 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 *B*
  measured in frame *M*, expressed in frame *E*."  In normal use
  :math:`V_{WB}^{W}` (body *B* in world *W*, expressed in *W*) contains
  :math:`(\omega_{WB}^{W},\;v_{WB_o}^{W})`, 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
  angular part :math:`(\omega_x,\omega_y,\omega_z)` 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,
  the linear velocity is *not* the CoM velocity—see :ref:`MuJoCo conversion <MuJoCo conversion>`
  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 arrays
such as :attr:`newton.State.body_qd` and :attr:`newton.State.body_f`.
Newton's :attr:`State.body_qd <newton.State.body_qd>` 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`)
   * - *Modern Robotics* — **Spatial twist**
     - World origin, world frame
     - World frame
     - "Spatial twist" (:math:`V_s`)
   * - **Drake**
     - Body-frame origin :math:`B_o` (not necessarily COM), world frame
     - World frame
     - Spatial velocity :math:`V_{WB}^{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:`[\mathbf{v}_{com}^W;\ \boldsymbol{\omega}^W]`
   * - **Newton**
     - COM, world frame
     - World frame
     - :attr:`~newton.State.body_qd`

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`;
the inverse uses :math:`R^{\mathsf T}` 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}`,
   :math:`v_b = R^{\mathsf T}v_{\text{com}}^{W}`.

3. Shift to another origin offset :math:`r` from COM:  

   :math:`v_{\text{origin}}^{W} = v_{\text{com}}^{W} + \omega^{W}\times r^{W}`,
   where :math:`r^{W}=R\,r`.

.. _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}`
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:`\mathbf{r}` 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
  COM-wrench semantics for that root-free-joint case, :class:`~newton.solvers.SolverMuJoCo` applies free-joint
  :attr:`Control.joint_f <newton.Control.joint_f>` 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 with
  :attr:`State.body_f <newton.State.body_f>`.

  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 
(real) part and :math:`(x, y, z)` 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` 
  or :math:`q = (w, x, y, z)`, 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`,
:attr:`newton.Mesh.color`, and the ``color`` arguments on
:class:`newton.ModelBuilder` 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` and
:func:`newton.utils.color_linear_to_srgb`.

.. 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. Set
``SensorTiledCamera.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 overlay
  on a shared :class:`Model`.
- :class:`newton.solvers.experimental.coupled.CouplingInterface`: the hook
  protocol implemented by solvers that need custom coupled behavior.
- :class:`newton.solvers.experimental.coupled.SolverCoupled`: the shared base
  for partitioning models, distributing state, stepping entries, and
  reconciling results.
- :class:`newton.solvers.experimental.coupled.SolverCoupledProxy`: a lagged or
  staggered proxy coupling wrapper.
- :class:`newton.solvers.experimental.coupled.SolverCoupledADMM`: 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
a :class:`~newton.solvers.experimental.coupled.ModelView` 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 shared
``state_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 or
can report interface forces more accurately than a generic momentum fallback.
Those solvers implement
:class:`newton.solvers.experimental.coupled.CouplingInterface` hooks. The
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:`NotImplementedError` 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 ``flags``
  argument uses :class:`newton.StateFlags`. 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 public
``state.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 usual
``notify_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 a
``collision_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 helper
``SolverCoupledADMM.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 to
``SolverCoupledADMM.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 a
``ContactPair`` 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.
- 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:`~newton.solvers.SolverVBD` 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
* **State Object** (:class:`~newton.State`) - Dynamic quantities that evolve during simulation
* **Control Object** (:class:`~newton.Control`) - Control inputs and actuator commands
* **Contact Object** (:class:`~newton.Contacts`) - 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``)
* :attr:`~newton.ModelBuilder.CustomAttribute.mjcf_value_transformer` — callable that converts the XML string value to the target dtype
* :attr:`~newton.ModelBuilder.CustomAttribute.urdf_attribute_name` — name of the XML attribute to read (defaults to the attribute ``name``)
* :attr:`~newton.ModelBuilder.CustomAttribute.urdf_value_transformer` — 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.
* :attr:`~newton.ModelBuilder.CustomFrequency.usd_entry_expander` (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()``).
4. For each prim, evaluate matching frequencies in registration order:
   
   - If :attr:`~newton.ModelBuilder.CustomFrequency.usd_entry_expander` 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
     prim in that frequency, the expander output is the only source of row values.
   - In that expander code path, the normal :class:`~newton.ModelBuilder.CustomAttribute` USD parsing path is skipped
     for that frequency/prim. In other words,
     :attr:`~newton.ModelBuilder.CustomAttribute.usd_attribute_name` and
     :attr:`~newton.ModelBuilder.CustomAttribute.usd_value_transformer` 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.
* When called from :meth:`~newton.ModelBuilder.add_usd` 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``.

---