Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
43ae9f3
expose newton collision cfg
ooctipus Apr 10, 2026
c9c1a29
Add collision pipeline cfg, bump to 0.5.11
ooctipus Apr 10, 2026
7bab8a0
line break
ooctipus Apr 10, 2026
7bde3cb
Address review feedback on collision pipeline config
AntoineRichard Apr 10, 2026
99b6848
Add SDF/hydroelastic config design spec and implementation plan
AntoineRichard Apr 10, 2026
a106714
Add SDFCfg configclass and missing HydroelasticSDFCfg fields
AntoineRichard Apr 10, 2026
a01f9fd
Wire SDFCfg into NewtonCfg and update exports
AntoineRichard Apr 10, 2026
398f666
Add SDF manager methods for shape preparation
AntoineRichard Apr 10, 2026
07f71ac
Integrate SDF config into manager lifecycle
AntoineRichard Apr 10, 2026
d7e93d1
Skip convex hull for SDF shapes and apply SDF on cloner prototypes
AntoineRichard Apr 10, 2026
13508bb
Add tests for SDF config and shape preparation
AntoineRichard Apr 10, 2026
0d602a6
Add SDF changelog entries and bump to 0.5.12
AntoineRichard Apr 10, 2026
8d24953
Remove design spec and plan docs from PR
AntoineRichard Apr 10, 2026
9d630a5
Fix docstring/code contradiction and remove dead code
AntoineRichard Apr 10, 2026
87af9c5
Address important review findings
AntoineRichard Apr 10, 2026
1d09ba1
Add tests for _create_sdf_collision_from_visual
AntoineRichard Apr 10, 2026
1e08da8
isaaclab_newton: add collision_decimation knob for mid-tick re-collide
mzamoramora-nvidia May 21, 2026
c2df943
isaaclab_newton: tests + changelog for collision_decimation
mzamoramora-nvidia May 21, 2026
d15879c
Vendor PR #5228: Add SDF collision and hydroelastic shape configuration
mzamoramora-nvidia May 26, 2026
a58cd15
Vendor PR #5729: Add NewtonCfg.collision_decimation for mid-tick re-c…
mzamoramora-nvidia May 26, 2026
a028b0e
Newton: fix RigidObjectData shape for kinematic single-body assets
mzamoramora-nvidia May 12, 2026
e0f9755
newton_manager: add sanitize_world_state hook for NaN recovery
mzamoramora-nvidia May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Added
^^^^^

* Added :attr:`~isaaclab_newton.physics.NewtonCfg.collision_decimation` to
re-invoke the Newton collision pipeline every ``N`` solver substeps within
a physics tick. Defaults to ``0`` (legacy: one collide per tick). When set
to ``0 < N < num_substeps``, the substep loop in
:meth:`~isaaclab_newton.physics.NewtonManager._run_solver_substeps` calls
the collision pipeline again at the matching substep boundaries so contact
normals reflect the bodies' just-integrated poses. The last substep is
intentionally skipped — its contact set would only feed the next tick.
:meth:`~isaaclab_newton.physics.NewtonCfg.__post_init__` warns when
``collision_decimation >= num_substeps`` (the gate is silently bypassed).
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Fixed
^^^^^

* Fixed :class:`~isaaclab_newton.assets.RigidObjectData` crashing with
``IndexError: tuple index out of range`` for kinematic-enabled
single-body fixed-base rigid objects. The ``is_fixed_base`` branch
in ``_create_simulation_bindings`` indexed ``[:, 0, 0]`` assuming a
3D ``(count, links, 1)`` layout, but Newton returns a 2D
``(count, links)`` array when the view contains a single body.
Dispatch on actual ``ndim`` instead so both multi-link fixed-base
articulations and single-body kinematic rigid objects are handled
correctly. Also fixes the matching no-velocity fallback in
``_create_buffers``: ``_sim_bind_body_com_vel_w`` is now allocated
as ``(num_instances, 1)`` to match the
``derive_body_acceleration_from_body_com_velocities`` kernel's
2D signature.
Original file line number Diff line number Diff line change
Expand Up @@ -804,18 +804,22 @@ def _create_simulation_bindings(self) -> None:
self._num_bodies = self._root_view.link_count

