From d5a8e36b0414d068b0efba9404cad7e771618a09 Mon Sep 17 00:00:00 2001 From: Maximilian Krause Date: Wed, 7 Oct 2026 11:36:17 +0200 Subject: [PATCH 1/4] Fix VBD articulation state and cloth bending integration --- .../concepts/solver-tuning/tune_vbd.rst | 6 ++ .../max-vbd-articulation-fixes.skip | 0 .../test/coupling/test_coupler.py | 2 +- .../max-vbd-articulation-fixes.added.rst | 1 + .../max-vbd-articulation-fixes.fixed.rst | 2 + .../max-vbd-articulation-fixes.minor | 0 .../envs/mdp/actions/newton_ik_actions.py | 3 +- .../isaaclab_newton/physics/vbd_manager.py | 21 ++++++- .../physics/vbd_manager_cfg.py | 13 ++++ .../test/assets/test_rigid_object.py | 44 +++++++++++++ .../test/physics/test_vbd_core.py | 61 ++++++++++++++++--- 11 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 source/isaaclab_contrib/changelog.d/max-vbd-articulation-fixes.skip create mode 100644 source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.added.rst create mode 100644 source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.fixed.rst create mode 100644 source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.minor diff --git a/docs/source/concepts/solver-tuning/tune_vbd.rst b/docs/source/concepts/solver-tuning/tune_vbd.rst index 193a2c700a45..4aef9e84254a 100644 --- a/docs/source/concepts/solver-tuning/tune_vbd.rst +++ b/docs/source/concepts/solver-tuning/tune_vbd.rst @@ -159,6 +159,12 @@ Core Solve - 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. * - ``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 joint positions and velocities from VBD-integrated body motion each tick. Disable only for body-state-only consumers. External rigid integration always skips this update, preserving the external solver's joint state. * - ``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`` diff --git a/source/isaaclab_contrib/changelog.d/max-vbd-articulation-fixes.skip b/source/isaaclab_contrib/changelog.d/max-vbd-articulation-fixes.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_contrib/test/coupling/test_coupler.py b/source/isaaclab_contrib/test/coupling/test_coupler.py index bf2e72e6ad3b..f5042d29ca3d 100644 --- a/source/isaaclab_contrib/test/coupling/test_coupler.py +++ b/source/isaaclab_contrib/test/coupling/test_coupler.py @@ -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) diff --git a/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.added.rst b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.added.rst new file mode 100644 index 000000000000..117370030e48 --- /dev/null +++ b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.added.rst @@ -0,0 +1 @@ +* Added VBD structural joint stiffness settings and optional generalized joint-state synchronization. diff --git a/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.fixed.rst b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.fixed.rst new file mode 100644 index 000000000000..86e74cb58ddb --- /dev/null +++ b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.fixed.rst @@ -0,0 +1,2 @@ +* Fixed VBD graph coloring for active cloth bending constraints and stale joint state after rigid-body integration. +* Fixed Newton IK prototype selection for nested articulation roots. diff --git a/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.minor b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.minor new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_newton/isaaclab_newton/envs/mdp/actions/newton_ik_actions.py b/source/isaaclab_newton/isaaclab_newton/envs/mdp/actions/newton_ik_actions.py index 5e123de01223..d4fc2f3dd934 100644 --- a/source/isaaclab_newton/isaaclab_newton/envs/mdp/actions/newton_ik_actions.py +++ b/source/isaaclab_newton/isaaclab_newton/envs/mdp/actions/newton_ik_actions.py @@ -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 "") 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) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py index b6857de8384c..e5b7ac380b61 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py @@ -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 @@ -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: @@ -34,6 +38,19 @@ 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: + """Synchronize generalized joint state from VBD's maximal-coordinate body state.""" + super()._run_solver_substeps(contacts) + if ( + 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) @classmethod def _simulate_physics_only(cls) -> None: diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py index d1a9effc177e..bef380d4fee9 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py @@ -30,6 +30,13 @@ class VBDSolverCfg(NewtonSolverCfg): integrate_with_external_rigid_solver: bool = False """Whether an external solver integrates rigid bodies.""" + update_joint_state: bool = True + """Derive joint positions and velocities from VBD-integrated body motion after each physics tick. + + Disable only when consumers use body state exclusively. External rigid integration always skips + this synchronization so the external solver retains ownership of its generalized joint state. + """ + particle_enable_self_contact: bool = False """Whether to enable particle self-contact.""" @@ -57,6 +64,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].""" diff --git a/source/isaaclab_newton/test/assets/test_rigid_object.py b/source/isaaclab_newton/test/assets/test_rigid_object.py index fac10ee7bdfa..0f5cf3fad7c8 100644 --- a/source/isaaclab_newton/test/assets/test_rigid_object.py +++ b/source/isaaclab_newton/test/assets/test_rigid_object.py @@ -33,6 +33,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 @@ -111,6 +112,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: + """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() + 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: diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index 54641e10b664..39469b54a682 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -8,9 +8,11 @@ from __future__ import annotations import importlib +import math from types import SimpleNamespace import pytest +import warp as wp from isaaclab_newton.physics import NewtonBackendCfg, NewtonBuilderCfg, NewtonCfg, NewtonManager, NewtonSoftContactCfg from newton import ModelBuilder from newton.solvers import SolverVBD @@ -79,16 +81,23 @@ def control(self): def test_vbd_colors_builder_before_finalization(): - """VBD colors the completed builder in the existing pre-finalization hook.""" + """Particles coupled by active cloth bending must receive distinct solver colors.""" physics = importlib.import_module("isaaclab_newton.physics") - events = [] - - class Builder: - def color(self, *, balance_colors): - events.append(("color", balance_colors)) - - physics.NewtonVBDManager._prepare_builder_for_finalize(Builder()) - assert events == [("color", False)] + builder = ModelBuilder() + builder.add_cloth_grid( + pos=wp.vec3(0.0), + rot=wp.quat_identity(), + vel=wp.vec3(0.0), + dim_x=1, + dim_y=1, + cell_x=0.1, + cell_y=0.1, + mass=0.01, + edge_ke=1.0, + ) + physics.NewtonVBDManager._prepare_builder_for_finalize(builder) + colors = {int(particle): color for color, group in enumerate(builder.particle_color_groups) for particle in group} + assert len(set(colors.values())) == builder.particle_count def test_vbd_solver_force_input_capability(monkeypatch): @@ -158,6 +167,40 @@ def test_vbd_compliant_alm_cable_stiffness(): assert state_0.body_q.numpy()[body, 2] == pytest.approx(-expected_extension, rel=0.01) +@pytest.mark.parametrize("stiffness", [None, (1.0e3, 1.0e2)], ids=["defaults", "finite-attachment"]) +def test_vbd_structural_joint_stiffness_under_load(stiffness): + """A fixed joint balances gravity with independently configured linear and angular springs.""" + physics = importlib.import_module("isaaclab_newton.physics") + gravity, lever = 9.81, 0.1 + builder = ModelBuilder(gravity=(0.0, 0.0, -gravity)) + body = builder.add_link(xform=wp.transform(wp.vec3(lever, 0.0, 0.0), wp.quat_identity())) + builder.add_shape_sphere(body=body, radius=0.05) + mass = builder.body_mass[body] + joint = builder.add_joint_fixed( + parent=-1, child=body, child_xform=wp.transform(wp.vec3(-lever, 0.0, 0.0), wp.quat_identity()) + ) + builder.add_articulation([joint]) + builder.color() + model = builder.finalize(device="cpu") + solver_cfg = physics.VBDSolverCfg(rigid_compliant_alm=True, iterations=15) + linear_ke, angular_ke = (1.0e5, 1.0e5) if stiffness is None else stiffness + if stiffness is not None: + solver_cfg = solver_cfg.replace(rigid_joint_linear_ke=linear_ke, rigid_joint_angular_ke=angular_ke) + solver = physics.NewtonVBDManager._create_solver(model, solver_cfg) + state_0, state_1, control = model.state(), model.state(), model.control() + for _ in range(480): + state_0.clear_forces() + solver.step(state_0, state_1, control, None, 1.0 / 240.0) + state_0, state_1 = state_1, state_0 + + pose = state_0.body_q.numpy()[body] + angle = 2.0 * math.atan2(float(pose[4]), float(pose[6])) + anchor_height = float(pose[2]) + lever * math.sin(angle) + # The joint's linear force balances weight; its angular torque balances the offset load. + assert -anchor_height * linear_ke == pytest.approx(mass * gravity, rel=0.01) + assert angle * angular_ke == pytest.approx(mass * gravity * lever * math.cos(angle), rel=0.01) + + def test_vbd_rebuilds_particle_bvh_before_physics_step(monkeypatch): """VBD rebuilds its particle BVH before the base physics step.""" physics = importlib.import_module("isaaclab_newton.physics") From 2b32a52e119003750302d76f2d57e1e2b832b079 Mon Sep 17 00:00:00 2001 From: Maximilian Krause Date: Sun, 11 Oct 2026 12:43:20 +0200 Subject: [PATCH 2/4] Preserve coupled joint-state ownership in VBD --- docs/source/how-to/solver_tuning_vbd.rst | 4 ++- .../test/coupling/test_coupler_runtime.py | 28 ++++++++++++++--- .../max-vbd-articulation-fixes.added.rst | 2 +- .../max-vbd-articulation-fixes.fixed.rst | 1 + .../isaaclab_newton/physics/vbd_manager.py | 5 ++-- .../physics/vbd_manager_cfg.py | 9 ++++-- .../test/assets/test_rigid_object.py | 5 ++-- .../test/physics/test_vbd_core.py | 30 +++++++++++++------ 8 files changed, 62 insertions(+), 22 deletions(-) diff --git a/docs/source/how-to/solver_tuning_vbd.rst b/docs/source/how-to/solver_tuning_vbd.rst index d071f0f32f5f..a835efbfb439 100644 --- a/docs/source/how-to/solver_tuning_vbd.rst +++ b/docs/source/how-to/solver_tuning_vbd.rst @@ -159,6 +159,8 @@ 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`` @@ -166,7 +168,7 @@ Core Solve * - ``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 joint positions and velocities from VBD-integrated body motion each tick. Disable only for body-state-only consumers. External rigid integration always skips this update, preserving the external solver's 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`` diff --git a/source/isaaclab_contrib/test/coupling/test_coupler_runtime.py b/source/isaaclab_contrib/test/coupling/test_coupler_runtime.py index 5cd3b037fca8..8f130f45424d 100644 --- a/source/isaaclab_contrib/test/coupling/test_coupler_runtime.py +++ b/source/isaaclab_contrib/test/coupling/test_coupler_runtime.py @@ -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, @@ -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, @@ -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 @@ -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": @@ -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 diff --git a/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.added.rst b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.added.rst index 117370030e48..67308815908e 100644 --- a/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.added.rst +++ b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.added.rst @@ -1 +1 @@ -* Added VBD structural joint stiffness settings and optional generalized joint-state synchronization. +* Added VBD structural joint stiffness and friction-smoothing settings, and optional standalone generalized joint-state synchronization. diff --git a/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.fixed.rst b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.fixed.rst index 86e74cb58ddb..7318292f3b9a 100644 --- a/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.fixed.rst +++ b/source/isaaclab_newton/changelog.d/max-vbd-articulation-fixes.fixed.rst @@ -1,2 +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. diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py index e5b7ac380b61..7d51b10bb7c4 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py @@ -42,10 +42,11 @@ def _build_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> None: @classmethod def _run_solver_substeps(cls, contacts) -> None: - """Synchronize generalized joint state from VBD's maximal-coordinate body state.""" + """Publish standalone VBD joint state without changing coupled-solver ownership.""" super()._run_solver_substeps(contacts) if ( - cls._update_joint_state + isinstance(cls._solver, SolverVBD) + and cls._update_joint_state and cls.backend.model.articulation_count > 0 and cls.backend.model.joint_coord_count > 0 ): diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py index bef380d4fee9..b241924e7906 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py @@ -27,14 +27,17 @@ 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 joint positions and velocities from VBD-integrated body motion after each physics tick. + """Derive standalone VBD joint positions and velocities after each physics tick. - Disable only when consumers use body state exclusively. External rigid integration always skips - this synchronization so the external solver retains ownership of its generalized joint state. + 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 diff --git a/source/isaaclab_newton/test/assets/test_rigid_object.py b/source/isaaclab_newton/test/assets/test_rigid_object.py index a3c1f68d1c16..7415620c0837 100644 --- a/source/isaaclab_newton/test/assets/test_rigid_object.py +++ b/source/isaaclab_newton/test/assets/test_rigid_object.py @@ -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 diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index 39469b54a682..112aa5dc166d 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -121,13 +121,24 @@ def test_vbd_solver_force_input_capability(monkeypatch): assert NewtonManager._supports_rigid_body_force_input is False -@pytest.mark.parametrize("overrides", [{}, {"rigid_compliant_alm": False}], ids=["defaults", "legacy"]) +@pytest.mark.parametrize( + "overrides", [{}, {"rigid_compliant_alm": False, "friction_epsilon": 1.0e-3}], ids=["defaults", "legacy"] +) def test_vbd_rigid_solver_controls(overrides): - """VBD preserves the default controls and explicit legacy-mode selection.""" + """VBD configuration preserves native defaults and forwards explicit controls.""" physics = importlib.import_module("isaaclab_newton.physics") kwargs = NewtonManager._filter_solver_kwargs(SolverVBD, physics.VBDSolverCfg(**overrides)) - assert kwargs["rigid_compliant_alm"] is overrides.get("rigid_compliant_alm") assert kwargs["rigid_body_contact_buffer_size"] == 64 + model = ModelBuilder().finalize(device="cpu") + native_solver = SolverVBD(model, **overrides) + configured_solver = physics.NewtonVBDManager._create_solver(model, physics.VBDSolverCfg(**overrides)) + for name in ( + "rigid_compliant_alm", + "rigid_joint_linear_ke", + "rigid_joint_angular_ke", + "friction_epsilon", + ): + assert getattr(configured_solver, name) == getattr(native_solver, name) def test_vbd_compliant_alm_cable_stiffness(): @@ -167,8 +178,7 @@ def test_vbd_compliant_alm_cable_stiffness(): assert state_0.body_q.numpy()[body, 2] == pytest.approx(-expected_extension, rel=0.01) -@pytest.mark.parametrize("stiffness", [None, (1.0e3, 1.0e2)], ids=["defaults", "finite-attachment"]) -def test_vbd_structural_joint_stiffness_under_load(stiffness): +def test_vbd_structural_joint_stiffness_under_load(): """A fixed joint balances gravity with independently configured linear and angular springs.""" physics = importlib.import_module("isaaclab_newton.physics") gravity, lever = 9.81, 0.1 @@ -182,10 +192,12 @@ def test_vbd_structural_joint_stiffness_under_load(stiffness): builder.add_articulation([joint]) builder.color() model = builder.finalize(device="cpu") - solver_cfg = physics.VBDSolverCfg(rigid_compliant_alm=True, iterations=15) - linear_ke, angular_ke = (1.0e5, 1.0e5) if stiffness is None else stiffness - if stiffness is not None: - solver_cfg = solver_cfg.replace(rigid_joint_linear_ke=linear_ke, rigid_joint_angular_ke=angular_ke) + linear_ke, angular_ke = 1.0e3, 1.0e2 + solver_cfg = replace( + physics.VBDSolverCfg(rigid_compliant_alm=True, iterations=15), + rigid_joint_linear_ke=linear_ke, + rigid_joint_angular_ke=angular_ke, + ) solver = physics.NewtonVBDManager._create_solver(model, solver_cfg) state_0, state_1, control = model.state(), model.state(), model.control() for _ in range(480): From f5fa3853cd525d403dd3e64e9239f6ec792f9d49 Mon Sep 17 00:00:00 2001 From: Maximilian Krause Date: Mon, 12 Oct 2026 13:08:32 +0200 Subject: [PATCH 3/4] Validate nested-root Newton IK action construction --- .../test/assets/test_articulation.py | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/source/isaaclab_newton/test/assets/test_articulation.py b/source/isaaclab_newton/test/assets/test_articulation.py index a79b4b918c80..56a778ced34f 100644 --- a/source/isaaclab_newton/test/assets/test_articulation.py +++ b/source/isaaclab_newton/test/assets/test_articulation.py @@ -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. @@ -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 @@ -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: From f2a6caef2e342d511c2b15677f24fe862fcfef66 Mon Sep 17 00:00:00 2001 From: Maximilian Krause Date: Mon, 12 Oct 2026 13:24:46 +0200 Subject: [PATCH 4/4] Document structural stiffness and collision scheduling --- docs/source/how-to/solver_tuning_vbd.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/source/how-to/solver_tuning_vbd.rst b/docs/source/how-to/solver_tuning_vbd.rst index a835efbfb439..c1c5d31bb908 100644 --- a/docs/source/how-to/solver_tuning_vbd.rst +++ b/docs/source/how-to/solver_tuning_vbd.rst @@ -176,6 +176,11 @@ Core Solve * - ``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 ^^^^^^^^^^^^ @@ -229,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 ------------------------------