Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions docs/source/how-to/solver_tuning_vbd.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,28 @@ Core Solve
- Description
* - ``iterations``
- Default: ``10``. Number of VBD iterations per substep. Increasing this value improves deformation and contact convergence, especially for stiff materials or rigid gripper contacts, but increases runtime.
* - ``friction_epsilon``
- Default: ``1.0e-2`` [m/s]. Relative tangential-speed threshold for smoothing friction near rest. Reducing it retains Coulomb-like friction at lower sliding speeds.
* - ``rigid_compliant_alm``
- Default: ``None``. Preserves Newton's rigid solver mode. In Newton 1.6, ``None`` selects deprecated legacy AVBD. Set to ``True`` to use compliant ALM for rigid joints and body-body contacts, or ``False`` to explicitly retain legacy AVBD.
* - ``rigid_joint_linear_ke``
- Default: ``1.0e5`` [N/m]. Structural linear stiffness for non-rod rigid joints, separate from actuator gains.
* - ``rigid_joint_angular_ke``
- Default: ``1.0e5`` [N m/rad]. Structural angular stiffness for non-rod rigid joints, separate from actuator gains.
* - ``update_joint_state``
- Default: ``True``. Derives standalone VBD joint positions and velocities each tick. Disable only for body-state-only consumers. External rigid integration and coupled solvers retain their generalized joint-state ownership and skip this update.
* - ``rigid_body_contact_buffer_size``
- Default: ``64``. Per-body capacity for body-body contacts when VBD integrates rigid bodies. Increase it if Newton reports a per-body body-body contact buffer overflow.
* - ``rigid_body_particle_contact_buffer_size``
- Default: ``256``. Per-body capacity for particle, edge, and face soft contacts. Increase it if Newton reports a per-body contact buffer overflow.
* - ``integrate_with_external_rigid_solver``
- Default: ``False``. Set to ``True`` only when a manual manager integrates rigid bodies in the shared model. Proxy-coupled entries use partitioned model views and leave this ``False``.

Structural joint stiffness keeps rigid links attached; actuator stiffness drives
the allowed joint coordinates toward their targets. Tune these separately under
the intended contact loads. Higher structural stiffness can require more solver
iterations for drive and contact convergence. Set it before creating the
simulation because Newton captures these material values during solver construction.

Rigid Cables
^^^^^^^^^^^^
Expand Down Expand Up @@ -221,6 +234,19 @@ Self-Contact
- Default: ``0.0`` [m]. Filters self-contact candidates whose rest-configuration distance is shorter than this distance. Increase it when rest-neighbor contacts produce unwanted resistance.


Contact Scheduling
------------------

Contact scheduling belongs to :class:`~isaaclab_newton.physics.NewtonCfg`,
not :class:`~isaaclab_newton.physics.VBDSolverCfg`.
:attr:`~isaaclab_newton.physics.NewtonCfg.collision_decimation` controls how
often the collision pipeline refreshes contacts within a physics tick. ``0``
runs collision once per tick. A positive value ``k`` re-runs it every ``k``
solver substeps. Use ``1`` for fast or thin rigid-soft interactions that can
cross the contact band within one substep; increase it only after verifying that
the cheaper, staler contact set does not cause tunneling or ejection.


Custom MJWarp + VBD Parameters
------------------------------