# -- root properties
if self._root_view.is_fixed_base:
self._sim_bind_root_link_pose_w = self._root_view.get_root_transforms(SimulationManager.get_state_0())[
:, 0, 0
]
else:
self._sim_bind_root_link_pose_w = self._root_view.get_root_transforms(SimulationManager.get_state_0())[:, 0]
# Newton's ``get_root_transforms`` / ``get_root_velocities`` return
# either a 2D ``(count, links_per_env)`` array (single-body
# kinematic-enabled rigid objects, e.g. Factory's bolt) or a 3D
# ``(count, links_per_env, 1)`` array (multi-link fixed-base
# articulations). Dispatch on actual ``ndim`` so we don't crash
# with ``IndexError: tuple index out of range`` when a fixed-base
# rigid object yields the 2D layout.
root_xforms = self._root_view.get_root_transforms(SimulationManager.get_state_0())
self._sim_bind_root_link_pose_w = root_xforms[:, 0, 0] if root_xforms.ndim >= 3 else root_xforms[:, 0]
self._sim_bind_root_com_vel_w = self._root_view.get_root_velocities(SimulationManager.get_state_0())
if self._sim_bind_root_com_vel_w is not None:
if self._root_view.is_fixed_base:
self._sim_bind_root_com_vel_w = self._sim_bind_root_com_vel_w[:, 0, 0]
else:
self._sim_bind_root_com_vel_w = self._sim_bind_root_com_vel_w[:, 0]
self._sim_bind_root_com_vel_w = (
self._sim_bind_root_com_vel_w[:, 0, 0]
if self._sim_bind_root_com_vel_w.ndim >= 3
else self._sim_bind_root_com_vel_w[:, 0]
)
# -- body properties
self._sim_bind_body_com_pos_b = self._root_view.get_attribute("body_com", SimulationManager.get_model())[:, 0]
self._sim_bind_body_link_pose_w = self._root_view.get_link_transforms(SimulationManager.get_state_0())[:, 0]
Expand Down Expand Up @@ -854,11 +858,17 @@ def _create_buffers(self) -> None:
"Failed to get root com velocity. If the rigid object is fixed, this is expected. "
"Setting root com velocity to zeros."
)
# ``_sim_bind_root_com_vel_w`` is consumed as a 1D array by
# ``get_root_link_vel_from_root_com_vel`` (kernel signature has
# ``com_vel: wp.array(dtype=wp.spatial_vectorf)``), so the fallback
# stays 1D. ``_sim_bind_body_com_vel_w`` feeds the 2D-typed
# ``derive_body_acceleration_from_body_com_velocities`` kernel, so
# the fallback is 2D ``(num_instances, 1)`` to match.
self._sim_bind_root_com_vel_w = wp.zeros(
(self._num_instances,), dtype=wp.spatial_vectorf, device=self.device
)
self._sim_bind_body_com_vel_w = wp.zeros(
(self._num_instances,), dtype=wp.spatial_vectorf, device=self.device
(self._num_instances, 1), dtype=wp.spatial_vectorf, device=self.device
)
# -- default root pose and velocity
self._default_root_pose = wp.zeros((self._num_instances,), dtype=wp.transformf, device=self.device)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,32 @@

from __future__ import annotations

import re
from collections.abc import Sequence

import torch
import warp as wp
from newton import ModelBuilder, solvers
from newton import GeoType, ModelBuilder, solvers
from newton._src.usd.schemas import SchemaResolverNewton, SchemaResolverPhysx

from pxr import Usd

from isaaclab.physics import PhysicsManager

from isaaclab_newton.physics import NewtonManager


def _compile_sdf_patterns(patterns: list[str]) -> list[re.Pattern]:
"""Compile regex patterns with validation, raising on invalid regex."""
compiled = []
for i, p in enumerate(patterns):
try:
compiled.append(re.compile(p))
except re.error as e:
raise ValueError(f"Invalid regex in SDFCfg pattern[{i}]: {p!r} — {e}") from e
return compiled


def _build_newton_builder_from_mapping(
stage: Usd.Stage,
sources: Sequence[str],
Expand Down Expand Up @@ -66,8 +80,6 @@ def _build_newton_builder_from_mapping(
# Deformable prim paths are handled by per_world_builder_hooks, not add_usd.
# Resolve the regex prim_path patterns to concrete env_0 paths so add_usd
# can skip them via ignore_paths.
import re

_deformable_ignore_paths: list[str] = []
if hasattr(NewtonManager, "_deformable_registry"):
for entry in NewtonManager._deformable_registry:
Expand All @@ -81,6 +93,16 @@ def _build_newton_builder_from_mapping(
if pat.match(child_path):
_deformable_ignore_paths.append(child_path)

# SDF collision requires original triangle meshes for mesh.build_sdf().
# Convex hull approximation destroys the source geometry, so shapes
# matching SDF patterns must be excluded from approximation here.
# _apply_sdf_config() builds the SDF on each prototype after approximation.
cfg = PhysicsManager._cfg
sdf_cfg = cfg.sdf_cfg if cfg is not None else None # type: ignore[union-attr]
body_pats = _compile_sdf_patterns(sdf_cfg.body_patterns) if sdf_cfg and sdf_cfg.body_patterns else None
shape_pats = _compile_sdf_patterns(sdf_cfg.shape_patterns) if sdf_cfg and sdf_cfg.shape_patterns else None
has_sdf_patterns = body_pats is not None or shape_pats is not None

protos: dict[str, ModelBuilder] = {}
for src_path in sources:
p = NewtonManager.create_builder(up_axis=up_axis)
Expand All @@ -94,7 +116,33 @@ def _build_newton_builder_from_mapping(
ignore_paths=_deformable_ignore_paths if _deformable_ignore_paths else None,
)
if simplify_meshes:
p.approximate_meshes("convex_hull", keep_visual_shapes=True)
if has_sdf_patterns:
sdf_bodies: set[int] = set()
if body_pats is not None:
for bi in range(len(p.body_label)):
if any(pat.search(p.body_label[bi]) for pat in body_pats):
sdf_bodies.add(bi)

approx_indices = []
for i in range(len(p.shape_type)):
if p.shape_type[i] != GeoType.MESH:
continue
# Skip shapes that will use SDF (matched by body or shape pattern)
if p.shape_body[i] in sdf_bodies:
continue
if shape_pats is not None:
lbl = p.shape_label[i] if i < len(p.shape_label) else ""
if any(pat.search(lbl) for pat in shape_pats):
continue
approx_indices.append(i)
if approx_indices:
p.approximate_meshes("convex_hull", shape_indices=approx_indices, keep_visual_shapes=True)
else:
p.approximate_meshes("convex_hull", keep_visual_shapes=True)
# Build SDF on prototype before add_builder copies it N times.
# Mesh objects are shared by reference, so SDF is built once and
# all environments inherit it.
NewtonManager._apply_sdf_config(p)
protos[src_path] = p

# Inject registered sites into prototypes (and global sites into main builder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ __all__ = [
"NewtonShapeCfg",
"NewtonSolverCfg",
"NewtonXPBDManager",
"SDFCfg",
"XPBDSolverCfg",
]

Expand All @@ -26,7 +27,7 @@ from .kamino_manager import NewtonKaminoManager
from .kamino_manager_cfg import KaminoSolverCfg
from .mjwarp_manager import NewtonMJWarpManager
from .mjwarp_manager_cfg import MJWarpSolverCfg
from .newton_collision_cfg import HydroelasticSDFCfg, NewtonCollisionPipelineCfg
from .newton_collision_cfg import HydroelasticSDFCfg, NewtonCollisionPipelineCfg, SDFCfg
from .newton_manager import NewtonManager
from .newton_manager_cfg import (
NewtonCfg,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,38 @@ class HydroelasticSDFCfg:
Defaults to ``False`` (same as Newton's default).
"""

moment_matching: bool = False
"""Whether to adjust reduced contact friction so net max moment matches unreduced.

Only active when ``reduce_contacts`` is True.

Defaults to ``False`` (same as Newton's default).
"""

buffer_mult_broad: int = 1
"""Multiplier for preallocated broadphase buffer.

Defaults to ``1`` (same as Newton's default).
"""

buffer_mult_iso: int = 1
"""Multiplier for iso-surface extraction buffers.

Defaults to ``1`` (same as Newton's default).
"""

buffer_mult_contact: int = 1
"""Multiplier for face contact buffer.

Defaults to ``1`` (same as Newton's default).
"""

grid_size: int = 262144
"""Grid size for hydroelastic contact handling (256 * 8 * 128).

Defaults to ``262144`` (same as Newton's default).
"""


@configclass
class NewtonCollisionPipelineCfg:
Expand Down Expand Up @@ -184,3 +216,119 @@ def to_pipeline_args(self) -> dict[str, Any]:
if hydro_cfg is not None:
cfg_dict["sdf_hydroelastic_config"] = HydroelasticSDF.Config(**hydro_cfg)
return cfg_dict


@configclass
class SDFCfg:
"""Configuration for SDF mesh collision shapes.

Specifies how SDF (Signed Distance Field) voxel grids are built and assigned
to bodies or shapes in a Newton model. Bodies and shapes are selected by
regex patterns; the SDF resolution can be set globally or overridden
per-pattern.

Optional hydroelastic stiffness can be assigned to matched SDF shapes.
Pipeline-level hydroelastic parameters (contact reduction, buffer sizes,
etc.) are configured separately via
:attr:`NewtonCollisionPipelineCfg.sdf_hydroelastic_config`.

Note:
At least one of :attr:`body_patterns` or :attr:`shape_patterns` must be
set. At least one of :attr:`max_resolution` or
:attr:`target_voxel_size` must be set.
"""

max_resolution: int | None = None
"""Maximum voxel dimension for the SDF grid.

Must be divisible by 8. Typical values: 128, 256, 512. When both this
and :attr:`target_voxel_size` are set, both are forwarded to Newton's
``mesh.build_sdf()``; Newton uses :attr:`target_voxel_size` to derive
the actual resolution.

Defaults to ``None``.
"""

target_voxel_size: float | None = None
"""Target voxel size [m] for the SDF grid.

When both this and :attr:`max_resolution` are set, both are forwarded to
Newton and this value is used to derive the actual resolution.

Defaults to ``None``.
"""

narrow_band_range: tuple[float, float] = (-0.1, 0.1)
"""Narrow band distance range (inner, outer) [m].

Defines the signed-distance extent stored in the SDF voxel grid.
Negative values are inside the mesh, positive values outside.

Defaults to ``(-0.1, 0.1)``.
"""

margin: float | None = None
"""Collision margin [m] for SDF shapes.

When ``None``, the Newton builder default is used.

Defaults to ``None``.
"""

body_patterns: list[str] | None = None
"""Regex patterns for body labels.

Matched bodies receive SDF collision shapes on all their mesh geometries.
At least one of :attr:`body_patterns` or :attr:`shape_patterns` must be set.

Defaults to ``None``.
"""

shape_patterns: list[str] | None = None
"""Regex patterns for shape labels.

Matched shapes receive SDF collision geometry directly.
At least one of :attr:`body_patterns` or :attr:`shape_patterns` must be set.

Defaults to ``None``.
"""

pattern_resolutions: dict[str, int] | None = None
"""Per-pattern SDF resolution overrides.

Maps a regex string to a ``max_resolution`` value. Patterns are evaluated
in insertion order; the first match wins. Unmatched bodies or shapes fall
back to the global :attr:`max_resolution` or :attr:`target_voxel_size`.

Defaults to ``None``.
"""

use_visual_meshes: bool = False
"""Whether to create collision shapes from visual meshes.

When ``True``, matched bodies that lack explicit collision geometry have SDF
collision shapes built from their visual meshes instead.

Defaults to ``False``.
"""

k_hydro: float | None = None
"""Hydroelastic stiffness [Pa] assigned to matched SDF shapes.

When ``None``, no ``HYDROELASTIC`` flag is set and hydroelastic contacts are
disabled for these shapes. When set, matched shapes receive the flag with
this stiffness value.

Defaults to ``None``.
"""

hydroelastic_shape_patterns: list[str] | None = None
"""Regex patterns restricting which SDF shapes receive hydroelastic stiffness.

Only relevant when :attr:`k_hydro` is set. When ``None``, all SDF shapes
matched by :attr:`body_patterns` or :attr:`shape_patterns` get the
hydroelastic flag. When set, only shapes whose labels match at least one
pattern here receive it.

Defaults to ``None``.
"""
Loading