Expand Down
Empty file.
2 changes: 1 addition & 1 deletion source/isaaclab_contrib/test/coupling/test_coupler.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ def test_mpm_entry_reuses_builder_lifecycle_hooks(monkeypatch, vbd_entries):
"""Prepare the shared builder once per selected solver, without unconditional VBD coloring."""
events: list[tuple[str, object]] = []
builder = ModelBuilder()
monkeypatch.setattr(builder, "color", lambda *, balance_colors: events.append(("color", balance_colors)))
monkeypatch.setattr(builder, "color", lambda *, balance_colors, **_: events.append(("color", balance_colors)))
entries = [CouplerEntryCfg(name="media", solver_cfg=MPMSolverCfg())]
entries.extend(CouplerEntryCfg(name=f"cloth_{index}", solver_cfg=VBDSolverCfg()) for index in range(vbd_entries))
solver_cfg = CouplerProxyCfg(entries=entries)
Expand Down
28 changes: 24 additions & 4 deletions source/isaaclab_contrib/test/coupling/test_coupler_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
import numpy as np
import pytest
import warp as wp
from isaaclab_newton.physics import NewtonManager, XPBDSolverCfg
from isaaclab_newton.physics import NewtonManager, NewtonVBDManager, XPBDSolverCfg
from newton import CollisionPipeline, Mesh, Model, ModelBuilder
from newton.solvers import SolverXPBD
from newton.solvers.experimental.coupled import SolverCoupledADMM, SolverCoupledProxy

from isaaclab.physics import PhysicsManager

from isaaclab_contrib.coupling import (
CouplerAdmmCfg,
CouplerEntryCfg,
Expand All @@ -32,7 +34,7 @@
def isolated_newton_manager(monkeypatch: pytest.MonkeyPatch):
"""Isolate every global manager slot touched by coupler construction."""
clean_values = {
"backend": SimpleNamespace(model=None),
"backend": SimpleNamespace(model=None, state_0=None, state_1=None, control=None),
"_solver": None,
"_use_single_state": None,
"_contacts": None,
Expand All @@ -42,9 +44,15 @@ def isolated_newton_manager(monkeypatch: pytest.MonkeyPatch):
"_supports_contact_sensors": True,
"_supports_rigid_body_force_input": False,
"_report_contacts": False,
"_solver_dt": 1.0 / 60.0,
"_num_substeps": 1,
"_collision_decimation": 0,
"_state_force_callbacks": [],
}
for name, value in clean_values.items():
monkeypatch.setattr(NewtonManager, name, value)
monkeypatch.setattr(NewtonVBDManager, "_update_joint_state", True)
monkeypatch.setattr(PhysicsManager, "_cfg", None)
yield


Expand Down Expand Up @@ -131,7 +139,7 @@ def test_real_coupler_constructs_resets_and_steps(
expected_solver_type: type,
isolated_newton_manager,
):
"""Exercise supported coupling configurations or reject unsupported capacity overrides."""
"""Step supported coupling configurations without overwriting nested-solver joint state."""
model = _build_overlapping_body_model(mesh_contact=algorithm == "admm_capacity")
entries = _entry_configs()
if algorithm == "proxy":
Expand Down Expand Up @@ -192,18 +200,30 @@ def test_real_coupler_constructs_resets_and_steps(
state_1 = model.state()
solver.reset(state_0)
assert solver.entry_output_state_valid() is False
state_0.joint_qd.fill_(0.125)

collision_pipeline.collide(state_0, contacts)
assert int(contacts.rigid_contact_count.numpy()[0]) >= 1
body_q_before = state_0.body_q.numpy().copy()

solver.step(state_0, state_1, model.control(), contacts, 1.0 / 60.0)
# The native coupled solver, not global inverse kinematics, owns the published joint state.
native_output = model.state()
control = model.control()
solver.step(state_0, native_output, control, contacts, 1.0 / 60.0)
solver.reset(state_0, flags=0)
NewtonManager.backend.state_0 = state_0
NewtonManager.backend.state_1 = state_1
NewtonManager.backend.control = control
NewtonCouplerManager._run_solver_substeps(contacts)
state_1 = NewtonManager.backend.state_0

body_q_after = state_1.body_q.numpy()
assert solver.entry_output_state_valid() is True
assert np.all(np.isfinite(body_q_after))
assert np.all(np.isfinite(state_1.body_qd.numpy()))
assert np.any(body_q_after[:, 2] < body_q_before[:, 2])
np.testing.assert_array_equal(state_1.joint_q.numpy(), native_output.joint_q.numpy())
np.testing.assert_array_equal(state_1.joint_qd.numpy(), native_output.joint_qd.numpy())
if algorithm == "admm_capacity":
assert int(solver._admm_internal_contacts.rigid_contact_count.numpy()[0]) >= 1
assert solver._admm_internal_contacts.rigid_contact_match_index is not None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Added VBD structural joint stiffness and friction-smoothing settings, and optional standalone generalized joint-state synchronization.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Fixed VBD graph coloring for active cloth bending constraints and stale joint state after rigid-body integration.
Coupled and external rigid solvers retained ownership of their generalized joint state.
* Fixed Newton IK prototype selection for nested articulation roots.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,9 @@ def __init__(self, cfg: NewtonInverseKinematicsActionCfg, env: ManagerBasedEnv):
sources = cloner.path.get_asset_prototype_paths(plan)
self._source_path = next(sources[index] for index in asset_ids if sources[index] is not None)
prototype_model = NewtonManager._cl_protos[self._source_path].finalize(device=NewtonManager.get_model().device)
articulation_path = self._source_path + (self._asset.cfg.articulation_root_prim_path or "")
Comment thread
maxkra15 marked this conversation as resolved.
prototype_view = ArticulationView(
prototype_model, self._source_path, verbose=False, exclude_joint_types=[JointType.FREE, JointType.FIXED]
prototype_model, articulation_path, verbose=False, exclude_joint_types=[JointType.FREE, JointType.FIXED]
)
coord_ids = self._resolve_prototype_joint_coord_ids(prototype_view, self._asset.joint_names)
controlled_ids = self._resolve_prototype_joint_coord_ids(prototype_view, self._joint_names)
Expand Down
22 changes: 20 additions & 2 deletions source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

from __future__ import annotations

from newton import Model, ModelBuilder
from typing import ClassVar

from newton import Model, ModelBuilder, eval_ik
from newton.solvers import SolverVBD

from .newton_manager import NewtonManager
Expand All @@ -17,10 +19,12 @@
class NewtonVBDManager(NewtonManager):
"""Newton manager specialization for the VBD solver."""

_update_joint_state: ClassVar[bool] = True

@classmethod
def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None:
"""Color the completed builder before allocating the model."""
builder.color(balance_colors=False)
builder.color(include_bending=True, balance_colors=False)

@classmethod
def _create_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> SolverVBD:
Expand All @@ -34,6 +38,20 @@ def _build_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> None:
NewtonManager._use_single_state = False
NewtonManager._needs_collision_pipeline = True
NewtonManager._supports_rigid_body_force_input = not solver_cfg.integrate_with_external_rigid_solver
cls._update_joint_state = solver_cfg.update_joint_state and not solver_cfg.integrate_with_external_rigid_solver

@classmethod
def _run_solver_substeps(cls, contacts) -> None:
"""Publish standalone VBD joint state without changing coupled-solver ownership."""
super()._run_solver_substeps(contacts)
if (
isinstance(cls._solver, SolverVBD)
and cls._update_joint_state
and cls.backend.model.articulation_count > 0
and cls.backend.model.joint_coord_count > 0
):
state = cls.backend.state_0
eval_ik(cls.backend.model, state, state.joint_q, state.joint_qd)
Comment thread
maxkra15 marked this conversation as resolved.

@classmethod
def _simulate_physics_only(cls) -> None:
Expand Down
16 changes: 16 additions & 0 deletions source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,19 @@ class VBDSolverCfg(NewtonSolverCfg):
iterations: int = 10
"""Number of VBD iterations per substep."""

friction_epsilon: float = 1.0e-2
"""Relative tangential speed used to smooth friction near rest [m/s]."""

integrate_with_external_rigid_solver: bool = False
"""Whether an external solver integrates rigid bodies."""

update_joint_state: bool = True
"""Derive standalone VBD joint positions and velocities after each physics tick.

Disable only when consumers use body state exclusively. External rigid integration and coupled
solvers retain ownership of their generalized joint state and skip this synchronization.
"""

particle_enable_self_contact: bool = False
"""Whether to enable particle self-contact."""

Expand Down Expand Up @@ -57,6 +67,12 @@ class VBDSolverCfg(NewtonSolverCfg):
rigid_compliant_alm: bool | None = None
"""Whether to use compliant ALM for rigid joints and contacts; ``None`` preserves Newton's default."""

rigid_joint_linear_ke: float = 1.0e5
"""Structural linear stiffness of non-rod rigid joints [N/m], separate from joint-drive gains."""

rigid_joint_angular_ke: float = 1.0e5
"""Structural angular stiffness of non-rod rigid joints [N m/rad], separate from joint-drive gains."""

rigid_contact_k_start: float = 1.0e2
"""Initial stiffness seed for rigid-body contacts [N/m]."""

Expand Down
58 changes: 55 additions & 3 deletions source/isaaclab_newton/test/assets/test_articulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
rest and restores the properties it changes. Scene gravity is off; a test that needs gravity applies it to every
world for its own duration.

Configurations that fail initialization, the rebind scenario, which swaps the live Newton state, and the
CUDA-graph check build their own small scenes. Only one simulation context can be alive, so these tests come
first and fail if selected after a composite-scene test.
Configurations that fail initialization, nested-root IK, the rebind scenario, which swaps the live Newton
state, and the CUDA-graph check build their own small scenes. Only one simulation context can be alive, so
these tests come first and fail if selected after a composite-scene test.

The composite scene runs on the CPU without CUDA-graph capture. On CUDA, one own-scene test covers the
ordered-state republish recorded into a captured graph.
Expand Down Expand Up @@ -46,6 +46,9 @@
from isaaclab_newton.assets import Articulation
from isaaclab_newton.assets.articulation import kernels as articulation_kernels
from isaaclab_newton.assets.articulation.articulation_data import ArticulationData
from isaaclab_newton.controllers.ik import NewtonIKPoseObjectiveCfg
from isaaclab_newton.envs.mdp.actions.newton_ik_actions import NewtonInverseKinematicsAction
from isaaclab_newton.envs.mdp.actions.newton_ik_actions_cfg import NewtonInverseKinematicsActionCfg
from isaaclab_newton.physics import NewtonManager as SimulationManager
from isaaclab_physx.sim.schemas import PhysxJointCfg
from newton import JointType, ModelBuilder, ModelFlags, ShapeFlags
Expand Down Expand Up @@ -505,6 +508,55 @@ def test_world_hinged_root_has_no_base_dofs(monkeypatch, device):
##


@pytest.mark.parametrize("device", test_devices(DeviceScope.CPU))
def test_newton_ik_action_resolves_nested_articulation_root(device: str) -> None:
"""Resolve the nested prototype root and drive its joint toward an independently derived child pose."""
articulation_cfg = ArticulationCfg(
prim_path="/World/Env_[^/]*/Robot",
spawn=local_usd("floating_two_link.usda", fix_root_link=True),
articulation_root_prim_path="/Root",
actuators={"joint": ImplicitActuatorCfg(joint_names_expr=["Joint"], stiffness=20.0, damping=2.0)},
)
with build_simulation_context(sim_cfg=newton_sim_cfg(device)) as sim:
assets = spawn_assets({"robot": articulation_cfg}, num_envs=1)
sim.reset()
env = SimpleNamespace(scene=assets, sim=sim, num_envs=1, device=device)
action = NewtonInverseKinematicsAction(
NewtonInverseKinematicsActionCfg(
asset_name="robot",
joint_names=["Joint"],
objectives=[NewtonIKPoseObjectiveCfg(body_name="Child", use_relative_mode=False)],
),
env,
)
angle = 0.4
# The Z hinge is 0.25 m from each link origin in the checked-in two-link fixture.
target = torch.tensor(
[
[
0.25 + 0.25 * np.cos(angle),
0.25 * np.sin(angle),
0.0,
0.0,
0.0,
np.sin(0.5 * angle),
np.cos(0.5 * angle),
]
],
dtype=torch.float32,
device=device,
)
action.process_actions(target)
action.apply_actions()

torch.testing.assert_close(
assets["robot"].actuators.target_command.position.torch,
torch.tensor([[angle]], dtype=torch.float32, device=device),
atol=1.0e-4,
rtol=0.0,
)


@pytest.mark.parametrize("device", test_devices(DeviceScope.CPU))
@pytest.mark.parametrize("invalid_field", ["joint_pos", "joint_vel", "articulation_root_prim_path"])
def test_invalid_articulation_cfg_fails_initialization(device: str, invalid_field: str) -> None:
Expand Down
49 changes: 47 additions & 2 deletions source/isaaclab_newton/test/assets/test_rigid_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
Every environment holds a rigid-object cube and a three-cube collection, all locally spawned shapes. The cube
also stands beside the collection as an unrelated rigid body, so the collection view must select exactly its
configured bodies. Scene gravity is off; a test that needs gravity applies it to every world for its own duration.
Configurations that fail initialization and the single-instance cases build their own scenes. Only one simulation
context can be alive, so those tests come first and fail if selected after a shared-scene test.
Configurations that fail initialization, VBD state publication, and the single-instance cases build their own
scenes. Only one simulation context can be alive, so those tests come first and fail if selected after a
shared-scene test.
"""

from isaaclab_newton.physics import NewtonCfg
Expand All @@ -33,6 +34,7 @@
import warp as wp
from isaaclab_newton.assets import RigidObject, RigidObjectCollection
from isaaclab_newton.physics import NewtonManager as SimulationManager
from isaaclab_newton.physics import VBDSolverCfg
from newton import ModelFlags
from newton_test_utils import env_origins, local_usd, newton_sim_cfg, spawn_assets, world_gravity

Expand Down Expand Up @@ -111,6 +113,49 @@ def _collection_cfg(
##


@pytest.mark.parametrize("device", test_devices(DeviceScope.CUDA))
@pytest.mark.parametrize("state_owner", ["vbd", "body-only", "external"])
def test_vbd_root_state_tracks_integrated_body(device: str, state_owner: str) -> None:
Comment thread
maxkra15 marked this conversation as resolved.
"""Publish VBD body motion unless joint-state ownership is explicitly delegated."""
sim_cfg = SimulationCfg(
device=device,
physics=NewtonCfg(
solver_cfg=VBDSolverCfg(
iterations=5,
rigid_compliant_alm=True,
update_joint_state=state_owner != "body-only",
integrate_with_external_rigid_solver=state_owner == "external",
)
),
)
with build_simulation_context(sim_cfg=sim_cfg) as sim:
cube_object = spawn_assets({"cube": _cube_cfg()}, num_envs=1)["cube"]
sim.reset()

initial_height = cube_object.data.root_link_pose_w.torch[0, 2].item()
if state_owner == "external":
# An external rigid solver owns these generalized velocities, not VBD's body buffers.
SimulationManager.get_state_0().joint_qd.fill_(0.125)
SimulationManager.get_state_1().joint_qd.fill_(0.125)
sim.step()
Comment thread
maxkra15 marked this conversation as resolved.
cube_object.update(sim.cfg.dt)

body_pose = cube_object.data.body_link_pose_w.torch[:, 0]
body_velocity = cube_object.data.body_com_vel_w.torch[:, 0]
if state_owner == "external":
assert body_pose[0, 2].item() == initial_height
torch.testing.assert_close(cube_object.data.root_com_vel_w.torch, torch.full_like(body_velocity, 0.125))
return
assert body_pose[0, 2] < initial_height
assert body_velocity[0, 2] < 0.0
if state_owner == "vbd":
torch.testing.assert_close(cube_object.data.root_link_pose_w.torch, body_pose)
torch.testing.assert_close(cube_object.data.root_com_vel_w.torch, body_velocity)
else:
assert cube_object.data.root_link_pose_w.torch[0, 2].item() == initial_height
assert torch.count_nonzero(cube_object.data.root_com_vel_w.torch) == 0


@pytest.mark.isaacsim_ci
@pytest.mark.parametrize("api", ["none", "articulation"])
def test_initialization_rejects_non_rigid_body_prims(api: str) -> None:
Expand Down
Loading
Loading