From 17d514658c995c71451396dcda305a153f9281c6 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Wed, 5 Aug 2026 14:24:59 +0200 Subject: [PATCH 01/16] Move VBD solver support into Newton core --- .../isaaclab_contrib.deformable.rst | 27 +- .../lab_newton/isaaclab_newton.physics.rst | 18 + .../experimental-features/bleeding-edge.rst | 7 +- .../migration/migrating_deformables.rst | 30 +- .../physical-backends/newton/index.rst | 5 +- .../newton/newton-manager-abstraction.rst | 12 +- .../newton/supported-features.rst | 15 +- .../physical-backends/newton/using-cables.rst | 6 +- .../newton/using-vbd-solver.rst | 37 +- .../01_assets/run_deformable_object.rst | 3 +- scripts/demos/deformables.py | 4 +- .../changelog.d/mmichelis-vbd-core.rst | 7 + source/isaaclab/isaaclab/app/sim_launcher.py | 11 +- source/isaaclab/test/app/test_kwarg_launch.py | 8 + .../changelog.d/mmichelis-vbd-core.rst | 22 + .../isaaclab_contrib/coupling/coupler.py | 4 +- .../coupled_mjwarp_vbd_manager.py | 11 +- .../custom_coupling/franka_soft_env_cfg.py | 1 - .../deformable/deformable_object.py | 18 +- .../deformable/newton_manager_cfg.py | 113 +---- .../deformable/vbd_manager.py | 255 +----------- .../test/coupling/test_coupler.py | 2 +- .../test/custom_coupling/test_manager.py | 21 + .../test/deformable/test_vbd_core_compat.py | 138 +++++++ .../changelog.d/mmichelis-vbd-core.minor.rst | 23 ++ .../isaaclab_newton/physics/__init__.pyi | 6 + .../isaaclab_newton/physics/newton_manager.py | 32 +- .../physics/newton_manager_cfg.py | 37 ++ .../isaaclab_newton/physics/vbd_manager.py | 51 +++ .../physics/vbd_manager_cfg.py | 67 +++ .../test_newton_manager_abstraction.py | 41 +- .../test/physics/test_vbd_core.py | 389 ++++++++++++++++++ .../changelog.d/mmichelis-vbd-core.rst | 8 + .../franka_soft/franka_cloth_env_cfg.py | 15 +- .../config/franka_soft/franka_soft_env_cfg.py | 13 +- 35 files changed, 1012 insertions(+), 445 deletions(-) create mode 100644 source/isaaclab/changelog.d/mmichelis-vbd-core.rst create mode 100644 source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst create mode 100644 source/isaaclab_contrib/test/deformable/test_vbd_core_compat.py create mode 100644 source/isaaclab_newton/changelog.d/mmichelis-vbd-core.minor.rst create mode 100644 source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py create mode 100644 source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py create mode 100644 source/isaaclab_newton/test/physics/test_vbd_core.py create mode 100644 source/isaaclab_tasks/changelog.d/mmichelis-vbd-core.rst diff --git a/docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst b/docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst index 71f19f451368..c67f078e0b81 100644 --- a/docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst +++ b/docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst @@ -32,30 +32,41 @@ Deformable Object Newton Solver Configurations ---------------------------- -.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.NewtonModelCfg +.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.NewtonModelSolverCfg :members: :show-inheritance: :exclude-members: __init__ -.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.NewtonModelSolverCfg +.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.CoupledMJWarpVBDSolverCfg :members: :show-inheritance: :exclude-members: __init__ -.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.VBDSolverCfg +Deprecated Compatibility Aliases +-------------------------------- + +``NewtonModelCfg`` and ``VBDSolverCfg`` remain available from +``isaaclab_contrib.deformable`` for compatibility. ``NewtonVBDManager`` remains +available from ``isaaclab_contrib.deformable.vbd_manager``. Use +:class:`isaaclab_newton.physics.NewtonSoftContactCfg`, +:class:`isaaclab_newton.physics.VBDSolverCfg`, and +:class:`isaaclab_newton.physics.NewtonVBDManager` for new code. Pass the +soft-contact configuration through +:attr:`isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. + +.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.NewtonModelCfg :members: + :no-index: :show-inheritance: :exclude-members: __init__ -.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.CoupledMJWarpVBDSolverCfg +.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.VBDSolverCfg :members: + :no-index: :show-inheritance: :exclude-members: __init__ -Newton Solver Managers ----------------------- - .. autoclass:: isaaclab_contrib.deformable.vbd_manager.NewtonVBDManager :members: - :inherited-members: + :no-index: :show-inheritance: diff --git a/docs/source/api/lab_newton/isaaclab_newton.physics.rst b/docs/source/api/lab_newton/isaaclab_newton.physics.rst index 6fc699d4205e..cb21aceb62e2 100644 --- a/docs/source/api/lab_newton/isaaclab_newton.physics.rst +++ b/docs/source/api/lab_newton/isaaclab_newton.physics.rst @@ -9,15 +9,18 @@ NewtonManager NewtonCfg + NewtonSoftContactCfg NewtonCollisionPipelineCfg NewtonFeatherstoneManager NewtonKaminoManager NewtonMPMManager NewtonMJWarpManager + NewtonVBDManager NewtonShapeCfg NewtonSolverCfg NewtonXPBDManager MJWarpSolverCfg + VBDSolverCfg XPBDSolverCfg FeatherstoneSolverCfg KaminoCollisionDetectorCfg @@ -50,6 +53,11 @@ Physics Configuration :show-inheritance: :exclude-members: __init__ +.. autoclass:: NewtonSoftContactCfg + :members: + :show-inheritance: + :exclude-members: __init__ + .. autoclass:: NewtonSolverCfg :members: :show-inheritance: @@ -60,6 +68,11 @@ Physics Configuration :show-inheritance: :exclude-members: __init__ +.. autoclass:: VBDSolverCfg + :members: + :show-inheritance: + :exclude-members: __init__ + .. autoclass:: XPBDSolverCfg :members: :show-inheritance: @@ -143,6 +156,11 @@ Solver Managers :inherited-members: :show-inheritance: +.. autoclass:: NewtonVBDManager + :members: + :inherited-members: + :show-inheritance: + .. autoclass:: NewtonXPBDManager :members: :inherited-members: diff --git a/docs/source/experimental-features/bleeding-edge.rst b/docs/source/experimental-features/bleeding-edge.rst index 48e0937f74a1..7fe5725809bc 100644 --- a/docs/source/experimental-features/bleeding-edge.rst +++ b/docs/source/experimental-features/bleeding-edge.rst @@ -63,9 +63,10 @@ The following features are currently available in ``isaaclab_contrib``: on SO(3) (Lee et al.). Suitable for both trajectory following and RL baselines. - API reference: :mod:`~isaaclab_contrib.controllers` * - **Newton VBD Deformable Objects** - - Extended deformable object support using the Newton physics backend with Vertex Block Descent (VBD), - including proxy coupling between MJWarp and VBD. - - API reference: :mod:`~isaaclab_contrib.deformable`, :mod:`~isaaclab_contrib.coupling` + - Extended deformable object support with proxy and custom coupling between MJWarp and VBD. + Standalone VBD is provided by :mod:`isaaclab_newton.physics`. + - API reference: :mod:`~isaaclab_contrib.deformable`, :mod:`~isaaclab_contrib.coupling`, + :mod:`~isaaclab_contrib.custom_coupling` Contributing ------------ diff --git a/docs/source/migration/migrating_deformables.rst b/docs/source/migration/migrating_deformables.rst index dbfc75cd1a52..6ccfe37f377a 100644 --- a/docs/source/migration/migrating_deformables.rst +++ b/docs/source/migration/migrating_deformables.rst @@ -71,6 +71,29 @@ from the physics backend package: - ``from isaaclab_newton.sim.spawners.materials import NewtonSurfaceDeformableBodyMaterialCfg`` * - ``from isaaclab_physx.assets import DeformableObjectCfg`` - ``from isaaclab.assets import DeformableObjectCfg`` + * - ``from isaaclab_contrib.deformable.vbd_manager import NewtonVBDManager`` + - ``from isaaclab_newton.physics import NewtonVBDManager`` + * - ``from isaaclab_contrib.deformable import VBDSolverCfg`` + - ``from isaaclab_newton.physics import VBDSolverCfg`` + * - ``from isaaclab_contrib.deformable import NewtonModelCfg`` + - ``from isaaclab_newton.physics import NewtonSoftContactCfg`` + +The contrib VBD configuration names remain as compatibility aliases. New code +should use the core imports above. Global soft-contact configuration also moved +from ``solver_cfg.model_cfg`` to the outer +:attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` field. + +The ``soft_contact_kd`` default also changed from ``0.01`` to ``10.0`` to match +Newton. Set ``soft_contact_kd=0.01`` explicitly to retain the previous behavior: + +.. code-block:: python + + from isaaclab_newton.physics import NewtonCfg, NewtonSoftContactCfg, VBDSolverCfg + + physics_cfg = NewtonCfg( + solver_cfg=VBDSolverCfg(iterations=10), + soft_contact_cfg=NewtonSoftContactCfg(soft_contact_kd=0.01), # retain legacy damping + ) Removed Properties @@ -245,9 +268,10 @@ Limitations deformable will raise a ``ValueError``. - **Surface-specific solver properties** (``collision_pair_update_frequency``, ``collision_iteration_multiplier``) have no effect on volume deformables. -- **Newton deformables are experimental.** They are implemented in - :mod:`isaaclab_contrib.deformable` and currently target VBD-based solvers and - coupled rigid-deformable workflows. +- **Newton deformables are experimental.** Standalone VBD configuration and its + manager live in :mod:`isaaclab_newton.physics`. The deformable object + integration and coupled rigid-deformable configurations remain in + :mod:`isaaclab_contrib.deformable`. .. _Omni Physics documentation: https://docs.omniverse.nvidia.com/kit/docs/omni_physics/110.0/dev_guide/deformables/deformable_bodies.html diff --git a/docs/source/overview/core-concepts/physical-backends/newton/index.rst b/docs/source/overview/core-concepts/physical-backends/newton/index.rst index fb91324469e2..09dd077c014b 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/index.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/index.rst @@ -35,8 +35,9 @@ the following solver pages: * :doc:`mjwarp-solver` — the primary, validated solver path. * :doc:`kamino-solver` — beta support on selected classic tasks. -* :doc:`using-vbd-solver`: experimental VBD solver for cloth and soft bodies, - with MJWarp and VBD proxy coupling through :mod:`isaaclab_contrib.coupling`. +* :doc:`using-vbd-solver`: experimental VBD solver for cloth and soft bodies. + Standalone VBD is available through :mod:`isaaclab_newton.physics`; MJWarp and + VBD proxy and ADMM coupling are available through :mod:`isaaclab_contrib.coupling`. * :doc:`using-mpm`: experimental implicit MPM for particle materials, including rigid-MPM proxy coupling. diff --git a/docs/source/overview/core-concepts/physical-backends/newton/newton-manager-abstraction.rst b/docs/source/overview/core-concepts/physical-backends/newton/newton-manager-abstraction.rst index 3634ecf073f8..5ea53a97f38d 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/newton-manager-abstraction.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/newton-manager-abstraction.rst @@ -140,7 +140,7 @@ The MJWarp + VBD deformable manager is a concrete example: * :class:`~isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg` stores a ``rigid_solver_cfg`` for :class:`~isaaclab_newton.physics.MJWarpSolverCfg`, a - ``soft_solver_cfg`` for :class:`~isaaclab_contrib.deformable.VBDSolverCfg`, + ``soft_solver_cfg`` for :class:`~isaaclab_newton.physics.VBDSolverCfg`, and a ``coupling_mode``. * ``NewtonCoupledMJWarpVBDManager._build_solver()`` constructs ``SolverMuJoCo`` and ``SolverVBD`` from those sub-configs. @@ -232,13 +232,15 @@ Tune the coupled contact behavior before training a policy: * Lower the arm actuator stiffness enough that the arm can respond to contact penalties. Prefer the arm being pushed back over the gripper clipping into the deformable. -* Tune :attr:`~isaaclab_contrib.deformable.NewtonModelCfg.soft_contact_ke` +* Tune :attr:`~isaaclab_newton.physics.NewtonSoftContactCfg.soft_contact_ke` first. Increase it only as much as needed to prevent clipping, then adjust - :attr:`~isaaclab_contrib.deformable.NewtonModelCfg.soft_contact_mu` so the + :attr:`~isaaclab_newton.physics.NewtonSoftContactCfg.soft_contact_mu` so the gripper can carry the object without requiring an obviously unphysical friction value. Use - :attr:`~isaaclab_contrib.deformable.NewtonModelCfg.soft_contact_kd` for + :attr:`~isaaclab_newton.physics.NewtonSoftContactCfg.soft_contact_kd` for stabilization if contacts chatter. + Set this configuration on the outer + :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` field. * Tune the ``soft_contact_*`` values together with the rigid shape contact material, because the shape's ``ke``/``kd``/``mu`` also affect the effective contact. Set shape defaults via @@ -254,7 +256,7 @@ Tune the coupled contact behavior before training a policy: increase ``particle_radius`` in the deformable material so contact is detected earlier from a larger distance. * If the rigid shapes still clip through the deformable, increase - :attr:`~isaaclab_contrib.deformable.VBDSolverCfg.iterations`; more VBD + :attr:`~isaaclab_newton.physics.VBDSolverCfg.iterations`; more VBD iterations can improve contact convergence. diff --git a/docs/source/overview/core-concepts/physical-backends/newton/supported-features.rst b/docs/source/overview/core-concepts/physical-backends/newton/supported-features.rst index 9fa30170a5cc..5e39b1f37b0f 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/supported-features.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/supported-features.rst @@ -56,6 +56,8 @@ isaaclab isaaclab_newton ^^^^^^^^^^^^^^^^ +* Standalone VBD deformable solver + * Implicit Material Point Method (MPM) solver and declarative particle assets * Fixed and capture-compatible capacity-bounded sparse MPM grids * Standard visual-material binding for MPM particle visualization @@ -63,9 +65,8 @@ isaaclab_newton isaaclab_contrib ^^^^^^^^^^^^^^^^ -* Experimental Newton deformable objects -* VBD deformable solver (see :doc:`using-vbd-solver`) -* MJWarp and VBD proxy coupling +* Newton deformable object integration +* MJWarp and VBD proxy and ADMM coupling * Proxy-based coupling for rigid and particle solvers, including MJWarp + MPM The following sensors are backend-agnostic (implemented in ``isaaclab`` core) @@ -112,10 +113,10 @@ Solver Coverage * **Kamino solver**: beta. Currently validated on ``Isaac-Cartpole-Direct``, ``Isaac-Ant-Direct``, ``Isaac-Cartpole``, and ``Isaac-Ant``. See :doc:`kamino-solver`. -* **VBD solver**: experimental, exposed through :mod:`isaaclab_contrib.deformable` - for cloth, soft-body, and cable simulation. Rigid and deformable scenes can use - proxy coupling from :mod:`isaaclab_contrib.coupling` so MJWarp advances rigid - bodies and VBD advances deformable particles. Cable objects work with standalone +* **VBD solver**: experimental, exposed through :mod:`isaaclab_newton.physics` + for standalone cloth, soft-body, and cable simulation. Rigid and deformable + scenes can use proxy or ADMM coupling from :mod:`isaaclab_contrib.coupling` so + MJWarp advances rigid bodies and VBD advances deformable particles. Cable objects work with standalone VBD and with :class:`~isaaclab_contrib.coupling.CouplerProxyCfg` when a named VBD entry owns the cable. See :doc:`using-vbd-solver` and :doc:`newton-manager-abstraction`. diff --git a/docs/source/overview/core-concepts/physical-backends/newton/using-cables.rst b/docs/source/overview/core-concepts/physical-backends/newton/using-cables.rst index f50afdc5b6e2..4ea02d2b7952 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/using-cables.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/using-cables.rst @@ -132,13 +132,11 @@ Picking a Solver Cables can **only** be simulated under Newton's VBD solver, which is the only solver that steps ``JointType.CABLE`` joints. A cable-only scene uses a -standalone :class:`~isaaclab_contrib.deformable.VBDSolverCfg`: +standalone :class:`~isaaclab_newton.physics.VBDSolverCfg`: .. code-block:: python - from isaaclab_newton.physics import NewtonCfg - - from isaaclab_contrib.deformable import VBDSolverCfg + from isaaclab_newton.physics import NewtonCfg, VBDSolverCfg sim_cfg = sim_utils.SimulationCfg( dt=0.01, diff --git a/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst b/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst index b2b7c63c453b..d33404498e05 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst @@ -5,8 +5,8 @@ VBD Solver Vertex Block Descent (VBD) is a Newton solver for cloth and soft-body simulation. In Isaac Lab, VBD is enabled by selecting a -:class:`~isaaclab_newton.physics.NewtonCfg` whose ``solver_cfg`` is provided by -:mod:`isaaclab_contrib.deformable`. +:class:`~isaaclab_newton.physics.NewtonCfg` whose ``solver_cfg`` is a +:class:`~isaaclab_newton.physics.VBDSolverCfg`. VBD support is experimental. The solver managers, configuration fields, and recommended tuning values may change while Newton deformable support is under @@ -16,7 +16,7 @@ before it works well with VBD. VBD is usually exposed through a task-specific physics preset rather than a general ``newton_vbd`` preset. Deformable-only scenes can use -:class:`~isaaclab_contrib.deformable.VBDSolverCfg` directly. Robot or +:class:`~isaaclab_newton.physics.VBDSolverCfg` directly. Robot or rigid-body scenes can use either: * :class:`~isaaclab_contrib.coupling.CouplerProxyCfg` for the proxy coupling @@ -86,9 +86,9 @@ Add a VBD Physics Preset Tasks that support multiple physics options usually store ``SimulationCfg.physics`` as a :class:`~isaaclab_tasks.utils.hydra.PresetCfg`. For deformable Newton tasks, -the preset is a plain :class:`~isaaclab_newton.physics.NewtonCfg` whose solver -config carries :class:`~isaaclab_contrib.deformable.NewtonModelCfg` through its -:class:`~isaaclab_contrib.deformable.NewtonModelSolverCfg` base class. +the preset is a plain :class:`~isaaclab_newton.physics.NewtonCfg`. Standalone +VBD and soft-contact configuration live in :mod:`isaaclab_newton.physics`, while +proxy and custom solver coupling remain in :mod:`isaaclab_contrib`. The Franka soft-body and cloth tasks define task-specific proxy presets. @@ -102,8 +102,8 @@ The important pieces are: * Add :class:`~isaaclab_contrib.coupling.CouplerProxyMappingCfg` entries for the collidable rigid bodies exposed to VBD. Leave ``integrate_with_external_rigid_solver=False`` for proxy-coupled VBD entries. -* Set the solver config's ``model_cfg`` to a - :class:`~isaaclab_contrib.deformable.NewtonModelCfg` when body-particle or +* Set the outer :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` to a + :class:`~isaaclab_newton.physics.NewtonSoftContactCfg` when body-particle or self-contact values need task-level tuning. * Keep the preset at the same config path used by the task's :class:`~isaaclab.sim.SimulationCfg`, for example ``env.sim.physics``. @@ -164,8 +164,8 @@ task, validate the following before tuning solver parameters: * Proxy-coupled robot tasks should expose only the collidable bodies needed for deformable contact. * Contact-heavy scenes usually need task-specific ``num_substeps``, - :class:`~isaaclab_contrib.deformable.VBDSolverCfg`, and - :class:`~isaaclab_contrib.deformable.NewtonModelCfg` values. Start from the + :class:`~isaaclab_newton.physics.VBDSolverCfg`, and + :class:`~isaaclab_newton.physics.NewtonSoftContactCfg` values. Start from the Franka soft-body or cloth preset that most closely resembles the scene. * Use a small visual smoke test before training. Confirm that the deformable spawns, renders, deforms, and contacts rigid bodies as expected. @@ -174,7 +174,7 @@ VBD Solver Parameters --------------------- The following fields are specific to -:class:`~isaaclab_contrib.deformable.VBDSolverCfg`. They are grouped by the part +:class:`~isaaclab_newton.physics.VBDSolverCfg`. They are grouped by the part of the solver they affect. Core Solve @@ -378,8 +378,9 @@ Contact and Material Parameters Contact Model ^^^^^^^^^^^^^ -:class:`~isaaclab_contrib.deformable.NewtonModelCfg` applies contact parameters -to the finalized Newton model: +:class:`~isaaclab_newton.physics.NewtonSoftContactCfg` applies contact parameters +to the finalized Newton model through +:attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`: .. list-table:: :header-rows: 1 @@ -390,7 +391,7 @@ to the finalized Newton model: * - ``soft_contact_ke`` - Default: ``1.0e3`` [N/m]. Stiffness for body-particle and particle self-contact. Increase it to reduce clipping through rigid shapes or through other deformable particles. If it is too high, the object can stop visibly deforming or require more VBD iterations and substeps. * - ``soft_contact_kd`` - - Default: ``1.0e-2`` [N*s/m]. Contact damping. Increase it to reduce chatter or bouncing. Too much damping can make contact response sticky or overdamped. + - Default: ``1.0e1`` [N*s/m]. Contact damping. Increase it to reduce chatter or bouncing. Too much damping can make contact response sticky or overdamped. * - ``soft_contact_mu`` - Default: ``0.5``. Friction coefficient for body-particle and particle self-contact. Increase it when a gripper cannot carry the deformable object without slipping. @@ -467,12 +468,12 @@ Use the following sequence when bringing up a new VBD task: the expected range without rigid contact. 5. Increase ``num_substeps`` or decrease ``dt`` if the object is unstable before increasing stiffness further. -6. Increase :attr:`~isaaclab_contrib.deformable.VBDSolverCfg.iterations` when +6. Increase :attr:`~isaaclab_newton.physics.VBDSolverCfg.iterations` when contacts or stiff materials do not converge within a substep. -7. Tune :attr:`~isaaclab_contrib.deformable.NewtonModelCfg.soft_contact_ke` to +7. Tune :attr:`~isaaclab_newton.physics.NewtonSoftContactCfg.soft_contact_ke` to reduce rigid/deformable clipping, then tune - :attr:`~isaaclab_contrib.deformable.NewtonModelCfg.soft_contact_mu` for grip - and :attr:`~isaaclab_contrib.deformable.NewtonModelCfg.soft_contact_kd` for + :attr:`~isaaclab_newton.physics.NewtonSoftContactCfg.soft_contact_mu` for grip + and :attr:`~isaaclab_newton.physics.NewtonSoftContactCfg.soft_contact_kd` for chatter. 8. Enable self-contact only after body-particle contact is stable, then tune ``particle_self_contact_radius`` for active self-contact thickness, diff --git a/docs/source/tutorials/01_assets/run_deformable_object.rst b/docs/source/tutorials/01_assets/run_deformable_object.rst index dc4470f4aefb..0b361cfb67ef 100644 --- a/docs/source/tutorials/01_assets/run_deformable_object.rst +++ b/docs/source/tutorials/01_assets/run_deformable_object.rst @@ -13,7 +13,8 @@ surface deformables (cloth), see the deformable demo at ``scripts/demos/deformab The deformable object API and schema define/modify functions are shared across backends, while deformable property and material configuration classes are backend-specific. PhysX simulates soft bodies using the Finite -Element Method (FEM); the Newton experimental backend uses VBD-based deformable support from +Element Method (FEM); the Newton experimental backend uses the core VBD solver from +:mod:`isaaclab_newton.physics` with the experimental deformable object integration from :mod:`isaaclab_contrib.deformable`. The volume deformable comprises of two tetrahedral meshes -- a simulation mesh and a collision mesh. The simulation mesh is used to simulate the deformations of the soft body, while the collision mesh is used to detect collisions diff --git a/scripts/demos/deformables.py b/scripts/demos/deformables.py index 5ecd9b41c9ef..cdc53bc8e27c 100644 --- a/scripts/demos/deformables.py +++ b/scripts/demos/deformables.py @@ -70,7 +70,7 @@ from isaaclab.assets import DeformableObject if args_cli.physics == "newton_vbd": - from isaaclab_contrib.deformable.newton_manager_cfg import NewtonModelCfg # isort:skip + from isaaclab_newton.physics import NewtonSoftContactCfg # isort:skip from isaaclab_newton.sim.schemas import NewtonDeformableBodyPropertiesCfg as DeformableBodyPropertiesCfg from isaaclab_newton.sim.spawners.materials import ( NewtonDeformableBodyMaterialCfg as VolumeDeformableMaterialCfg, @@ -270,7 +270,7 @@ def main(): physics_cfg.solver_cfg.particle_self_contact_radius = 0.0001 physics_cfg.solver_cfg.particle_self_contact_margin = 0.1 physics_cfg.num_substeps = 4 - physics_cfg.solver_cfg.model_cfg = NewtonModelCfg( + physics_cfg.soft_contact_cfg = NewtonSoftContactCfg( soft_contact_ke=1.0e5, soft_contact_kd=1.0e0, soft_contact_mu=0.01, diff --git a/source/isaaclab/changelog.d/mmichelis-vbd-core.rst b/source/isaaclab/changelog.d/mmichelis-vbd-core.rst new file mode 100644 index 000000000000..39c88d13632e --- /dev/null +++ b/source/isaaclab/changelog.d/mmichelis-vbd-core.rst @@ -0,0 +1,7 @@ +Changed +^^^^^^^ + +* Changed the ``newton_vbd`` launcher backend to use + :class:`~isaaclab_newton.physics.VBDSolverCfg` from core. Custom launchers + should import the solver configuration from ``isaaclab_newton.physics`` + instead of ``isaaclab_contrib.deformable``. diff --git a/source/isaaclab/isaaclab/app/sim_launcher.py b/source/isaaclab/isaaclab/app/sim_launcher.py index b3c2fff5d81a..8f0c3fbd0096 100644 --- a/source/isaaclab/isaaclab/app/sim_launcher.py +++ b/source/isaaclab/isaaclab/app/sim_launcher.py @@ -22,7 +22,7 @@ from dataclasses import dataclass from typing import Any -from isaaclab_newton.physics import NewtonCfg +from isaaclab_newton.physics import NewtonCfg, VBDSolverCfg from isaaclab_ov.physics import OvPhysxCfg from isaaclab_ov.renderers import OVRTXRendererCfg from isaaclab_physx.physics import PhysxCfg @@ -74,15 +74,6 @@ def make_physics_cfg(physics_cfg_str: str) -> PhysicsCfg: if physics_cfg_str == "newton_mjwarp": return NewtonCfg() if physics_cfg_str == "newton_vbd": - # lazy import: core depends on isaaclab_contrib only when VBD is requested - try: - from isaaclab_contrib.deformable.newton_manager_cfg import VBDSolverCfg - except ImportError as err: - raise ImportError( - "The 'newton_vbd' physics backend requires the isaaclab_contrib package." - " Install it with `./isaaclab.sh -i contrib`." - ) from err - return NewtonCfg(solver_cfg=VBDSolverCfg()) if physics_cfg_str == "ovphysx": return OvPhysxCfg() diff --git a/source/isaaclab/test/app/test_kwarg_launch.py b/source/isaaclab/test/app/test_kwarg_launch.py index 249fad068915..268ac7d9bba0 100644 --- a/source/isaaclab/test/app/test_kwarg_launch.py +++ b/source/isaaclab/test/app/test_kwarg_launch.py @@ -7,6 +7,7 @@ import logging import pytest +from isaaclab_newton.physics import NewtonCfg, VBDSolverCfg import isaaclab.app as app_module import isaaclab.app.app_launcher as app_launcher_module @@ -18,6 +19,13 @@ pytestmark = pytest.mark.integration +def test_make_physics_cfg_builds_core_vbd(): + physics_cfg = sim_launcher.make_physics_cfg("newton_vbd") + + assert isinstance(physics_cfg, NewtonCfg) + assert isinstance(physics_cfg.solver_cfg, VBDSolverCfg) + + @pytest.mark.usefixtures("mocker") def test_livestream_launch_with_kwargs(mocker): """Test launching with keyword arguments.""" diff --git a/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst new file mode 100644 index 000000000000..1949d56af595 --- /dev/null +++ b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst @@ -0,0 +1,22 @@ +Changed +^^^^^^^ + +* Changed standalone VBD classes to compatibility aliases of + :class:`~isaaclab_newton.physics.NewtonVBDManager` and + :class:`~isaaclab_newton.physics.VBDSolverCfg`. Import these classes from + ``isaaclab_newton.physics`` for new code. +* Changed the VBD soft-contact damping default from ``0.01`` to ``10.0`` + to match Newton. Set ``soft_contact_kd=0.01`` explicitly through + :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` to retain the + previous behavior. + +Deprecated +^^^^^^^^^^ + +* Deprecated ``isaaclab_contrib.deformable.vbd_manager.NewtonVBDManager`` and + ``isaaclab_contrib.deformable.VBDSolverCfg`` in favor of their + :mod:`isaaclab_newton.physics` aliases. +* Deprecated ``isaaclab_contrib.deformable.NewtonModelCfg`` in favor of + :class:`~isaaclab_newton.physics.NewtonSoftContactCfg`. Move the + configuration from ``solver_cfg.model_cfg`` to + :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. diff --git a/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py b/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py index c69aeb3d4133..b1e69b1412ea 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py +++ b/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py @@ -21,13 +21,13 @@ ) from isaaclab_newton.physics.mpm_manager import NewtonMPMManager from isaaclab_newton.physics.newton_manager import NewtonManager +from isaaclab_newton.physics.vbd_manager import NewtonVBDManager from newton import CollisionPipeline, Model, ModelBuilder, ShapeFlags from newton.solvers.experimental.coupled import SolverCoupled, SolverCoupledADMM, SolverCoupledProxy from isaaclab.physics import PhysicsManager from isaaclab.utils.string import resolve_matching_names -from ..deformable.vbd_manager import NewtonVBDManager from .coupler_cfg import ( CouplerAdmmCfg, CouplerCfg, @@ -144,7 +144,7 @@ def _validate_config(cls, solver_cfg: CouplerCfg) -> None: if getattr(nested_cfg, "model_cfg", None) is not None: raise ValueError( f"CouplerEntryCfg {entry.name!r} sets solver_cfg.model_cfg, but model parameters are global. " - "Set model_cfg on the outer CouplerCfg instead." + "Set soft_contact_cfg on the outer NewtonCfg instead." ) manager = nested_cfg.class_type factory = getattr(manager, "_create_solver", None) diff --git a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/coupled_mjwarp_vbd_manager.py b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/coupled_mjwarp_vbd_manager.py index 1dcdafdb8479..ace9b6d98271 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/coupled_mjwarp_vbd_manager.py +++ b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/coupled_mjwarp_vbd_manager.py @@ -9,11 +9,10 @@ import warp as wp from isaaclab_newton.physics.newton_manager import NewtonManager +from isaaclab_newton.physics.vbd_manager import NewtonVBDManager from newton import Contacts, Control, Model, State from newton.solvers import SolverBase, SolverMuJoCo, SolverVBD -from isaaclab_contrib.deformable.vbd_manager import NewtonVBDManager - from .kernels import _kernel_body_particle_reaction from .newton_manager_cfg import CoupledMJWarpVBDSolverCfg @@ -111,11 +110,11 @@ def _solver_specific_clear(cls) -> None: cls._coupling_mode = None @classmethod - def _simulate_physics_only(cls) -> None: - # Rebuild the BVH before stepping solvers that require it, such as VBD cloth. - if hasattr(cls._soft_solver, "rebuild_bvh"): + def _pre_physics_step(cls) -> None: + """Rebuild the coupled VBD particle BVH before collision detection.""" + super()._pre_physics_step() + if cls._model.particle_count > 0 and hasattr(cls._soft_solver, "rebuild_bvh"): cls._soft_solver.rebuild_bvh(cls._state_0) - super()._simulate_physics_only() @classmethod def _step_one_way(cls, state_in: State, state_out: State, control: Control, dt: float) -> None: diff --git a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/franka_soft_env_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/franka_soft_env_cfg.py index b4fce318d405..3cacab218fc8 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/franka_soft_env_cfg.py +++ b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/franka_soft_env_cfg.py @@ -35,7 +35,6 @@ class PhysicsCfg(CorePhysicsCfg): soft_solver_cfg=VBDSolverCfg( integrate_with_external_rigid_solver=True, ), - model_cfg=CorePhysicsCfg().newton_mjwarp_vbd_proxy.solver_cfg.model_cfg, ), ) diff --git a/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py b/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py index c71dec35be40..698f87b4944c 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py +++ b/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py @@ -240,11 +240,16 @@ def setup_registered_deformable_fabric_sync(manager_cls: type[SimulationManager] def install_deformable_builder_hooks() -> None: """Install deformable builder hooks without removing hooks owned by other extensions.""" - SimulationManager._deformable_registry = [] + if not hasattr(SimulationManager, "_deformable_registry"): + SimulationManager._deformable_registry = [] if not hasattr(SimulationManager, "_per_world_builder_hooks"): SimulationManager._per_world_builder_hooks = [] + if not hasattr(SimulationManager, "_post_start_simulation_hooks"): + SimulationManager._post_start_simulation_hooks = [] if add_registered_deformables_to_builder not in SimulationManager._per_world_builder_hooks: SimulationManager._per_world_builder_hooks.append(add_registered_deformables_to_builder) + if setup_registered_deformable_fabric_sync not in SimulationManager._post_start_simulation_hooks: + SimulationManager._post_start_simulation_hooks.append(setup_registered_deformable_fabric_sync) def clear_deformable_builder_hooks() -> None: @@ -256,6 +261,12 @@ def clear_deformable_builder_hooks() -> None: for hook in SimulationManager._per_world_builder_hooks if hook is not add_registered_deformables_to_builder ] + if hasattr(SimulationManager, "_post_start_simulation_hooks"): + SimulationManager._post_start_simulation_hooks = [ + hook + for hook in SimulationManager._post_start_simulation_hooks + if hook is not setup_registered_deformable_fabric_sync + ] class DeformableObject(BaseDeformableObject): @@ -289,6 +300,7 @@ def __init__(self, cfg: DeformableObjectCfg): self._deformable_type: str | None = None # Read mesh from the spawned USD prim and register in the deformable registry. + install_deformable_builder_hooks() self._registry_entry = self._register_deformable() # Register custom vec6f type for nodal state validation. @@ -1000,6 +1012,10 @@ def _clear_callbacks(self) -> None: if hasattr(self, "_physics_ready_handle") and self._physics_ready_handle is not None: self._physics_ready_handle.deregister() self._physics_ready_handle = None + if hasattr(self, "_registry_entry"): + SimulationManager._deformable_registry = [ + entry for entry in SimulationManager._deformable_registry if entry is not self._registry_entry + ] def _invalidate_initialize_callback(self, event): """Invalidates the scene elements.""" diff --git a/source/isaaclab_contrib/isaaclab_contrib/deformable/newton_manager_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/deformable/newton_manager_cfg.py index 7bcbbe1c5b50..ed3dabcb4968 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/deformable/newton_manager_cfg.py +++ b/source/isaaclab_contrib/isaaclab_contrib/deformable/newton_manager_cfg.py @@ -10,7 +10,12 @@ import warnings from typing import TYPE_CHECKING, Literal -from isaaclab_newton.physics import MJWarpSolverCfg, NewtonSolverCfg +from isaaclab_newton.physics import ( + MJWarpSolverCfg, + NewtonSolverCfg, + VBDSolverCfg, +) +from isaaclab_newton.physics import NewtonSoftContactCfg as NewtonModelCfg from isaaclab.utils.configclass import configclass @@ -18,114 +23,14 @@ from isaaclab_newton.physics import NewtonManager -@configclass -class NewtonModelCfg: - """Global Newton model parameters applied after builder finalization. - - These control model-level contact behavior shared across all objects. - """ - - soft_contact_ke: float = 1.0e3 - """Body-particle and particle self-contact stiffness [N/m]. - - Effective per-contact stiffness is the average of this value and the rigid - shape's material stiffness. - """ - - soft_contact_kd: float = 1.0e-2 - """Body-particle contact damping [N*s/m].""" - - soft_contact_mu: float = 0.5 - """Body-particle contact friction coefficient [dimensionless]. - - Effective per-contact friction is ``sqrt(soft_contact_mu * shape_mu)``, where - ``shape_mu`` is the rigid shape's own friction coefficient (from its per-asset - material or :attr:`~isaaclab_newton.physics.NewtonShapeCfg.mu` default), not - set by this config. - """ - - @configclass class NewtonModelSolverCfg(NewtonSolverCfg): - """Base for solver configs whose manager applies :class:`NewtonModelCfg` to the finalized model. - - TODO: Temporary. This base only exists because :class:`NewtonModelCfg` lives in - ``isaaclab_contrib`` while :class:`NewtonSolverCfg` is in ``isaaclab_newton`` core. - Once these model params move into core, ``model_cfg`` should live on - :class:`NewtonSolverCfg` (or ``NewtonCfg``) directly and this class can be removed. - """ + """Compatibility base for coupled solver configs with legacy model parameters.""" model_cfg: NewtonModelCfg | None = None - """Global Newton model parameters applied after builder finalization.""" - - -@configclass -class VBDSolverCfg(NewtonModelSolverCfg): - """Configuration for the Vertex Block Descent (VBD) solver. - - Supports cloth, soft bodies, and coupled rigid-body systems. Requires - ``ModelBuilder.color()`` before ``finalize()`` to build the vertex coloring. - """ - - class_type: type[NewtonManager] | str = "{DIR}.vbd_manager:NewtonVBDManager" - """Manager class for the VBD solver.""" - - iterations: int = 10 - """Number of VBD iterations per substep.""" - - integrate_with_external_rigid_solver: bool = False - """Whether rigid bodies are integrated by an external solver (one-way coupling). - - Set to ``True`` when coupling cloth with a separate rigid-body solver so VBD - only integrates the cloth particles. - """ - - particle_enable_self_contact: bool = False - """Whether to enable VBD deformable's self-contact.""" - - particle_self_contact_radius: float = 0.005 - """Particle radius used for self-contact detection [m].""" - - particle_self_contact_margin: float = 0.005 - """Self-contact detection margin [m]. Should be >= particle_self_contact_radius.""" - - particle_collision_detection_interval: int = -1 - """How often particle self-contact detection is applied. - - ``< 0``: once before initialization. ``0``: once before and once after - initialization. ``k >= 1``: before every ``k`` VBD iterations. - """ - - particle_vertex_contact_buffer_size: int = 32 - """Preallocation size for each vertex's vertex-triangle collision buffer.""" - - particle_edge_contact_buffer_size: int = 64 - """Preallocation size for each edge's edge-edge collision buffer.""" - - particle_topological_contact_filter_threshold: int = 2 - """Maximum topological distance (in rings) below which self-contacts are discarded. - - Only used when ``particle_enable_self_contact`` is ``True``. Values > 3 - significantly increase computation time. - """ - - particle_rest_shape_contact_exclusion_radius: float = 0.0 - """Rest-configuration separation threshold for filtering close primitives [m]. - - Only used when ``particle_enable_self_contact`` is ``True``. - """ - - rigid_contact_k_start: float = 1.0e2 - """Initial stiffness seed for all rigid body contacts [N/m].""" - - rigid_body_particle_contact_buffer_size: int = 256 - """Per-body capacity of the particle, edge, and face soft-contact list. + """Deprecated global soft-contact configuration. - Contacts past this count are dropped from the body's reaction list: the particles are still - pushed but the body does not recoil, injecting energy. Newton prints ``Per-body particle - contact buffer overflowed N > size`` on overflow; raise this above the observed ``N``. Only - used when VBD integrates the rigid bodies itself, i.e. - :attr:`integrate_with_external_rigid_solver` is ``False``. + Use :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` instead. """ diff --git a/source/isaaclab_contrib/isaaclab_contrib/deformable/vbd_manager.py b/source/isaaclab_contrib/isaaclab_contrib/deformable/vbd_manager.py index ca49ec379816..67db8fb19b0f 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/deformable/vbd_manager.py +++ b/source/isaaclab_contrib/isaaclab_contrib/deformable/vbd_manager.py @@ -3,257 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""VBD Newton manager.""" +"""Compatibility alias for the core VBD Newton manager.""" -from __future__ import annotations +from isaaclab_newton.physics.vbd_manager import NewtonVBDManager -import logging -from typing import TYPE_CHECKING - -import warp as wp -from isaaclab_newton.physics.newton_manager import NewtonManager -from newton import Model -from newton._src.usd.schemas import SchemaResolverNewton, SchemaResolverPhysx -from newton.solvers import SolverVBD - -from isaaclab.sim.utils.stage import get_current_stage - -from .deformable_object import ( - add_deformable_entry_to_builder, - clear_deformable_builder_hooks, - install_deformable_builder_hooks, - setup_registered_deformable_fabric_sync, -) -from .newton_manager_cfg import NewtonModelSolverCfg, VBDSolverCfg - -if TYPE_CHECKING: - from isaaclab.sim.simulation_context import SimulationContext - -logger = logging.getLogger(__name__) - - -def _apply_model_cfg(model: Model) -> None: - """Apply the active solver cfg's :class:`NewtonModelCfg` to the finalized model. - - Sets the model-global ``soft_contact_ke/kd/mu``. Per-shape material defaults - are applied earlier via ``builder.default_shape_cfg``, not here. - """ - from isaaclab.physics import PhysicsManager - - solver_cfg = getattr(PhysicsManager._cfg, "solver_cfg", None) - if not isinstance(solver_cfg, NewtonModelSolverCfg) or solver_cfg.model_cfg is None: - return - - model_cfg = solver_cfg.model_cfg - model.soft_contact_ke = float(model_cfg.soft_contact_ke) - model.soft_contact_kd = float(model_cfg.soft_contact_kd) - model.soft_contact_mu = float(model_cfg.soft_contact_mu) - - -class NewtonVBDManager(NewtonManager): - """:class:`NewtonManager` specialization for the VBD solver. - - Always uses Newton's :class:`CollisionPipeline` for contact handling. - """ - - @classmethod - def initialize(cls, sim_context: SimulationContext) -> None: - """Initialize the manager with simulation context. - - Args: - sim_context: Parent simulation context. - - TODO: Subclass should not override this method, once deformables - supported on Newton import_usd, this can be unified with NewtonManager's - implementation. - """ - - # Deformable body registry and extension hooks. - # Experimental deformable support registers callbacks here so the manager - # and cloner can invoke them without hard-coding deformable logic. - install_deformable_builder_hooks() - - super().initialize(sim_context) - - @classmethod - def _solver_specific_clear(cls): - """Clear VBD-specific state.""" - clear_deformable_builder_hooks() - - @classmethod - def _get_deformable_ignore_paths(cls) -> list[str]: - """Return USD prim paths to skip when calling ``builder.add_usd``. - - For each registered deformable body, both the simulation mesh (which - carries ``UsdPhysics.CollisionAPI``) and the visual mesh are returned. - The sim mesh must be skipped so Newton does not create a redundant - static mesh collider alongside the particles produced by - ``add_soft_mesh``. The visual mesh is skipped so Newton does not - treat it as a collider — Kit reads it directly from USD for rendering. - - Paths may contain regex patterns; Newton's ``add_usd`` matches them - via :func:`re.match`. - """ - paths: list[str] = [] - for entry in cls._deformable_registry: - paths.append(entry.sim_mesh_prim_path) - paths.append(entry.vis_mesh_prim_path) - return paths - - @classmethod - def start_simulation(cls) -> None: - """Start simulation by finalizing model and initializing state. - - This function finalizes the model and initializes the simulation state. - Note: Collision pipeline is initialized later in initialize_solver() after - we determine whether the solver needs external collision detection. - - TODO: Subclass should not override this method, missing piece is - having Newton bind a surface mesh to volume deformable tetrahedral mesh - in addition to removing the deformable_registry data structure. - """ - # Color the replicated builder before finalization. - if cls._builder is not None: - cls._builder.color() - super().start_simulation() - - if cls._model is not None: - _apply_model_cfg(cls._model) - - # Setup USD/Fabric sync for Kit viewport deformable rendering - setup_registered_deformable_fabric_sync(cls) - - @classmethod - def instantiate_builder_from_stage(cls): - """Create builder from USD stage with special treatment for deformable - bodies, as these are not read from USD yet. - - Detects env Xforms (e.g. ``/World/Env_0``, ``/World/Env_1``) and builds - each as a separate Newton world via ``begin_world``/``end_world``. - Falls back to a flat ``add_usd`` when no env Xforms are found. - - TODO: Subclass should not override this method, once deformables - supported on Newton import_usd, this can be unified with NewtonManager's - implementation. - """ - import re - - from pxr import UsdGeom - - stage = get_current_stage() - up_axis = UsdGeom.GetStageUpAxis(stage) - - # Scan /World children for env-like Xforms (Env_0, env_1, ...) - env_pattern = re.compile(r"^[Ee]nv_(\d+)$") - world_prim = stage.GetPrimAtPath("/World") - env_paths: list[tuple[int, str]] = [] - if world_prim and world_prim.IsValid(): - for child in world_prim.GetChildren(): - m = env_pattern.match(child.GetName()) - if m: - env_paths.append((int(m.group(1)), child.GetPath().pathString)) - env_paths.sort(key=lambda x: x[0]) - - builder = cls.create_builder(up_axis=up_axis) - - schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] - - # Deformable sim/visual mesh paths must be skipped by ``add_usd`` - # so they don't get duplicated as static colliders. - deformable_ignore_paths = cls._get_deformable_ignore_paths() - - if not env_paths: - # No env Xforms — flat loading - builder.add_usd(stage, ignore_paths=deformable_ignore_paths, schema_resolvers=schema_resolvers) - - # Add deformable bodies from the registry (single world at origin). - for entry in cls._deformable_registry: - add_deformable_entry_to_builder(builder, entry, 0, [0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]) - else: - # Load everything except the env subtrees (ground plane, lights, etc.) - ignore_paths = [path for _, path in env_paths] + deformable_ignore_paths - builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) - - # Build a prototype from the first env (all envs assumed identical) - _, proto_path = env_paths[0] - proto = cls.create_builder(up_axis=up_axis) - proto.add_usd( - stage, - root_path=proto_path, - ignore_paths=deformable_ignore_paths, - schema_resolvers=schema_resolvers, - ) - - # Inject registered sites into the proto before replication - global_sites, proto_sites, world_sites = cls._cl_inject_sites(builder, {proto_path: proto}) - global_site_map: dict[str, tuple[int, None]] = {label: (idx, None) for label, idx in global_sites.items()} - num_worlds = len(env_paths) - local_site_map: dict[str, list[list[int]]] = {} - site_entries = proto_sites.get(id(proto), {}) - - # Add each env as a separate Newton world - xform_cache = UsdGeom.XformCache() - for col, (_, env_path) in enumerate(env_paths): - builder.begin_world() - offset = builder.shape_count - world_xform = xform_cache.GetLocalToWorldTransform(stage.GetPrimAtPath(env_path)) - translation = world_xform.ExtractTranslation() - rotation = world_xform.ExtractRotationQuat() - pos = (translation[0], translation[1], translation[2]) - quat = ( - rotation.GetImaginary()[0], - rotation.GetImaginary()[1], - rotation.GetImaginary()[2], - rotation.GetReal(), - ) - env_xform = wp.transform(pos, quat) - builder.add_builder(proto, xform=env_xform) - for label, xform in world_sites.items(): - if label not in local_site_map: - local_site_map[label] = [[] for _ in range(num_worlds)] - site_idx = builder.add_site(body=-1, xform=wp.transform_multiply(env_xform, xform), label=label) - local_site_map[label][col].append(site_idx) - for label, proto_shape_indices in site_entries.items(): - if label not in local_site_map: - local_site_map[label] = [[] for _ in range(num_worlds)] - for proto_shape_idx in proto_shape_indices: - local_site_map[label][col].append(offset + proto_shape_idx) - - # Add deformable bodies from the registry into this world. - for entry in cls._deformable_registry: - add_deformable_entry_to_builder(builder, entry, col, list(pos), quat) - - builder.end_world() - - NewtonManager._cl_site_index_map = { - **global_site_map, - **{label: (None, per_world) for label, per_world in local_site_map.items()}, - } - NewtonManager._num_envs = len(env_paths) - - builder.color() - cls.set_builder(builder) - - @classmethod - def _create_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> SolverVBD: - """Construct the configured VBD solver.""" - return SolverVBD(model, **cls._filter_solver_kwargs(SolverVBD, solver_cfg)) - - @classmethod - def _build_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> None: - """Construct :class:`SolverVBD` and populate the base-class slots. - - VBD always uses Newton's :class:`CollisionPipeline` and steps with - separate input/output states, so the flags are fixed. - """ - NewtonManager._solver = cls._create_solver(model, solver_cfg) - NewtonManager._use_single_state = False - NewtonManager._needs_collision_pipeline = True - NewtonManager._supports_rigid_body_force_input = not solver_cfg.integrate_with_external_rigid_solver - - @classmethod - def _simulate_physics_only(cls) -> None: - # Rebuild BVH once per step for solvers that require it (e.g. VBD cloth). - if cls._model.particle_count > 0 and hasattr(cls._solver, "rebuild_bvh"): - cls._solver.rebuild_bvh(cls._state_0) - super()._simulate_physics_only() +__all__ = ["NewtonVBDManager"] diff --git a/source/isaaclab_contrib/test/coupling/test_coupler.py b/source/isaaclab_contrib/test/coupling/test_coupler.py index 5bf2a3fe6dc7..2576d6394e01 100644 --- a/source/isaaclab_contrib/test/coupling/test_coupler.py +++ b/source/isaaclab_contrib/test/coupling/test_coupler.py @@ -558,7 +558,7 @@ def test_coupler_clear_releases_nested_manager_state(monkeypatch): def test_mpm_entry_reuses_builder_lifecycle_hooks(monkeypatch): """Coupled MPM entries register attributes and normalize kinematic colliders.""" events: list[tuple[str, object]] = [] - builder = object() + builder = SimpleNamespace(color=lambda: None) solver_cfg = CouplerProxyCfg( entries=[CouplerEntryCfg(name="media", solver_cfg=MPMSolverCfg())], ) diff --git a/source/isaaclab_contrib/test/custom_coupling/test_manager.py b/source/isaaclab_contrib/test/custom_coupling/test_manager.py index 3ae1d62e809f..b575fc49bc3d 100644 --- a/source/isaaclab_contrib/test/custom_coupling/test_manager.py +++ b/source/isaaclab_contrib/test/custom_coupling/test_manager.py @@ -5,6 +5,7 @@ """Unit tests for the custom coupling manager.""" +from types import SimpleNamespace from unittest.mock import MagicMock import pytest @@ -147,6 +148,26 @@ def test_step_preserves_input_forces(mode: str, monkeypatch: pytest.MonkeyPatch) reactions.assert_not_called() +def test_pre_physics_step_rebuilds_soft_solver(monkeypatch: pytest.MonkeyPatch) -> None: + """Rebuild the real VBD solver through the shared pre-step hook.""" + base_hook = MagicMock() + soft_solver = MagicMock() + state = object() + monkeypatch.setattr( + manager_module.NewtonVBDManager, + "_pre_physics_step", + classmethod(lambda cls: base_hook()), + ) + monkeypatch.setattr(NewtonCoupledMJWarpVBDManager, "_model", SimpleNamespace(particle_count=1)) + monkeypatch.setattr(NewtonCoupledMJWarpVBDManager, "_soft_solver", soft_solver) + monkeypatch.setattr(NewtonCoupledMJWarpVBDManager, "_state_0", state) + + NewtonCoupledMJWarpVBDManager._pre_physics_step() + + base_hook.assert_called_once_with() + soft_solver.rebuild_bvh.assert_called_once_with(state) + + def test_solver_specific_clear_releases_subsolvers(monkeypatch: pytest.MonkeyPatch) -> None: base_clear = MagicMock() monkeypatch.setattr( diff --git a/source/isaaclab_contrib/test/deformable/test_vbd_core_compat.py b/source/isaaclab_contrib/test/deformable/test_vbd_core_compat.py new file mode 100644 index 000000000000..f2bd8a478963 --- /dev/null +++ b/source/isaaclab_contrib/test/deformable/test_vbd_core_compat.py @@ -0,0 +1,138 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Compatibility tests for contrib deformables using core VBD.""" + +from types import SimpleNamespace + +from isaaclab_newton.physics import ( + NewtonManager, + NewtonSoftContactCfg, +) +from isaaclab_newton.physics import ( + VBDSolverCfg as CoreVBDSolverCfg, +) +from isaaclab_newton.physics.vbd_manager import NewtonVBDManager as CoreNewtonVBDManager + +from isaaclab.assets.deformable_object.base_deformable_object import BaseDeformableObject + +import isaaclab_contrib.deformable.deformable_object as deformable_object_module +from isaaclab_contrib.deformable import DeformableObject, VBDSolverCfg +from isaaclab_contrib.deformable.deformable_object import ( + add_registered_deformables_to_builder, + clear_deformable_builder_hooks, + install_deformable_builder_hooks, + setup_registered_deformable_fabric_sync, +) +from isaaclab_contrib.deformable.newton_manager_cfg import NewtonModelCfg +from isaaclab_contrib.deformable.vbd_manager import NewtonVBDManager + + +def _fake_base_init(self, cfg) -> None: + self.cfg = cfg + self._DTYPE_TO_TORCH_TRAILING_DIMS = {} + self._initialize_handle = None + self._invalidate_initialize_handle = None + self._prim_deletion_handle = None + self._debug_vis_handle = None + self._physics_ready_handle = None + + +def test_contrib_vbd_symbols_alias_core_implementations(): + """Retained contrib symbols resolve to their core implementations.""" + assert NewtonModelCfg is NewtonSoftContactCfg + assert VBDSolverCfg is CoreVBDSolverCfg + assert NewtonVBDManager is CoreNewtonVBDManager + + +def test_contrib_soft_contact_alias_uses_core_defaults(): + """The compatibility alias uses the core soft-contact defaults.""" + cfg = NewtonModelCfg() + + assert cfg.soft_contact_ke == 1.0e3 + assert cfg.soft_contact_kd == 10.0 + assert cfg.soft_contact_mu == 0.5 + + +def test_builder_hook_lifecycle_preserves_third_party_state(monkeypatch): + """Hook setup is idempotent and preserves third-party state.""" + + def third_party_per_world(*args): + pass + + def third_party_post_start(*args): + pass + + registry_entry = object() + monkeypatch.setattr(NewtonManager, "_deformable_registry", [registry_entry]) + monkeypatch.setattr(NewtonManager, "_per_world_builder_hooks", [third_party_per_world]) + monkeypatch.setattr(NewtonManager, "_post_start_simulation_hooks", [third_party_post_start]) + + install_deformable_builder_hooks() + install_deformable_builder_hooks() + + assert NewtonManager._deformable_registry == [registry_entry] + assert NewtonManager._per_world_builder_hooks.count(add_registered_deformables_to_builder) == 1 + assert NewtonManager._post_start_simulation_hooks.count(setup_registered_deformable_fabric_sync) == 1 + + clear_deformable_builder_hooks() + + assert NewtonManager._per_world_builder_hooks == [third_party_per_world] + assert NewtonManager._post_start_simulation_hooks == [third_party_post_start] + + +def test_deformable_installs_hooks_before_registration(monkeypatch): + """Asset construction installs hooks before registry mutation.""" + calls = [] + registry_entry = object() + + monkeypatch.setattr(BaseDeformableObject, "__init__", _fake_base_init) + monkeypatch.setattr(deformable_object_module, "install_deformable_builder_hooks", lambda: calls.append("install")) + monkeypatch.setattr( + DeformableObject, + "_register_deformable", + lambda self: calls.append("register") or registry_entry, + ) + + asset = DeformableObject(SimpleNamespace()) + + assert calls == ["install", "register"] + assert asset._registry_entry is registry_entry + + +def test_deformable_registration_preserves_existing_assets(monkeypatch): + """Constructing multiple deformables retains every registry entry.""" + entries = [object(), object()] + + def fake_register(self): + entry = entries[len(NewtonManager._deformable_registry)] + NewtonManager._deformable_registry.append(entry) + return entry + + monkeypatch.setattr(NewtonManager, "_deformable_registry", []) + monkeypatch.setattr(BaseDeformableObject, "__init__", _fake_base_init) + monkeypatch.setattr(DeformableObject, "_register_deformable", fake_register) + + first = DeformableObject(SimpleNamespace()) + second = DeformableObject(SimpleNamespace()) + + assert NewtonManager._deformable_registry == entries + assert first._registry_entry is entries[0] + assert second._registry_entry is entries[1] + + +def test_deformable_cleanup_removes_only_its_registry_entry(monkeypatch): + """Asset cleanup preserves other registered deformables.""" + first_entry = object() + second_entry = object() + asset = object.__new__(DeformableObject) + asset._registry_entry = first_entry + asset._physics_ready_handle = None + monkeypatch.setattr(NewtonManager, "_deformable_registry", [first_entry, second_entry]) + monkeypatch.setattr(BaseDeformableObject, "_clear_callbacks", lambda self: None) + + asset._clear_callbacks() + + assert NewtonManager._deformable_registry == [second_entry] diff --git a/source/isaaclab_newton/changelog.d/mmichelis-vbd-core.minor.rst b/source/isaaclab_newton/changelog.d/mmichelis-vbd-core.minor.rst new file mode 100644 index 000000000000..dc6f6f343b64 --- /dev/null +++ b/source/isaaclab_newton/changelog.d/mmichelis-vbd-core.minor.rst @@ -0,0 +1,23 @@ +Added +^^^^^ + +* Added :class:`~isaaclab_newton.physics.NewtonVBDManager`, + :class:`~isaaclab_newton.physics.VBDSolverCfg`, and + :class:`~isaaclab_newton.physics.NewtonSoftContactCfg` to the core Newton + physics package. + +Changed +^^^^^^^ + +* Changed the VBD soft-contact damping default from ``0.01`` to ``10.0`` + to match Newton. Set ``soft_contact_kd=0.01`` explicitly through + :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` to retain the + previous behavior. + +Deprecated +^^^^^^^^^^ + +* Deprecated ``VBDSolverCfg.model_cfg`` in favor of + :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. Move + :class:`~isaaclab_newton.physics.NewtonSoftContactCfg` to the outer + :class:`~isaaclab_newton.physics.NewtonCfg`. diff --git a/source/isaaclab_newton/isaaclab_newton/physics/__init__.pyi b/source/isaaclab_newton/isaaclab_newton/physics/__init__.pyi index 3e15ec5b1c54..5edd6ce555aa 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/__init__.pyi +++ b/source/isaaclab_newton/isaaclab_newton/physics/__init__.pyi @@ -25,7 +25,10 @@ __all__ = [ "NewtonManager", "NewtonMJWarpManager", "NewtonShapeCfg", + "NewtonSoftContactCfg", "NewtonSolverCfg", + "NewtonVBDManager", + "VBDSolverCfg", "NewtonXPBDManager", "XPBDSolverCfg", ] @@ -53,7 +56,10 @@ from .newton_manager import NewtonManager from .newton_manager_cfg import ( NewtonCfg, NewtonShapeCfg, + NewtonSoftContactCfg, NewtonSolverCfg, ) +from .vbd_manager import NewtonVBDManager +from .vbd_manager_cfg import VBDSolverCfg from .xpbd_manager import NewtonXPBDManager from .xpbd_manager_cfg import XPBDSolverCfg diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 4b4d44d6f768..b65b0657f048 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -499,6 +499,7 @@ class NewtonManager(PhysicsManager): _cl_protos: dict[str, ModelBuilder] = {} _deformable_registry: list = [] _per_world_builder_hooks: list[Callable[[ModelBuilder, int, list[float], list[float]], None]] = [] + _post_start_simulation_hooks: list[Callable[[type[NewtonManager]], None]] = [] @classmethod def initialize(cls, sim_context: SimulationContext) -> None: @@ -1108,6 +1109,7 @@ def clear(cls): NewtonManager._mpm_object_registry = [] NewtonManager._deformable_registry = [] NewtonManager._per_world_builder_hooks = [] + NewtonManager._post_start_simulation_hooks = [] NewtonManager._up_axis = "Z" NewtonManager._scene_data = None NewtonManager._scene_data_mapping = None @@ -1194,6 +1196,11 @@ def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None: The default implementation is a no-op. """ + @classmethod + def _get_usd_ignore_paths(cls) -> list[str]: + """Return prim paths excluded from USD import.""" + return [] + @classmethod def cl_register_site(cls, body_pattern: str | None, xform: wp.transform, *, per_world: bool = False) -> str: """Register a site request for injection into prototypes before replication. @@ -1542,6 +1549,11 @@ def start_simulation(cls) -> None: cls._prepare_builder_for_finalize(cls._builder) with Timer(name="newton_finalize_builder", msg="Finalize builder took:", activity="Finalizing physics model"): NewtonManager._model = cls._builder.finalize(device=device) + cfg = PhysicsManager._cfg + if isinstance(cfg, NewtonCfg) and cfg.soft_contact_cfg is not None: + cls._model.soft_contact_ke = float(cfg.soft_contact_cfg.soft_contact_ke) + cls._model.soft_contact_kd = float(cfg.soft_contact_cfg.soft_contact_kd) + cls._model.soft_contact_mu = float(cfg.soft_contact_cfg.soft_contact_mu) cls._model.set_gravity(cls._gravity_vector) cls._model.num_envs = cls._num_envs @@ -1600,6 +1612,9 @@ def start_simulation(cls) -> None: cls.sync_cables_to_usd() cls.sync_particles_to_usd() + for hook in cls._post_start_simulation_hooks: + hook(cls) + @staticmethod def _initialize_fabric_body_prims(stage, fabric_hierarchy, usdrt, body_bindings: Sequence[tuple[str, int]]) -> None: """Initialize Fabric body prims used by Newton transform sync.""" @@ -1789,6 +1804,7 @@ def instantiate_builder_from_stage(cls): builder = cls.create_builder(up_axis=up_axis) schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] + usd_ignore_paths = cls._get_usd_ignore_paths() # NOTE: None of the add_usd calls below pass joint_ordering or # bodies_follow_joint_ordering, so the live articulation's native @@ -1804,7 +1820,8 @@ def instantiate_builder_from_stage(cls): if not env_paths: # No env Xforms — flat loading - import_result = builder.add_usd(stage, ignore_paths=hf_ignore_paths, schema_resolvers=schema_resolvers) + ignore_paths = [*hf_ignore_paths, *usd_ignore_paths] + import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) replace_newton_builder_shape_colors(builder, stage) NewtonManager._world_xforms = [wp.transform()] @@ -1813,7 +1830,7 @@ def instantiate_builder_from_stage(cls): else: # Load everything except the env subtrees (ground plane, lights, etc.) # and any terrain colliders already added as heightfields above. - ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + usd_ignore_paths import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) replace_newton_builder_shape_colors(builder, stage) @@ -1821,7 +1838,10 @@ def instantiate_builder_from_stage(cls): _, proto_path = env_paths[0] source_builders = {proto_path: cls.create_builder(up_axis=up_axis)} import_result = source_builders[proto_path].add_usd( - stage, root_path=proto_path, schema_resolvers=schema_resolvers + stage, + root_path=proto_path, + ignore_paths=usd_ignore_paths, + schema_resolvers=schema_resolvers, ) _restore_visible_colliders_without_visual_shapes( source_builders[proto_path], stage, import_result["path_shape_map"] @@ -2376,6 +2396,10 @@ def _update_sensors(cls, contacts) -> None: # Composite stepping routines # ------------------------------------------------------------------ + @classmethod + def _pre_physics_step(cls) -> None: + """Run solver-specific work before collision detection.""" + @classmethod def _simulate_full(cls) -> None: """Run ``decimation x (actuators + solver substeps)``, then sensors. @@ -2387,6 +2411,7 @@ def _simulate_full(cls) -> None: contacts = cls._contacts if cls._needs_collision_pipeline else None for _ in range(cls._decimation): + cls._pre_physics_step() if cls._needs_collision_pipeline: cls._collision_pipeline.collide(cls._state_0, cls._contacts) @@ -2408,6 +2433,7 @@ def _simulate_physics_only(cls) -> None: Used when actuators are stepped eagerly outside the graph, or when there are no actuators at all. """ + cls._pre_physics_step() if cls._needs_collision_pipeline: cls._collision_pipeline.collide(cls._state_0, cls._contacts) contacts = cls._contacts diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py index c7f0d59fc270..7608642542c3 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py @@ -8,6 +8,7 @@ from __future__ import annotations import logging +import warnings from typing import TYPE_CHECKING, Literal from isaaclab.physics import PhysicsCfg @@ -53,6 +54,20 @@ class NewtonSolverCfg: """ +@configclass +class NewtonSoftContactCfg: + """Global soft-contact parameters applied to the finalized Newton model.""" + + soft_contact_ke: float = 1.0e3 + """Body-particle and particle self-contact stiffness [N/m].""" + + soft_contact_kd: float = 10.0 + """Body-particle contact damping [N*s/m].""" + + soft_contact_mu: float = 0.5 + """Body-particle contact friction coefficient [dimensionless].""" + + @configclass class NewtonShapeCfg: """Default per-shape collision properties applied to all shapes in a Newton scene. @@ -158,6 +173,12 @@ class NewtonCfg(PhysicsCfg): solver_cfg: NewtonSolverCfg | None = None """Solver configuration. If None (default), MJWarpSolverCfg is used by default.""" + soft_contact_cfg: NewtonSoftContactCfg | None = None + """Global soft-contact parameters applied after model finalization. + + If ``None``, Newton model defaults are preserved. + """ + collision_cfg: NewtonCollisionPipelineCfg | None = None """Newton collision pipeline configuration. @@ -168,6 +189,7 @@ class NewtonCfg(PhysicsCfg): - :class:`KaminoPADMMSolverCfg` or :class:`KaminoDVISolverCfg` with ``use_collision_detector=False``, - :class:`XPBDSolverCfg` (always), + - :class:`VBDSolverCfg` (always), - :class:`FeatherstoneSolverCfg` (always). :class:`~isaaclab_newton.physics.MPMSolverCfg` does not use this pipeline; @@ -247,6 +269,21 @@ def __post_init__(self): from isaaclab_newton.physics.mjwarp_manager_cfg import MJWarpSolverCfg self.solver_cfg = MJWarpSolverCfg() + + legacy_model_cfg = getattr(self.solver_cfg, "model_cfg", None) + if legacy_model_cfg is not None: + if self.soft_contact_cfg is not None: + raise ValueError( + "Cannot set soft-contact configuration through both NewtonCfg.soft_contact_cfg " + "and solver_cfg.model_cfg." + ) + warnings.warn( + "solver_cfg.model_cfg is deprecated; use NewtonCfg.soft_contact_cfg instead.", + DeprecationWarning, + stacklevel=2, + ) + self.soft_contact_cfg = legacy_model_cfg + self.class_type = self.solver_cfg.class_type # Mid-tick re-collide is silently disabled when collision_decimation >= num_substeps. diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py new file mode 100644 index 000000000000..1e396e84cd7b --- /dev/null +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py @@ -0,0 +1,51 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""VBD Newton manager.""" + +from __future__ import annotations + +from newton import Model, ModelBuilder +from newton.solvers import SolverVBD + +from .newton_manager import NewtonManager +from .vbd_manager_cfg import VBDSolverCfg + + +class NewtonVBDManager(NewtonManager): + """Newton manager specialization for the VBD solver.""" + + @classmethod + def _get_usd_ignore_paths(cls) -> list[str]: + """Return registered deformable mesh paths excluded from USD import.""" + return [ + path for entry in cls._deformable_registry for path in (entry.sim_mesh_prim_path, entry.vis_mesh_prim_path) + ] + + @classmethod + def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None: + """Color the builder for VBD.""" + super()._prepare_builder_for_finalize(builder) + builder.color() + + @classmethod + def _create_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> SolverVBD: + """Construct the configured VBD solver.""" + return SolverVBD(model, **cls._filter_solver_kwargs(SolverVBD, solver_cfg)) + + @classmethod + def _build_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> None: + """Construct VBD and configure its base-manager state.""" + NewtonManager._solver = cls._create_solver(model, solver_cfg) + NewtonManager._use_single_state = False + NewtonManager._needs_collision_pipeline = True + NewtonManager._supports_rigid_body_force_input = not solver_cfg.integrate_with_external_rigid_solver + + @classmethod + def _pre_physics_step(cls) -> None: + """Rebuild the VBD particle BVH before collision detection.""" + super()._pre_physics_step() + if cls._model.particle_count > 0 and hasattr(cls._solver, "rebuild_bvh"): + cls._solver.rebuild_bvh(cls._state_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 new file mode 100644 index 000000000000..95a37a01a61f --- /dev/null +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py @@ -0,0 +1,67 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configuration for the Newton VBD solver.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from isaaclab.utils.configclass import configclass + +from .newton_manager_cfg import NewtonSoftContactCfg, NewtonSolverCfg + +if TYPE_CHECKING: + from isaaclab_newton.physics import NewtonManager + + +@configclass +class VBDSolverCfg(NewtonSolverCfg): + """Configuration for the Vertex Block Descent solver.""" + + class_type: type[NewtonManager] | str = "{DIR}.vbd_manager:NewtonVBDManager" + """Manager class for the VBD solver.""" + + model_cfg: NewtonSoftContactCfg | None = None + """Deprecated global soft-contact configuration. + + Use :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` instead. + """ + + iterations: int = 10 + """Number of VBD iterations per substep.""" + + integrate_with_external_rigid_solver: bool = False + """Whether an external solver integrates rigid bodies.""" + + particle_enable_self_contact: bool = False + """Whether to enable particle self-contact.""" + + particle_self_contact_radius: float = 0.005 + """Particle radius used for self-contact detection [m].""" + + particle_self_contact_margin: float = 0.005 + """Self-contact detection margin [m].""" + + particle_collision_detection_interval: int = -1 + """How often particle self-contact detection runs.""" + + particle_vertex_contact_buffer_size: int = 32 + """Preallocation size for each vertex contact buffer.""" + + particle_edge_contact_buffer_size: int = 64 + """Preallocation size for each edge contact buffer.""" + + particle_topological_contact_filter_threshold: int = 2 + """Topological distance below which self-contacts are discarded.""" + + particle_rest_shape_contact_exclusion_radius: float = 0.0 + """Rest-shape separation threshold for filtering contacts [m].""" + + rigid_contact_k_start: float = 1.0e2 + """Initial stiffness seed for rigid-body contacts [N/m].""" + + rigid_body_particle_contact_buffer_size: int = 256 + """Initial capacity of the rigid-body particle-contact buffer.""" diff --git a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py index ffbec53b167b..2c14d7162186 100644 --- a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py +++ b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py @@ -49,11 +49,13 @@ NewtonMPMManager, NewtonShapeCfg, NewtonSolverCfg, + NewtonVBDManager, NewtonXPBDManager, + VBDSolverCfg, XPBDSolverCfg, ) from isaaclab_newton.physics.mpm_manager import _make_solver_config -from newton.solvers import SolverFeatherstone, SolverImplicitMPM, SolverKamino, SolverMuJoCo, SolverXPBD +from newton.solvers import SolverFeatherstone, SolverImplicitMPM, SolverKamino, SolverMuJoCo, SolverVBD, SolverXPBD from isaaclab.physics import PhysicsManager from isaaclab.sim import SimulationCfg, build_simulation_context @@ -89,6 +91,14 @@ True, id="xpbd", ), + pytest.param( + lambda: VBDSolverCfg(), + NewtonVBDManager, + SolverVBD, + False, + True, + id="vbd", + ), pytest.param( lambda: FeatherstoneSolverCfg(), NewtonFeatherstoneManager, @@ -957,7 +967,14 @@ def reset(self, state, world_mask=None, flags=0): @pytest.mark.parametrize( "manager", - [NewtonMJWarpManager, NewtonXPBDManager, NewtonFeatherstoneManager, NewtonKaminoManager, NewtonMPMManager], + [ + NewtonMJWarpManager, + NewtonXPBDManager, + NewtonVBDManager, + NewtonFeatherstoneManager, + NewtonKaminoManager, + NewtonMPMManager, + ], ) def test_subclass_of_newton_manager(manager): """All concrete managers inherit from :class:`NewtonManager`.""" @@ -1024,7 +1041,14 @@ def test_abstract_create_solver_raises(): @pytest.mark.parametrize( "manager", - [NewtonMJWarpManager, NewtonXPBDManager, NewtonFeatherstoneManager, NewtonKaminoManager, NewtonMPMManager], + [ + NewtonMJWarpManager, + NewtonXPBDManager, + NewtonVBDManager, + NewtonFeatherstoneManager, + NewtonKaminoManager, + NewtonMPMManager, + ], ) def test_manager_name_starts_with_newton(manager): """The ``"newton"`` prefix is required by :class:`InteractiveScene` and the @@ -1108,6 +1132,17 @@ def test_initialize_solver_populates_canonical_state( jitter=0.0, radius_mean=0.02, ) + elif expected_solver_cls is SolverVBD: + builder.add_cloth_mesh( + pos=wp.vec3(0.0, 0.0, 0.1), + rot=wp.quat_identity(), + scale=1.0, + vel=wp.vec3(0.0), + vertices=[wp.vec3(0.0, 0.0, 0.0), wp.vec3(0.1, 0.0, 0.0), wp.vec3(0.0, 0.1, 0.0)], + indices=[0, 1, 2], + density=1.0, + particle_radius=0.01, + ) else: # Pre-populate the builder with a minimal scene so MJCF conversion has # something to work with. diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py new file mode 100644 index 000000000000..e2bb6e91b3e8 --- /dev/null +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -0,0 +1,389 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Tests for the core Newton VBD integration.""" + +from __future__ import annotations + +import importlib +import sys +from types import SimpleNamespace + +import pytest +from isaaclab_newton.physics import NewtonCfg, NewtonManager, NewtonSolverCfg + +from isaaclab.utils.configclass import configclass + + +@configclass +class _LegacyModelSolverCfg(NewtonSolverCfg): + """Non-VBD solver config carrying the legacy model field.""" + + model_cfg: object | None = None + + +def test_vbd_symbols_are_exported_from_core(): + """Core exports the VBD manager and config with resolvable names.""" + physics = importlib.import_module("isaaclab_newton.physics") + + assert physics.NewtonVBDManager.__name__ == "NewtonVBDManager" + assert physics.VBDSolverCfg.__name__ == "VBDSolverCfg" + assert physics.VBDSolverCfg().class_type.__name__ == "NewtonVBDManager" + assert issubclass(physics.NewtonVBDManager, NewtonManager) + + +def test_soft_contact_cfg_defaults_match_newton(): + """Soft-contact defaults match the pinned Newton model.""" + physics = importlib.import_module("isaaclab_newton.physics") + cfg = physics.NewtonSoftContactCfg() + + assert cfg.soft_contact_ke == pytest.approx(1.0e3) + assert cfg.soft_contact_kd == pytest.approx(10.0) + assert cfg.soft_contact_mu == pytest.approx(0.5) + assert NewtonCfg().soft_contact_cfg is None + + +def test_legacy_vbd_model_cfg_is_promoted(): + """The core VBD legacy field is promoted with a warning.""" + physics = importlib.import_module("isaaclab_newton.physics") + legacy_cfg = physics.NewtonSoftContactCfg() + + with pytest.warns(DeprecationWarning, match=r"NewtonCfg\.soft_contact_cfg"): + cfg = NewtonCfg(solver_cfg=physics.VBDSolverCfg(model_cfg=legacy_cfg)) + + assert cfg.soft_contact_cfg == cfg.solver_cfg.model_cfg + + +def test_duck_typed_legacy_model_cfg_is_promoted(): + """Legacy promotion is not restricted to VBD config types.""" + physics = importlib.import_module("isaaclab_newton.physics") + legacy_cfg = physics.NewtonSoftContactCfg(soft_contact_ke=321.0) + + with pytest.warns(DeprecationWarning, match=r"NewtonCfg\.soft_contact_cfg"): + cfg = NewtonCfg(solver_cfg=_LegacyModelSolverCfg(model_cfg=legacy_cfg)) + + assert cfg.soft_contact_cfg == cfg.solver_cfg.model_cfg + + +def test_outer_and_legacy_soft_contact_cfg_are_rejected(): + """Setting both soft-contact paths is ambiguous.""" + physics = importlib.import_module("isaaclab_newton.physics") + + with pytest.raises(ValueError, match="soft-contact configuration"): + NewtonCfg( + soft_contact_cfg=physics.NewtonSoftContactCfg(), + solver_cfg=physics.VBDSolverCfg(model_cfg=physics.NewtonSoftContactCfg()), + ) + + +def test_vbd_usd_ignore_paths_include_registered_meshes(monkeypatch): + """VBD excludes registered simulation and visual meshes from USD import.""" + physics = importlib.import_module("isaaclab_newton.physics") + registry = [ + SimpleNamespace(sim_mesh_prim_path="/World/cloth/sim", vis_mesh_prim_path="/World/cloth/visual"), + SimpleNamespace(sim_mesh_prim_path="/World/soft/sim", vis_mesh_prim_path="/World/soft/visual"), + ] + monkeypatch.setattr(physics.NewtonVBDManager, "_deformable_registry", registry) + + assert physics.NewtonVBDManager._get_usd_ignore_paths() == [ + "/World/cloth/sim", + "/World/cloth/visual", + "/World/soft/sim", + "/World/soft/visual", + ] + + +def test_vbd_pre_physics_step_calls_base_and_guards_rebuild(monkeypatch): + """VBD calls the base hook and rebuilds only when supported.""" + physics = importlib.import_module("isaaclab_newton.physics") + base_calls = [] + rebuild_calls = [] + state = object() + model = SimpleNamespace(particle_count=1) + + def base_hook(cls): + base_calls.append(cls) + + class SolverWithRebuild: + def rebuild_bvh(self, solver_state): + rebuild_calls.append(solver_state) + + monkeypatch.setattr(NewtonManager, "_pre_physics_step", classmethod(base_hook)) + monkeypatch.setattr(physics.NewtonVBDManager, "_state_0", state) + monkeypatch.setattr(physics.NewtonVBDManager, "_model", model) + monkeypatch.setattr(physics.NewtonVBDManager, "_solver", SolverWithRebuild()) + + physics.NewtonVBDManager._pre_physics_step() + + model.particle_count = 0 + physics.NewtonVBDManager._pre_physics_step() + + model.particle_count = 1 + monkeypatch.setattr(physics.NewtonVBDManager, "_solver", object()) + physics.NewtonVBDManager._pre_physics_step() + + assert base_calls == [physics.NewtonVBDManager] * 3 + assert rebuild_calls == [state] + + +class _FakeNewtonModel: + def __init__(self, events): + self._events = events + self.soft_contact_ke = 7.0 + self.soft_contact_kd = 8.0 + self.soft_contact_mu = 9.0 + self.body_label = () + self.world_count = 0 + self.articulation_count = 0 + + def set_gravity(self, gravity): + self.gravity = gravity + + def state(self): + self._events.append(("state", self.soft_contact_ke, self.soft_contact_kd, self.soft_contact_mu)) + return object() + + def control(self): + self._events.append("control") + return object() + + +class _FakeNewtonBuilder: + def __init__(self, model): + self._model = model + self.up_axis = None + + def finalize(self, device): + self._model._events.append("finalize") + return self._model + + +def _start_with_fake_model(monkeypatch, soft_contact_cfg, hook=None): + from isaaclab.physics import PhysicsManager + + physics = importlib.import_module("isaaclab_newton.physics") + events = [] + model = _FakeNewtonModel(events) + builder = _FakeNewtonBuilder(model) + + monkeypatch.setattr( + PhysicsManager, + "_cfg", + NewtonCfg(solver_cfg=physics.VBDSolverCfg(), soft_contact_cfg=soft_contact_cfg), + raising=False, + ) + monkeypatch.setattr(PhysicsManager, "_device", "cpu", raising=False) + monkeypatch.setattr(NewtonManager, "_builder", builder, raising=False) + monkeypatch.setattr(NewtonManager, "_up_axis", "Z", raising=False) + monkeypatch.setattr(NewtonManager, "_gravity_vector", (0.0, 0.0, -9.81), raising=False) + monkeypatch.setattr(NewtonManager, "_num_envs", 1, raising=False) + monkeypatch.setattr(NewtonManager, "_clone_physics_only", True, raising=False) + monkeypatch.setattr(NewtonManager, "_pending_extended_state_attributes", set(), raising=False) + monkeypatch.setattr(NewtonManager, "_pending_extended_contact_attributes", set(), raising=False) + monkeypatch.setattr(NewtonManager, "_drain_stale_cuda_error", classmethod(lambda cls: None)) + monkeypatch.setattr(NewtonManager, "_register_builder_attributes", classmethod(lambda cls, value: None)) + monkeypatch.setattr(NewtonManager, "_cl_inject_sites_fallback", classmethod(lambda cls: None)) + monkeypatch.setattr(NewtonManager, "dispatch_event", classmethod(lambda cls, event: None)) + monkeypatch.setattr(NewtonManager, "_post_start_simulation_hooks", [], raising=False) + if hook is not None: + NewtonManager._post_start_simulation_hooks.append(hook) + + NewtonManager.start_simulation() + return model, events + + +@pytest.mark.parametrize( + "soft_contact_cfg, expected", + [ + pytest.param(None, (7.0, 8.0, 9.0), id="preserve_newton_defaults"), + pytest.param( + SimpleNamespace(soft_contact_ke=11.0, soft_contact_kd=12.0, soft_contact_mu=13.0), + (11.0, 12.0, 13.0), + id="apply_outer_cfg", + ), + ], +) +def test_soft_contact_cfg_is_applied_before_state_allocation(monkeypatch, soft_contact_cfg, expected): + """Soft-contact values are finalized before Newton state allocation.""" + model, events = _start_with_fake_model(monkeypatch, soft_contact_cfg) + + assert (model.soft_contact_ke, model.soft_contact_kd, model.soft_contact_mu) == expected + assert events[0] == "finalize" + assert events[1] == ("state", *expected) + assert events[2] == ("state", *expected) + + +def test_post_start_hooks_run_and_clear(monkeypatch): + """Post-start hooks run after allocation and are cleared globally.""" + hook_calls = [] + + def hook(manager): + hook_calls.append(manager) + + _, events = _start_with_fake_model(monkeypatch, None, hook) + + assert hook_calls == [NewtonManager] + assert events[-1] == "control" + NewtonManager.clear() + assert NewtonManager._post_start_simulation_hooks == [] + + +def test_pre_step_hook_precedes_collision_in_both_paths(monkeypatch): + """The pre-step hook runs before collision in both stepping paths.""" + events = [] + monkeypatch.setattr(NewtonManager, "_pre_physics_step", classmethod(lambda cls: events.append("pre"))) + monkeypatch.setattr( + NewtonManager, + "_collision_pipeline", + SimpleNamespace(collide=lambda state, contacts: events.append("collision")), + raising=False, + ) + monkeypatch.setattr( + NewtonManager, "_run_solver_substeps", classmethod(lambda cls, contacts: events.append("solver")) + ) + monkeypatch.setattr(NewtonManager, "_update_sensors", classmethod(lambda cls, contacts: events.append("sensors"))) + monkeypatch.setattr(NewtonManager, "_needs_collision_pipeline", True, raising=False) + monkeypatch.setattr(NewtonManager, "_state_0", object(), raising=False) + monkeypatch.setattr(NewtonManager, "_contacts", object(), raising=False) + monkeypatch.setattr(NewtonManager, "_solver_dt", 0.01, raising=False) + monkeypatch.setattr(NewtonManager, "_num_substeps", 1, raising=False) + monkeypatch.setattr(NewtonManager, "_decimation", 2, raising=False) + monkeypatch.setattr(NewtonManager, "_adapter", None, raising=False) + monkeypatch.setattr(NewtonManager, "_post_actuator_callbacks", [], raising=False) + monkeypatch.setattr(NewtonManager, "_post_step_callbacks", [], raising=False) + + NewtonManager._simulate_full() + assert events == [ + "pre", + "collision", + "solver", + "pre", + "collision", + "solver", + "sensors", + ] + + events.clear() + NewtonManager._simulate_physics_only() + assert events == ["pre", "collision", "solver", "sensors"] + + +class _FakePath: + def __init__(self, value): + self.pathString = value + + +class _FakePrim: + def __init__(self, name): + self._name = name + + def GetName(self): + return self._name + + def GetPath(self): + return _FakePath(f"/World/{self._name}") + + +class _FakeWorldPrim: + def __init__(self, children): + self._children = children + + def IsValid(self): + return True + + def GetChildren(self): + return self._children + + +class _FakeStage: + def __init__(self, env_names): + self._world = _FakeWorldPrim([_FakePrim(name) for name in env_names]) + + def GetPrimAtPath(self, path): + if path == "/World": + return self._world + return _FakePrim(path.rsplit("/", 1)[-1]) + + +class _FakeRotation: + def GetImaginary(self): + return (0.0, 0.0, 0.0) + + def GetReal(self): + return 1.0 + + +class _FakeMatrix: + def ExtractTranslation(self): + return (0.0, 0.0, 0.0) + + def ExtractRotationQuat(self): + return _FakeRotation() + + +class _FakeXformCache: + def GetLocalToWorldTransform(self, prim): + return _FakeMatrix() + + +class _RecordingUsdBuilder: + def __init__(self): + self.calls = [] + + def add_usd(self, stage, **kwargs): + self.calls.append(kwargs) + return {"path_shape_map": {}} + + +@pytest.mark.parametrize("env_names", [[], ["Env_0", "Env_1"]], ids=["flat", "replicated"]) +def test_usd_ignore_paths_are_forwarded_to_all_importers(monkeypatch, env_names): + """Solver ignore paths reach flat, global, and prototype USD imports.""" + manager_module = importlib.import_module("isaaclab_newton.physics.newton_manager") + physics = importlib.import_module("isaaclab_newton.physics") + stage = _FakeStage(env_names) + builders = [] + ignore_paths = ["/World/cloth/sim", "/World/cloth/visual"] + + class FakeUsdGeom: + XformCache = _FakeXformCache + + @staticmethod + def GetStageUpAxis(stage): + return "Z" + + def create_builder(cls, up_axis=None, **kwargs): + builder = _RecordingUsdBuilder() + builders.append(builder) + return builder + + monkeypatch.setitem(sys.modules, "pxr", SimpleNamespace(UsdGeom=FakeUsdGeom)) + monkeypatch.setattr(manager_module, "get_current_stage", lambda: stage) + monkeypatch.setattr(manager_module, "replace_newton_builder_shape_colors", lambda builder, stage: None) + monkeypatch.setattr(manager_module, "replicate_builder_mapping", lambda *args, **kwargs: ({}, [])) + monkeypatch.setattr(physics.NewtonVBDManager, "create_builder", classmethod(create_builder)) + monkeypatch.setattr( + physics.NewtonVBDManager, "_inject_terrain_heightfields", classmethod(lambda cls, stage, builder: []) + ) + monkeypatch.setattr( + physics.NewtonVBDManager, + "_get_usd_ignore_paths", + classmethod(lambda cls: ignore_paths), + ) + monkeypatch.setattr( + physics.NewtonVBDManager, + "_cl_inject_sites", + classmethod(lambda cls, builder, sources: ({}, {}, {})), + ) + monkeypatch.setattr(physics.NewtonVBDManager, "_per_world_builder_hooks", [], raising=False) + + physics.NewtonVBDManager.instantiate_builder_from_stage() + + if not env_names: + assert builders[0].calls[0]["ignore_paths"] == ignore_paths + assert "schema_resolvers" in builders[0].calls[0] + else: + assert builders[0].calls[0]["ignore_paths"] == ["/World/Env_0", "/World/Env_1", *ignore_paths] + assert builders[1].calls[0]["root_path"] == "/World/Env_0" + assert builders[1].calls[0]["ignore_paths"] == ignore_paths diff --git a/source/isaaclab_tasks/changelog.d/mmichelis-vbd-core.rst b/source/isaaclab_tasks/changelog.d/mmichelis-vbd-core.rst new file mode 100644 index 000000000000..11b4aa9ebe6f --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/mmichelis-vbd-core.rst @@ -0,0 +1,8 @@ +Changed +^^^^^^^ + +* Changed the Franka soft-body task configurations to use the core + :class:`~isaaclab_newton.physics.VBDSolverCfg` and + :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. Custom + overrides should move nested ``solver_cfg.model_cfg`` values to the outer + Newton configuration. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py index a5c22a356016..a20cd141a5ab 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py @@ -7,7 +7,13 @@ from __future__ import annotations -from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg, NewtonCollisionPipelineCfg +from isaaclab_newton.physics import ( + MJWarpSolverCfg, + NewtonCfg, + NewtonCollisionPipelineCfg, + NewtonSoftContactCfg, + VBDSolverCfg, +) from isaaclab_newton.sim.schemas import NewtonDeformableBodyPropertiesCfg from isaaclab_newton.sim.spawners.materials import NewtonSurfaceDeformableBodyMaterialCfg from isaaclab_physx.physics import PhysxCfg @@ -25,7 +31,6 @@ from isaaclab.utils.configclass import configclass from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg -from isaaclab_contrib.deformable.newton_manager_cfg import NewtonModelCfg, VBDSolverCfg from isaaclab_tasks.utils import PresetCfg @@ -85,7 +90,11 @@ class PhysicsCfg(PresetCfg): ) ], iterations=1, - model_cfg=NewtonModelCfg(soft_contact_ke=8e3, soft_contact_mu=10.0), + ), + soft_contact_cfg=NewtonSoftContactCfg( + soft_contact_ke=8.0e3, + soft_contact_kd=1.0e-2, + soft_contact_mu=10.0, ), num_substeps=2, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py index 1fe189d3f140..13c14489a1b0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py @@ -11,6 +11,8 @@ MJWarpSolverCfg, NewtonCfg, NewtonCollisionPipelineCfg, + NewtonSoftContactCfg, + VBDSolverCfg, ) from isaaclab_newton.sim.schemas import NewtonDeformableBodyPropertiesCfg from isaaclab_newton.sim.spawners.materials import NewtonDeformableBodyMaterialCfg @@ -47,11 +49,6 @@ CouplerProxyCfg, CouplerProxyMappingCfg, ) -from isaaclab_contrib.deformable.newton_manager_cfg import ( - NewtonModelCfg, - VBDSolverCfg, -) - from isaaclab_tasks.utils import PresetCfg from isaaclab_tasks.utils.presets import MultiBackendRendererCfg @@ -177,7 +174,11 @@ class PhysicsCfg(PresetCfg): ) ], iterations=1, - model_cfg=NewtonModelCfg(soft_contact_ke=8.0e3, soft_contact_mu=10.0), + ), + soft_contact_cfg=NewtonSoftContactCfg( + soft_contact_ke=8.0e3, + soft_contact_kd=1.0e-2, + soft_contact_mu=10.0, ), num_substeps=2, ) From a6cfc0700fd45b4ad6a329403d1ee5ffddafd088 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Wed, 5 Aug 2026 16:22:28 +0200 Subject: [PATCH 02/16] Address VBD core review feedback --- .../isaaclab_contrib.deformable.rst | 47 --- .../experimental-features/bleeding-edge.rst | 9 +- .../migration/migrating_deformables.rst | 28 -- .../physical-backends/newton/index.rst | 6 +- .../newton/using-vbd-solver.rst | 63 +-- .../01_assets/run_deformable_object.rst | 2 +- .../changelog.d/mmichelis-vbd-core.rst | 22 +- .../isaaclab_contrib/coupling/coupler.py | 5 - .../isaaclab_contrib/coupling/coupler_cfg.py | 4 +- .../coupled_mjwarp_vbd_manager.py | 8 +- .../custom_coupling/franka_soft_env_cfg.py | 4 +- .../custom_coupling/newton_manager_cfg.py | 6 +- .../isaaclab_contrib/custom_coupling/tasks.py | 6 +- .../isaaclab_contrib/deformable/__init__.pyi | 10 - .../deformable/deformable_object.py | 18 +- .../deformable/newton_manager_cfg.py | 65 --- .../deformable/vbd_manager.py | 10 - .../test/coupling/test_coupler.py | 6 +- .../test/custom_coupling/test_manager.py | 38 +- .../custom_coupling/test_manual_coupling.py | 4 +- .../test_deformable_builder_hooks.py | 3 +- .../test/deformable/test_deformable_object.py | 4 +- .../test/deformable/test_vbd_core_compat.py | 138 ------- .../changelog.d/mmichelis-vbd-core.minor.rst | 16 - .../isaaclab_newton/physics/newton_manager.py | 32 +- .../physics/newton_manager_cfg.py | 15 - .../isaaclab_newton/physics/vbd_manager.py | 64 ++- .../physics/vbd_manager_cfg.py | 8 +- .../test/assets/test_cable_object.py | 3 +- .../physics/test_newton_fabric_body_sync.py | 4 +- .../test_newton_manager_abstraction.py | 2 + .../test/physics/test_vbd_core.py | 372 ++---------------- .../changelog.d/mmichelis-vbd-core.rst | 4 +- 33 files changed, 143 insertions(+), 883 deletions(-) delete mode 100644 source/isaaclab_contrib/isaaclab_contrib/deformable/newton_manager_cfg.py delete mode 100644 source/isaaclab_contrib/isaaclab_contrib/deformable/vbd_manager.py delete mode 100644 source/isaaclab_contrib/test/deformable/test_vbd_core_compat.py diff --git a/docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst b/docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst index c67f078e0b81..fc8bf8e979e4 100644 --- a/docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst +++ b/docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst @@ -9,11 +9,6 @@ isaaclab_contrib.deformable deformable_object.DeformableObject deformable_object_data.DeformableObjectData - newton_manager_cfg.NewtonModelCfg - newton_manager_cfg.NewtonModelSolverCfg - newton_manager_cfg.VBDSolverCfg - newton_manager_cfg.CoupledMJWarpVBDSolverCfg - vbd_manager.NewtonVBDManager Deformable Object ----------------- @@ -28,45 +23,3 @@ Deformable Object :inherited-members: :show-inheritance: :exclude-members: __init__ - -Newton Solver Configurations ----------------------------- - -.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.NewtonModelSolverCfg - :members: - :show-inheritance: - :exclude-members: __init__ - -.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.CoupledMJWarpVBDSolverCfg - :members: - :show-inheritance: - :exclude-members: __init__ - -Deprecated Compatibility Aliases --------------------------------- - -``NewtonModelCfg`` and ``VBDSolverCfg`` remain available from -``isaaclab_contrib.deformable`` for compatibility. ``NewtonVBDManager`` remains -available from ``isaaclab_contrib.deformable.vbd_manager``. Use -:class:`isaaclab_newton.physics.NewtonSoftContactCfg`, -:class:`isaaclab_newton.physics.VBDSolverCfg`, and -:class:`isaaclab_newton.physics.NewtonVBDManager` for new code. Pass the -soft-contact configuration through -:attr:`isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. - -.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.NewtonModelCfg - :members: - :no-index: - :show-inheritance: - :exclude-members: __init__ - -.. autoclass:: isaaclab_contrib.deformable.newton_manager_cfg.VBDSolverCfg - :members: - :no-index: - :show-inheritance: - :exclude-members: __init__ - -.. autoclass:: isaaclab_contrib.deformable.vbd_manager.NewtonVBDManager - :members: - :no-index: - :show-inheritance: diff --git a/docs/source/experimental-features/bleeding-edge.rst b/docs/source/experimental-features/bleeding-edge.rst index 7fe5725809bc..6332767f62e8 100644 --- a/docs/source/experimental-features/bleeding-edge.rst +++ b/docs/source/experimental-features/bleeding-edge.rst @@ -62,11 +62,10 @@ The following features are currently available in ``isaaclab_contrib``: - Geometric controllers for multirotor attitude, velocity, acceleration, and position tracking on SO(3) (Lee et al.). Suitable for both trajectory following and RL baselines. - API reference: :mod:`~isaaclab_contrib.controllers` - * - **Newton VBD Deformable Objects** - - Extended deformable object support with proxy and custom coupling between MJWarp and VBD. - Standalone VBD is provided by :mod:`isaaclab_newton.physics`. - - API reference: :mod:`~isaaclab_contrib.deformable`, :mod:`~isaaclab_contrib.coupling`, - :mod:`~isaaclab_contrib.custom_coupling` + * - **Newton Rigid-Deformable Coupling** + - Proxy and ADMM coupling between Newton solvers, including MJWarp and VBD. + Custom coupling examples are also available in contrib. + - API reference: :mod:`~isaaclab_contrib.coupling` Contributing ------------ diff --git a/docs/source/migration/migrating_deformables.rst b/docs/source/migration/migrating_deformables.rst index 6ccfe37f377a..0e213385408b 100644 --- a/docs/source/migration/migrating_deformables.rst +++ b/docs/source/migration/migrating_deformables.rst @@ -71,30 +71,6 @@ from the physics backend package: - ``from isaaclab_newton.sim.spawners.materials import NewtonSurfaceDeformableBodyMaterialCfg`` * - ``from isaaclab_physx.assets import DeformableObjectCfg`` - ``from isaaclab.assets import DeformableObjectCfg`` - * - ``from isaaclab_contrib.deformable.vbd_manager import NewtonVBDManager`` - - ``from isaaclab_newton.physics import NewtonVBDManager`` - * - ``from isaaclab_contrib.deformable import VBDSolverCfg`` - - ``from isaaclab_newton.physics import VBDSolverCfg`` - * - ``from isaaclab_contrib.deformable import NewtonModelCfg`` - - ``from isaaclab_newton.physics import NewtonSoftContactCfg`` - -The contrib VBD configuration names remain as compatibility aliases. New code -should use the core imports above. Global soft-contact configuration also moved -from ``solver_cfg.model_cfg`` to the outer -:attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` field. - -The ``soft_contact_kd`` default also changed from ``0.01`` to ``10.0`` to match -Newton. Set ``soft_contact_kd=0.01`` explicitly to retain the previous behavior: - -.. code-block:: python - - from isaaclab_newton.physics import NewtonCfg, NewtonSoftContactCfg, VBDSolverCfg - - physics_cfg = NewtonCfg( - solver_cfg=VBDSolverCfg(iterations=10), - soft_contact_cfg=NewtonSoftContactCfg(soft_contact_kd=0.01), # retain legacy damping - ) - Removed Properties ^^^^^^^^^^^^^^^^^^ @@ -268,10 +244,6 @@ Limitations deformable will raise a ``ValueError``. - **Surface-specific solver properties** (``collision_pair_update_frequency``, ``collision_iteration_multiplier``) have no effect on volume deformables. -- **Newton deformables are experimental.** Standalone VBD configuration and its - manager live in :mod:`isaaclab_newton.physics`. The deformable object - integration and coupled rigid-deformable configurations remain in - :mod:`isaaclab_contrib.deformable`. .. _Omni Physics documentation: https://docs.omniverse.nvidia.com/kit/docs/omni_physics/110.0/dev_guide/deformables/deformable_bodies.html diff --git a/docs/source/overview/core-concepts/physical-backends/newton/index.rst b/docs/source/overview/core-concepts/physical-backends/newton/index.rst index 09dd077c014b..8d07cf6df322 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/index.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/index.rst @@ -35,9 +35,9 @@ the following solver pages: * :doc:`mjwarp-solver` — the primary, validated solver path. * :doc:`kamino-solver` — beta support on selected classic tasks. -* :doc:`using-vbd-solver`: experimental VBD solver for cloth and soft bodies. - Standalone VBD is available through :mod:`isaaclab_newton.physics`; MJWarp and - VBD proxy and ADMM coupling are available through :mod:`isaaclab_contrib.coupling`. +* :doc:`using-vbd-solver`: VBD solver for cloth and soft bodies. Standalone VBD + is available through :mod:`isaaclab_newton.physics`; MJWarp and VBD proxy and + ADMM coupling is available through :mod:`isaaclab_contrib.coupling`. * :doc:`using-mpm`: experimental implicit MPM for particle materials, including rigid-MPM proxy coupling. diff --git a/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst b/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst index d33404498e05..c8bb530f4408 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst @@ -8,21 +8,19 @@ simulation. In Isaac Lab, VBD is enabled by selecting a :class:`~isaaclab_newton.physics.NewtonCfg` whose ``solver_cfg`` is a :class:`~isaaclab_newton.physics.VBDSolverCfg`. -VBD support is experimental. The solver managers, configuration fields, and -recommended tuning values may change while Newton deformable support is under -active development. A task that works with PhysX or with Newton's MuJoCo-Warp -solver may still need deformable assets, materials, contacts, and coupling tuned -before it works well with VBD. +A task that works with PhysX or with Newton's MuJoCo-Warp solver may still need +deformable assets, materials, contacts, and coupling tuned before it works well +with VBD. VBD is usually exposed through a task-specific physics preset rather than a general ``newton_vbd`` preset. Deformable-only scenes can use :class:`~isaaclab_newton.physics.VBDSolverCfg` directly. Robot or -rigid-body scenes can use either: +rigid-body scenes can use: * :class:`~isaaclab_contrib.coupling.CouplerProxyCfg` for the proxy coupling used by the core Franka tasks. -* :class:`~isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg` for the - opt-in shared-model example with custom substep ordering. +* :class:`~isaaclab_contrib.coupling.CouplerAdmmCfg` for linearized ADMM + coupling between named solver entries. Start from a Supported Deformable Task -------------------------------------- @@ -42,8 +40,7 @@ Start from a Supported Deformable Task ./isaaclab.sh -i tetrahedralization -Before adding VBD to a new task, first run one of the experimental Franka -deformable tasks: +Before adding VBD to a new task, first run one of the Franka deformable tasks: .. tab-set:: @@ -88,7 +85,7 @@ Tasks that support multiple physics options usually store ``SimulationCfg.physic as a :class:`~isaaclab_tasks.utils.hydra.PresetCfg`. For deformable Newton tasks, the preset is a plain :class:`~isaaclab_newton.physics.NewtonCfg`. Standalone VBD and soft-contact configuration live in :mod:`isaaclab_newton.physics`, while -proxy and custom solver coupling remain in :mod:`isaaclab_contrib`. +proxy and ADMM coupling live in :mod:`isaaclab_contrib.coupling`. The Franka soft-body and cloth tasks define task-specific proxy presets. @@ -221,45 +218,10 @@ 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. -Custom MJWarp + VBD Parameters ------------------------------- - -The opt-in -:class:`~isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg` runs -MJWarp and VBD over one shared model. Import -:mod:`isaaclab_contrib.custom_coupling.tasks` explicitly before using its -registered task. - -.. list-table:: - :header-rows: 1 - :widths: 30 70 - - * - Parameter - - Description - * - ``rigid_solver_cfg`` - - MJWarp configuration for rigid bodies. - * - ``soft_solver_cfg`` - - VBD configuration. Set ``integrate_with_external_rigid_solver=True`` - so VBD advances only particles. - * - ``coupling_mode="one_way"`` - - Advance rigid bodies first, then particles without rigid reaction - forces. - * - ``coupling_mode="two_way"`` - - Inject particle reactions before MJWarp, then advance VBD with the same - contacts. - -MJWarp ``nconmax`` and ``njmax`` must cover the rigid contacts and constraints -in the scene. ``ccd_iterations`` can affect fast rigid contacts near -deformables. See :doc:`mjwarp-solver` for the rigid-solver parameters. - -Use the custom manager for direct shared-model substep ordering. Use proxy -coupling when deformable contact is localized to selected rigid bodies. - - .. _newton-vbd-proxy-coupling: -Proxy-Coupled MJWarp + VBD --------------------------- +Coupled MJWarp + VBD +-------------------- :class:`~isaaclab_contrib.coupling.CouplerProxyCfg` is the coupling used by the core Franka tasks. It partitions the model between named solver entries and @@ -347,6 +309,11 @@ Key proxy-specific parameters: Body selectors must use full Newton body-label regexes, such as ``/World/envs/env_.*/Robot``. Proxy mappings also accept raw Newton body ids. +:class:`~isaaclab_contrib.coupling.CouplerAdmmCfg` provides linearized ADMM +coupling between the same named solver entries. Set ``contact_pairs`` to select +entry pairs explicitly, or leave it as ``None`` to detect every distinct pair. +Use ``iterations`` and ``rho`` to tune the ADMM solve. + Try the demo: .. tab-set:: diff --git a/docs/source/tutorials/01_assets/run_deformable_object.rst b/docs/source/tutorials/01_assets/run_deformable_object.rst index 0b361cfb67ef..f5ce4aed08ba 100644 --- a/docs/source/tutorials/01_assets/run_deformable_object.rst +++ b/docs/source/tutorials/01_assets/run_deformable_object.rst @@ -14,7 +14,7 @@ surface deformables (cloth), see the deformable demo at ``scripts/demos/deformab The deformable object API and schema define/modify functions are shared across backends, while deformable property and material configuration classes are backend-specific. PhysX simulates soft bodies using the Finite Element Method (FEM); the Newton experimental backend uses the core VBD solver from -:mod:`isaaclab_newton.physics` with the experimental deformable object integration from +:mod:`isaaclab_newton.physics` with the deformable object integration from :mod:`isaaclab_contrib.deformable`. The volume deformable comprises of two tetrahedral meshes -- a simulation mesh and a collision mesh. The simulation mesh is used to simulate the deformations of the soft body, while the collision mesh is used to detect collisions diff --git a/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst index 1949d56af595..4c24c044e3c7 100644 --- a/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst +++ b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst @@ -1,22 +1,6 @@ -Changed +Removed ^^^^^^^ -* Changed standalone VBD classes to compatibility aliases of +* Removed the contributed VBD solver implementation. Use :class:`~isaaclab_newton.physics.NewtonVBDManager` and - :class:`~isaaclab_newton.physics.VBDSolverCfg`. Import these classes from - ``isaaclab_newton.physics`` for new code. -* Changed the VBD soft-contact damping default from ``0.01`` to ``10.0`` - to match Newton. Set ``soft_contact_kd=0.01`` explicitly through - :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` to retain the - previous behavior. - -Deprecated -^^^^^^^^^^ - -* Deprecated ``isaaclab_contrib.deformable.vbd_manager.NewtonVBDManager`` and - ``isaaclab_contrib.deformable.VBDSolverCfg`` in favor of their - :mod:`isaaclab_newton.physics` aliases. -* Deprecated ``isaaclab_contrib.deformable.NewtonModelCfg`` in favor of - :class:`~isaaclab_newton.physics.NewtonSoftContactCfg`. Move the - configuration from ``solver_cfg.model_cfg`` to - :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. + :class:`~isaaclab_newton.physics.VBDSolverCfg` instead. diff --git a/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py b/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py index b1e69b1412ea..9cd385595783 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py +++ b/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py @@ -141,11 +141,6 @@ def _validate_config(cls, solver_cfg: CouplerCfg) -> None: raise ValueError( f"CouplerEntryCfg {entry.name!r} contains a nested CouplerCfg; nested couplers are not supported." ) - if getattr(nested_cfg, "model_cfg", None) is not None: - raise ValueError( - f"CouplerEntryCfg {entry.name!r} sets solver_cfg.model_cfg, but model parameters are global. " - "Set soft_contact_cfg on the outer NewtonCfg instead." - ) manager = nested_cfg.class_type factory = getattr(manager, "_create_solver", None) if not callable(factory) or getattr(factory, "__func__", factory) is NewtonManager._create_solver.__func__: diff --git a/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler_cfg.py index 82c433d97ed3..03c87860aceb 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler_cfg.py +++ b/source/isaaclab_contrib/isaaclab_contrib/coupling/coupler_cfg.py @@ -20,8 +20,6 @@ from isaaclab.utils.configclass import configclass -from ..deformable.newton_manager_cfg import NewtonModelSolverCfg - if TYPE_CHECKING: from isaaclab_newton.physics import NewtonManager from newton import CollisionPipeline @@ -130,7 +128,7 @@ class CouplerProxyMappingCfg: @configclass -class CouplerCfg(NewtonModelSolverCfg): +class CouplerCfg(NewtonSolverCfg): """Base configuration for a Newton experimental coupled solver. Bodies, particles, joints, and shapes may be assigned to at most one diff --git a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/coupled_mjwarp_vbd_manager.py b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/coupled_mjwarp_vbd_manager.py index ace9b6d98271..ddde78d10485 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/coupled_mjwarp_vbd_manager.py +++ b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/coupled_mjwarp_vbd_manager.py @@ -110,11 +110,11 @@ def _solver_specific_clear(cls) -> None: cls._coupling_mode = None @classmethod - def _pre_physics_step(cls) -> None: - """Rebuild the coupled VBD particle BVH before collision detection.""" - super()._pre_physics_step() - if cls._model.particle_count > 0 and hasattr(cls._soft_solver, "rebuild_bvh"): + def _simulate_physics_only(cls) -> None: + # Rebuild the BVH before stepping solvers that require it, such as VBD cloth. + if hasattr(cls._soft_solver, "rebuild_bvh"): cls._soft_solver.rebuild_bvh(cls._state_0) + super()._simulate_physics_only() @classmethod def _step_one_way(cls, state_in: State, state_out: State, control: Control, dt: float) -> None: diff --git a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/franka_soft_env_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/franka_soft_env_cfg.py index 3cacab218fc8..273e45dd8752 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/franka_soft_env_cfg.py +++ b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/franka_soft_env_cfg.py @@ -5,12 +5,10 @@ """Franka soft lifting environment using the custom coupling manager.""" -from isaaclab_newton.physics import MJWarpSolverCfg +from isaaclab_newton.physics import MJWarpSolverCfg, VBDSolverCfg from isaaclab.utils.configclass import configclass -from isaaclab_contrib.deformable.newton_manager_cfg import VBDSolverCfg - from isaaclab_tasks.core.lift.config.franka_soft.franka_soft_env_cfg import FrankaSoftEnvCfg from isaaclab_tasks.core.lift.config.franka_soft.franka_soft_env_cfg import PhysicsCfg as CorePhysicsCfg diff --git a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/newton_manager_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/newton_manager_cfg.py index 6ebac8e6b44b..13729d521bfe 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/newton_manager_cfg.py +++ b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/newton_manager_cfg.py @@ -9,18 +9,16 @@ from typing import TYPE_CHECKING, Literal -from isaaclab_newton.physics import MJWarpSolverCfg +from isaaclab_newton.physics import MJWarpSolverCfg, NewtonSolverCfg, VBDSolverCfg from isaaclab.utils.configclass import configclass -from isaaclab_contrib.deformable.newton_manager_cfg import NewtonModelSolverCfg, VBDSolverCfg - if TYPE_CHECKING: from isaaclab_newton.physics import NewtonManager @configclass -class CoupledMJWarpVBDSolverCfg(NewtonModelSolverCfg): +class CoupledMJWarpVBDSolverCfg(NewtonSolverCfg): """Configuration for the custom MJWarp and VBD coupling manager.""" class_type: type[NewtonManager] | str = "{DIR}.coupled_mjwarp_vbd_manager:NewtonCoupledMJWarpVBDManager" diff --git a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/tasks.py b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/tasks.py index 41a54d156f73..45ddaac048c8 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/tasks.py +++ b/source/isaaclab_contrib/isaaclab_contrib/custom_coupling/tasks.py @@ -5,10 +5,8 @@ """Gym registration for the opt-in custom coupling example. -Registration lives here rather than in the package ``__init__`` so that importing the -example's library modules, including from the deprecated -:class:`~isaaclab_contrib.deformable.CoupledMJWarpVBDSolverCfg`, does not register the -task. +Registration lives here rather than in the package ``__init__`` so importing the +example's library modules does not register the task. """ import gymnasium as gym diff --git a/source/isaaclab_contrib/isaaclab_contrib/deformable/__init__.pyi b/source/isaaclab_contrib/isaaclab_contrib/deformable/__init__.pyi index 385ad5977db5..7b61168e4d9a 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/deformable/__init__.pyi +++ b/source/isaaclab_contrib/isaaclab_contrib/deformable/__init__.pyi @@ -4,19 +4,9 @@ # SPDX-License-Identifier: BSD-3-Clause __all__ = [ - "CoupledMJWarpVBDSolverCfg", "DeformableObject", "DeformableObjectData", - "NewtonModelCfg", - "NewtonModelSolverCfg", - "VBDSolverCfg", ] from .deformable_object import DeformableObject from .deformable_object_data import DeformableObjectData -from .newton_manager_cfg import ( - CoupledMJWarpVBDSolverCfg, - NewtonModelCfg, - NewtonModelSolverCfg, - VBDSolverCfg, -) diff --git a/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py b/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py index 698f87b4944c..c71dec35be40 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py +++ b/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py @@ -240,16 +240,11 @@ def setup_registered_deformable_fabric_sync(manager_cls: type[SimulationManager] def install_deformable_builder_hooks() -> None: """Install deformable builder hooks without removing hooks owned by other extensions.""" - if not hasattr(SimulationManager, "_deformable_registry"): - SimulationManager._deformable_registry = [] + SimulationManager._deformable_registry = [] if not hasattr(SimulationManager, "_per_world_builder_hooks"): SimulationManager._per_world_builder_hooks = [] - if not hasattr(SimulationManager, "_post_start_simulation_hooks"): - SimulationManager._post_start_simulation_hooks = [] if add_registered_deformables_to_builder not in SimulationManager._per_world_builder_hooks: SimulationManager._per_world_builder_hooks.append(add_registered_deformables_to_builder) - if setup_registered_deformable_fabric_sync not in SimulationManager._post_start_simulation_hooks: - SimulationManager._post_start_simulation_hooks.append(setup_registered_deformable_fabric_sync) def clear_deformable_builder_hooks() -> None: @@ -261,12 +256,6 @@ def clear_deformable_builder_hooks() -> None: for hook in SimulationManager._per_world_builder_hooks if hook is not add_registered_deformables_to_builder ] - if hasattr(SimulationManager, "_post_start_simulation_hooks"): - SimulationManager._post_start_simulation_hooks = [ - hook - for hook in SimulationManager._post_start_simulation_hooks - if hook is not setup_registered_deformable_fabric_sync - ] class DeformableObject(BaseDeformableObject): @@ -300,7 +289,6 @@ def __init__(self, cfg: DeformableObjectCfg): self._deformable_type: str | None = None # Read mesh from the spawned USD prim and register in the deformable registry. - install_deformable_builder_hooks() self._registry_entry = self._register_deformable() # Register custom vec6f type for nodal state validation. @@ -1012,10 +1000,6 @@ def _clear_callbacks(self) -> None: if hasattr(self, "_physics_ready_handle") and self._physics_ready_handle is not None: self._physics_ready_handle.deregister() self._physics_ready_handle = None - if hasattr(self, "_registry_entry"): - SimulationManager._deformable_registry = [ - entry for entry in SimulationManager._deformable_registry if entry is not self._registry_entry - ] def _invalidate_initialize_callback(self, event): """Invalidates the scene elements.""" diff --git a/source/isaaclab_contrib/isaaclab_contrib/deformable/newton_manager_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/deformable/newton_manager_cfg.py deleted file mode 100644 index ed3dabcb4968..000000000000 --- a/source/isaaclab_contrib/isaaclab_contrib/deformable/newton_manager_cfg.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Configuration classes for VBD and global Newton model parameters.""" - -from __future__ import annotations - -import warnings -from typing import TYPE_CHECKING, Literal - -from isaaclab_newton.physics import ( - MJWarpSolverCfg, - NewtonSolverCfg, - VBDSolverCfg, -) -from isaaclab_newton.physics import NewtonSoftContactCfg as NewtonModelCfg - -from isaaclab.utils.configclass import configclass - -if TYPE_CHECKING: - from isaaclab_newton.physics import NewtonManager - - -@configclass -class NewtonModelSolverCfg(NewtonSolverCfg): - """Compatibility base for coupled solver configs with legacy model parameters.""" - - model_cfg: NewtonModelCfg | None = None - """Deprecated global soft-contact configuration. - - Use :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` instead. - """ - - -@configclass -class CoupledMJWarpVBDSolverCfg(NewtonModelSolverCfg): - """Deprecated configuration for the coupled MJWarp and VBD solver. - - .. deprecated:: 0.5.0 - Use :class:`isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg`. - """ - - class_type: type[NewtonManager] | str = ( - "isaaclab_contrib.custom_coupling.coupled_mjwarp_vbd_manager:NewtonCoupledMJWarpVBDManager" - ) - """Manager class for the coupled MJWarp and VBD solver.""" - - rigid_solver_cfg: MJWarpSolverCfg = MJWarpSolverCfg() - """Rigid-body sub-solver configuration.""" - - soft_solver_cfg: VBDSolverCfg = VBDSolverCfg(integrate_with_external_rigid_solver=True) - """VBD sub-solver configuration.""" - - coupling_mode: Literal["one_way", "two_way"] = "two_way" - """Coupling direction between the rigid and VBD solvers.""" - - def __post_init__(self) -> None: - warnings.warn( - "isaaclab_contrib.deformable.CoupledMJWarpVBDSolverCfg is deprecated. " - "Use isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/source/isaaclab_contrib/isaaclab_contrib/deformable/vbd_manager.py b/source/isaaclab_contrib/isaaclab_contrib/deformable/vbd_manager.py deleted file mode 100644 index 67db8fb19b0f..000000000000 --- a/source/isaaclab_contrib/isaaclab_contrib/deformable/vbd_manager.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Compatibility alias for the core VBD Newton manager.""" - -from isaaclab_newton.physics.vbd_manager import NewtonVBDManager - -__all__ = ["NewtonVBDManager"] diff --git a/source/isaaclab_contrib/test/coupling/test_coupler.py b/source/isaaclab_contrib/test/coupling/test_coupler.py index 2576d6394e01..c3c28fcc1ba6 100644 --- a/source/isaaclab_contrib/test/coupling/test_coupler.py +++ b/source/isaaclab_contrib/test/coupling/test_coupler.py @@ -25,6 +25,7 @@ MJWarpSolverCfg, MPMSolverCfg, NewtonCollisionPipelineCfg, + VBDSolverCfg, XPBDSolverCfg, ) from isaaclab_newton.physics.newton_manager import NewtonManager @@ -40,8 +41,6 @@ NewtonCouplerManager, coupler, ) -from isaaclab_contrib.deformable.newton_manager_cfg import NewtonModelCfg, VBDSolverCfg -from isaaclab_contrib.deformable.vbd_manager import NewtonVBDManager @dataclass @@ -168,7 +167,6 @@ def test_config_validation_requires_newton_solver_config(): ("solver_cfg", "entry_kwargs", "error_type", "match"), [ (CouplerProxyCfg(), {}, ValueError, "nested CouplerCfg"), - (VBDSolverCfg(model_cfg=NewtonModelCfg()), {}, ValueError, "model parameters are global"), (KaminoPADMMSolverCfg(), {}, NotImplementedError, "FK/reset lifecycle"), ( MPMSolverCfg(project_outside_colliders=True), @@ -558,7 +556,7 @@ def test_coupler_clear_releases_nested_manager_state(monkeypatch): def test_mpm_entry_reuses_builder_lifecycle_hooks(monkeypatch): """Coupled MPM entries register attributes and normalize kinematic colliders.""" events: list[tuple[str, object]] = [] - builder = SimpleNamespace(color=lambda: None) + builder = object() solver_cfg = CouplerProxyCfg( entries=[CouplerEntryCfg(name="media", solver_cfg=MPMSolverCfg())], ) diff --git a/source/isaaclab_contrib/test/custom_coupling/test_manager.py b/source/isaaclab_contrib/test/custom_coupling/test_manager.py index b575fc49bc3d..33033387ccf4 100644 --- a/source/isaaclab_contrib/test/custom_coupling/test_manager.py +++ b/source/isaaclab_contrib/test/custom_coupling/test_manager.py @@ -5,21 +5,14 @@ """Unit tests for the custom coupling manager.""" -from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from isaaclab_newton.physics import MJWarpSolverCfg +from isaaclab_newton.physics import MJWarpSolverCfg, VBDSolverCfg import isaaclab_contrib.custom_coupling.coupled_mjwarp_vbd_manager as manager_module from isaaclab_contrib.custom_coupling.coupled_mjwarp_vbd_manager import NewtonCoupledMJWarpVBDManager from isaaclab_contrib.custom_coupling.newton_manager_cfg import CoupledMJWarpVBDSolverCfg -from isaaclab_contrib.deformable.newton_manager_cfg import ( - CoupledMJWarpVBDSolverCfg as LegacyMJWarpCfg, -) -from isaaclab_contrib.deformable.newton_manager_cfg import ( - VBDSolverCfg, -) def test_reset_forwards_to_both_subsolvers(monkeypatch: pytest.MonkeyPatch) -> None: @@ -106,15 +99,6 @@ def test_build_solver_sets_capabilities(monkeypatch: pytest.MonkeyPatch) -> None assert manager_module.NewtonManager._supports_rigid_body_force_input is True -def test_legacy_mjwarp_solver_config_warns() -> None: - with pytest.warns(DeprecationWarning, match="custom_coupling.CoupledMJWarpVBDSolverCfg"): - solver_cfg = LegacyMJWarpCfg() - - assert solver_cfg.class_type == ( - "isaaclab_contrib.custom_coupling.coupled_mjwarp_vbd_manager:NewtonCoupledMJWarpVBDManager" - ) - - @pytest.mark.parametrize("mode", ["one_way", "two_way"]) def test_step_preserves_input_forces(mode: str, monkeypatch: pytest.MonkeyPatch) -> None: state_in = MagicMock() @@ -148,26 +132,6 @@ def test_step_preserves_input_forces(mode: str, monkeypatch: pytest.MonkeyPatch) reactions.assert_not_called() -def test_pre_physics_step_rebuilds_soft_solver(monkeypatch: pytest.MonkeyPatch) -> None: - """Rebuild the real VBD solver through the shared pre-step hook.""" - base_hook = MagicMock() - soft_solver = MagicMock() - state = object() - monkeypatch.setattr( - manager_module.NewtonVBDManager, - "_pre_physics_step", - classmethod(lambda cls: base_hook()), - ) - monkeypatch.setattr(NewtonCoupledMJWarpVBDManager, "_model", SimpleNamespace(particle_count=1)) - monkeypatch.setattr(NewtonCoupledMJWarpVBDManager, "_soft_solver", soft_solver) - monkeypatch.setattr(NewtonCoupledMJWarpVBDManager, "_state_0", state) - - NewtonCoupledMJWarpVBDManager._pre_physics_step() - - base_hook.assert_called_once_with() - soft_solver.rebuild_bvh.assert_called_once_with(state) - - def test_solver_specific_clear_releases_subsolvers(monkeypatch: pytest.MonkeyPatch) -> None: base_clear = MagicMock() monkeypatch.setattr( diff --git a/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py b/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py index 473d4589c7fa..beff8141dbf5 100644 --- a/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py +++ b/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py @@ -19,7 +19,7 @@ import pytest import torch from isaaclab_newton.assets import Articulation, RigidObject -from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg +from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg, VBDSolverCfg from isaaclab_newton.sim.schemas import NewtonDeformableBodyPropertiesCfg from isaaclab_newton.sim.spawners.materials import NewtonDeformableBodyMaterialCfg @@ -29,7 +29,7 @@ from isaaclab.sim import SimulationCfg, build_simulation_context from isaaclab_contrib.custom_coupling import CoupledMJWarpVBDSolverCfg -from isaaclab_contrib.deformable import DeformableObject, VBDSolverCfg +from isaaclab_contrib.deformable import DeformableObject from isaaclab_assets import FRANKA_PANDA_CFG # isort:skip diff --git a/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py b/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py index db0b40492ffd..ee068dbbce26 100644 --- a/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py +++ b/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py @@ -13,7 +13,7 @@ from isaaclab_newton.physics import NewtonManager from isaaclab_newton.sim.spawners.materials import NewtonDeformableMaterialCfg -from isaaclab_contrib.deformable import DeformableObject, VBDSolverCfg +from isaaclab_contrib.deformable import DeformableObject from isaaclab_contrib.deformable.deformable_object import ( DeformableRegistryEntry, add_deformable_entry_to_builder, @@ -82,7 +82,6 @@ def _vec3_as_tuple(value) -> tuple[float, float, float]: def test_deformable_package_exports_public_symbols(): """Test that deformable symbols are exported from the package root.""" assert DeformableObject.__name__ == "DeformableObject" - assert VBDSolverCfg.__name__ == "VBDSolverCfg" @pytest.mark.parametrize("external_rigid_solver", [False, True]) diff --git a/source/isaaclab_contrib/test/deformable/test_deformable_object.py b/source/isaaclab_contrib/test/deformable/test_deformable_object.py index 1676fd654cec..89ea2ddca4ce 100644 --- a/source/isaaclab_contrib/test/deformable/test_deformable_object.py +++ b/source/isaaclab_contrib/test/deformable/test_deformable_object.py @@ -20,7 +20,7 @@ import torch import warp as wp from flaky import flaky -from isaaclab_newton.physics import NewtonCfg, NewtonManager +from isaaclab_newton.physics import NewtonCfg, NewtonManager, VBDSolverCfg from isaaclab_newton.sim.schemas import NewtonDeformableBodyPropertiesCfg from isaaclab_newton.sim.spawners.materials import ( NewtonDeformableBodyMaterialCfg, @@ -32,8 +32,6 @@ from isaaclab.assets import DeformableObject, DeformableObjectCfg from isaaclab.sim import SimulationCfg, build_simulation_context -from isaaclab_contrib.deformable.newton_manager_cfg import VBDSolverCfg - NEWTON_VBD_CFG = SimulationCfg( physics=NewtonCfg( solver_cfg=VBDSolverCfg(iterations=3), diff --git a/source/isaaclab_contrib/test/deformable/test_vbd_core_compat.py b/source/isaaclab_contrib/test/deformable/test_vbd_core_compat.py deleted file mode 100644 index f2bd8a478963..000000000000 --- a/source/isaaclab_contrib/test/deformable/test_vbd_core_compat.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Compatibility tests for contrib deformables using core VBD.""" - -from types import SimpleNamespace - -from isaaclab_newton.physics import ( - NewtonManager, - NewtonSoftContactCfg, -) -from isaaclab_newton.physics import ( - VBDSolverCfg as CoreVBDSolverCfg, -) -from isaaclab_newton.physics.vbd_manager import NewtonVBDManager as CoreNewtonVBDManager - -from isaaclab.assets.deformable_object.base_deformable_object import BaseDeformableObject - -import isaaclab_contrib.deformable.deformable_object as deformable_object_module -from isaaclab_contrib.deformable import DeformableObject, VBDSolverCfg -from isaaclab_contrib.deformable.deformable_object import ( - add_registered_deformables_to_builder, - clear_deformable_builder_hooks, - install_deformable_builder_hooks, - setup_registered_deformable_fabric_sync, -) -from isaaclab_contrib.deformable.newton_manager_cfg import NewtonModelCfg -from isaaclab_contrib.deformable.vbd_manager import NewtonVBDManager - - -def _fake_base_init(self, cfg) -> None: - self.cfg = cfg - self._DTYPE_TO_TORCH_TRAILING_DIMS = {} - self._initialize_handle = None - self._invalidate_initialize_handle = None - self._prim_deletion_handle = None - self._debug_vis_handle = None - self._physics_ready_handle = None - - -def test_contrib_vbd_symbols_alias_core_implementations(): - """Retained contrib symbols resolve to their core implementations.""" - assert NewtonModelCfg is NewtonSoftContactCfg - assert VBDSolverCfg is CoreVBDSolverCfg - assert NewtonVBDManager is CoreNewtonVBDManager - - -def test_contrib_soft_contact_alias_uses_core_defaults(): - """The compatibility alias uses the core soft-contact defaults.""" - cfg = NewtonModelCfg() - - assert cfg.soft_contact_ke == 1.0e3 - assert cfg.soft_contact_kd == 10.0 - assert cfg.soft_contact_mu == 0.5 - - -def test_builder_hook_lifecycle_preserves_third_party_state(monkeypatch): - """Hook setup is idempotent and preserves third-party state.""" - - def third_party_per_world(*args): - pass - - def third_party_post_start(*args): - pass - - registry_entry = object() - monkeypatch.setattr(NewtonManager, "_deformable_registry", [registry_entry]) - monkeypatch.setattr(NewtonManager, "_per_world_builder_hooks", [third_party_per_world]) - monkeypatch.setattr(NewtonManager, "_post_start_simulation_hooks", [third_party_post_start]) - - install_deformable_builder_hooks() - install_deformable_builder_hooks() - - assert NewtonManager._deformable_registry == [registry_entry] - assert NewtonManager._per_world_builder_hooks.count(add_registered_deformables_to_builder) == 1 - assert NewtonManager._post_start_simulation_hooks.count(setup_registered_deformable_fabric_sync) == 1 - - clear_deformable_builder_hooks() - - assert NewtonManager._per_world_builder_hooks == [third_party_per_world] - assert NewtonManager._post_start_simulation_hooks == [third_party_post_start] - - -def test_deformable_installs_hooks_before_registration(monkeypatch): - """Asset construction installs hooks before registry mutation.""" - calls = [] - registry_entry = object() - - monkeypatch.setattr(BaseDeformableObject, "__init__", _fake_base_init) - monkeypatch.setattr(deformable_object_module, "install_deformable_builder_hooks", lambda: calls.append("install")) - monkeypatch.setattr( - DeformableObject, - "_register_deformable", - lambda self: calls.append("register") or registry_entry, - ) - - asset = DeformableObject(SimpleNamespace()) - - assert calls == ["install", "register"] - assert asset._registry_entry is registry_entry - - -def test_deformable_registration_preserves_existing_assets(monkeypatch): - """Constructing multiple deformables retains every registry entry.""" - entries = [object(), object()] - - def fake_register(self): - entry = entries[len(NewtonManager._deformable_registry)] - NewtonManager._deformable_registry.append(entry) - return entry - - monkeypatch.setattr(NewtonManager, "_deformable_registry", []) - monkeypatch.setattr(BaseDeformableObject, "__init__", _fake_base_init) - monkeypatch.setattr(DeformableObject, "_register_deformable", fake_register) - - first = DeformableObject(SimpleNamespace()) - second = DeformableObject(SimpleNamespace()) - - assert NewtonManager._deformable_registry == entries - assert first._registry_entry is entries[0] - assert second._registry_entry is entries[1] - - -def test_deformable_cleanup_removes_only_its_registry_entry(monkeypatch): - """Asset cleanup preserves other registered deformables.""" - first_entry = object() - second_entry = object() - asset = object.__new__(DeformableObject) - asset._registry_entry = first_entry - asset._physics_ready_handle = None - monkeypatch.setattr(NewtonManager, "_deformable_registry", [first_entry, second_entry]) - monkeypatch.setattr(BaseDeformableObject, "_clear_callbacks", lambda self: None) - - asset._clear_callbacks() - - assert NewtonManager._deformable_registry == [second_entry] diff --git a/source/isaaclab_newton/changelog.d/mmichelis-vbd-core.minor.rst b/source/isaaclab_newton/changelog.d/mmichelis-vbd-core.minor.rst index dc6f6f343b64..d1b695718648 100644 --- a/source/isaaclab_newton/changelog.d/mmichelis-vbd-core.minor.rst +++ b/source/isaaclab_newton/changelog.d/mmichelis-vbd-core.minor.rst @@ -5,19 +5,3 @@ Added :class:`~isaaclab_newton.physics.VBDSolverCfg`, and :class:`~isaaclab_newton.physics.NewtonSoftContactCfg` to the core Newton physics package. - -Changed -^^^^^^^ - -* Changed the VBD soft-contact damping default from ``0.01`` to ``10.0`` - to match Newton. Set ``soft_contact_kd=0.01`` explicitly through - :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` to retain the - previous behavior. - -Deprecated -^^^^^^^^^^ - -* Deprecated ``VBDSolverCfg.model_cfg`` in favor of - :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. Move - :class:`~isaaclab_newton.physics.NewtonSoftContactCfg` to the outer - :class:`~isaaclab_newton.physics.NewtonCfg`. diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index b65b0657f048..ec2c82b6cf2e 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -499,7 +499,6 @@ class NewtonManager(PhysicsManager): _cl_protos: dict[str, ModelBuilder] = {} _deformable_registry: list = [] _per_world_builder_hooks: list[Callable[[ModelBuilder, int, list[float], list[float]], None]] = [] - _post_start_simulation_hooks: list[Callable[[type[NewtonManager]], None]] = [] @classmethod def initialize(cls, sim_context: SimulationContext) -> None: @@ -1109,7 +1108,6 @@ def clear(cls): NewtonManager._mpm_object_registry = [] NewtonManager._deformable_registry = [] NewtonManager._per_world_builder_hooks = [] - NewtonManager._post_start_simulation_hooks = [] NewtonManager._up_axis = "Z" NewtonManager._scene_data = None NewtonManager._scene_data_mapping = None @@ -1196,11 +1194,6 @@ def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None: The default implementation is a no-op. """ - @classmethod - def _get_usd_ignore_paths(cls) -> list[str]: - """Return prim paths excluded from USD import.""" - return [] - @classmethod def cl_register_site(cls, body_pattern: str | None, xform: wp.transform, *, per_world: bool = False) -> str: """Register a site request for injection into prototypes before replication. @@ -1612,9 +1605,6 @@ def start_simulation(cls) -> None: cls.sync_cables_to_usd() cls.sync_particles_to_usd() - for hook in cls._post_start_simulation_hooks: - hook(cls) - @staticmethod def _initialize_fabric_body_prims(stage, fabric_hierarchy, usdrt, body_bindings: Sequence[tuple[str, int]]) -> None: """Initialize Fabric body prims used by Newton transform sync.""" @@ -1775,13 +1765,15 @@ def _inject_terrain_heightfields(cls, stage: Usd.Stage, builder: ModelBuilder) - return ignore_paths @classmethod - def instantiate_builder_from_stage(cls): + def instantiate_builder_from_stage(cls, ignore_paths: Sequence[str] = ()): """Create builder from USD stage. Detects env Xforms (e.g. ``/World/Env_0``, ``/World/Env_1``) and builds each as a separate Newton world via ``begin_world``/``end_world``. Falls back to a flat ``add_usd`` when no env Xforms are found. + Args: + ignore_paths: Additional USD prim paths excluded from import. """ import re @@ -1804,8 +1796,6 @@ def instantiate_builder_from_stage(cls): builder = cls.create_builder(up_axis=up_axis) schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] - usd_ignore_paths = cls._get_usd_ignore_paths() - # NOTE: None of the add_usd calls below pass joint_ordering or # bodies_follow_joint_ordering, so the live articulation's native # joint/body order comes from Newton's ModelBuilder.add_usd defaults @@ -1820,8 +1810,8 @@ def instantiate_builder_from_stage(cls): if not env_paths: # No env Xforms — flat loading - ignore_paths = [*hf_ignore_paths, *usd_ignore_paths] - import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) + import_ignore_paths = [*hf_ignore_paths, *ignore_paths] + import_result = builder.add_usd(stage, ignore_paths=import_ignore_paths, schema_resolvers=schema_resolvers) _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) replace_newton_builder_shape_colors(builder, stage) NewtonManager._world_xforms = [wp.transform()] @@ -1830,8 +1820,8 @@ def instantiate_builder_from_stage(cls): else: # Load everything except the env subtrees (ground plane, lights, etc.) # and any terrain colliders already added as heightfields above. - ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + usd_ignore_paths - import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) + import_ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + list(ignore_paths) + import_result = builder.add_usd(stage, ignore_paths=import_ignore_paths, schema_resolvers=schema_resolvers) _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) replace_newton_builder_shape_colors(builder, stage) @@ -1840,7 +1830,7 @@ def instantiate_builder_from_stage(cls): import_result = source_builders[proto_path].add_usd( stage, root_path=proto_path, - ignore_paths=usd_ignore_paths, + ignore_paths=ignore_paths, schema_resolvers=schema_resolvers, ) _restore_visible_colliders_without_visual_shapes( @@ -2396,10 +2386,6 @@ def _update_sensors(cls, contacts) -> None: # Composite stepping routines # ------------------------------------------------------------------ - @classmethod - def _pre_physics_step(cls) -> None: - """Run solver-specific work before collision detection.""" - @classmethod def _simulate_full(cls) -> None: """Run ``decimation x (actuators + solver substeps)``, then sensors. @@ -2411,7 +2397,6 @@ def _simulate_full(cls) -> None: contacts = cls._contacts if cls._needs_collision_pipeline else None for _ in range(cls._decimation): - cls._pre_physics_step() if cls._needs_collision_pipeline: cls._collision_pipeline.collide(cls._state_0, cls._contacts) @@ -2433,7 +2418,6 @@ def _simulate_physics_only(cls) -> None: Used when actuators are stepped eagerly outside the graph, or when there are no actuators at all. """ - cls._pre_physics_step() if cls._needs_collision_pipeline: cls._collision_pipeline.collide(cls._state_0, cls._contacts) contacts = cls._contacts diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py index 7608642542c3..aaa1ee53777f 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py @@ -8,7 +8,6 @@ from __future__ import annotations import logging -import warnings from typing import TYPE_CHECKING, Literal from isaaclab.physics import PhysicsCfg @@ -270,20 +269,6 @@ def __post_init__(self): self.solver_cfg = MJWarpSolverCfg() - legacy_model_cfg = getattr(self.solver_cfg, "model_cfg", None) - if legacy_model_cfg is not None: - if self.soft_contact_cfg is not None: - raise ValueError( - "Cannot set soft-contact configuration through both NewtonCfg.soft_contact_cfg " - "and solver_cfg.model_cfg." - ) - warnings.warn( - "solver_cfg.model_cfg is deprecated; use NewtonCfg.soft_contact_cfg instead.", - DeprecationWarning, - stacklevel=2, - ) - self.soft_contact_cfg = legacy_model_cfg - self.class_type = self.solver_cfg.class_type # Mid-tick re-collide is silently disabled when collision_decimation >= num_substeps. diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py index 1e396e84cd7b..e1a50f252bbe 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py @@ -7,28 +7,55 @@ from __future__ import annotations -from newton import Model, ModelBuilder +from typing import TYPE_CHECKING + +from newton import Model from newton.solvers import SolverVBD from .newton_manager import NewtonManager from .vbd_manager_cfg import VBDSolverCfg +if TYPE_CHECKING: + from isaaclab.sim.simulation_context import SimulationContext + class NewtonVBDManager(NewtonManager): """Newton manager specialization for the VBD solver.""" @classmethod - def _get_usd_ignore_paths(cls) -> list[str]: - """Return registered deformable mesh paths excluded from USD import.""" - return [ - path for entry in cls._deformable_registry for path in (entry.sim_mesh_prim_path, entry.vis_mesh_prim_path) - ] + def initialize(cls, sim_context: SimulationContext) -> None: + """Initialize VBD deformable integration when contrib is available.""" + try: + from isaaclab_contrib.deformable.deformable_object import install_deformable_builder_hooks + except ModuleNotFoundError as exc: + if exc.name not in {"isaaclab_contrib", "isaaclab_contrib.deformable"}: + raise + else: + install_deformable_builder_hooks() + super().initialize(sim_context) + + @classmethod + def start_simulation(cls) -> None: + """Start simulation and bind registered deformables to Fabric.""" + if cls._builder is not None: + cls._builder.color() + super().start_simulation() + try: + from isaaclab_contrib.deformable.deformable_object import setup_registered_deformable_fabric_sync + except ModuleNotFoundError as exc: + if exc.name not in {"isaaclab_contrib", "isaaclab_contrib.deformable"}: + raise + else: + setup_registered_deformable_fabric_sync(cls) @classmethod - def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None: - """Color the builder for VBD.""" - super()._prepare_builder_for_finalize(builder) - builder.color() + def instantiate_builder_from_stage(cls): + """Create a builder while excluding registered deformable meshes from USD import.""" + ignore_paths = [ + path for entry in cls._deformable_registry for path in (entry.sim_mesh_prim_path, entry.vis_mesh_prim_path) + ] + super().instantiate_builder_from_stage(ignore_paths) + cls._builder.color() @classmethod def _create_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> SolverVBD: @@ -44,8 +71,19 @@ def _build_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> None: NewtonManager._supports_rigid_body_force_input = not solver_cfg.integrate_with_external_rigid_solver @classmethod - def _pre_physics_step(cls) -> None: - """Rebuild the VBD particle BVH before collision detection.""" - super()._pre_physics_step() + def _solver_specific_clear(cls) -> None: + """Clear contrib deformable integration when available.""" + try: + from isaaclab_contrib.deformable.deformable_object import clear_deformable_builder_hooks + except ModuleNotFoundError as exc: + if exc.name not in {"isaaclab_contrib", "isaaclab_contrib.deformable"}: + raise + else: + clear_deformable_builder_hooks() + + @classmethod + def _simulate_physics_only(cls) -> None: + """Rebuild the VBD particle BVH before stepping physics.""" if cls._model.particle_count > 0 and hasattr(cls._solver, "rebuild_bvh"): cls._solver.rebuild_bvh(cls._state_0) + super()._simulate_physics_only() 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 95a37a01a61f..271c20c117f8 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py @@ -11,7 +11,7 @@ from isaaclab.utils.configclass import configclass -from .newton_manager_cfg import NewtonSoftContactCfg, NewtonSolverCfg +from .newton_manager_cfg import NewtonSolverCfg if TYPE_CHECKING: from isaaclab_newton.physics import NewtonManager @@ -24,12 +24,6 @@ class VBDSolverCfg(NewtonSolverCfg): class_type: type[NewtonManager] | str = "{DIR}.vbd_manager:NewtonVBDManager" """Manager class for the VBD solver.""" - model_cfg: NewtonSoftContactCfg | None = None - """Deprecated global soft-contact configuration. - - Use :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg` instead. - """ - iterations: int = 10 """Number of VBD iterations per substep.""" diff --git a/source/isaaclab_newton/test/assets/test_cable_object.py b/source/isaaclab_newton/test/assets/test_cable_object.py index b733e3761f20..3e1d1a667cf9 100644 --- a/source/isaaclab_newton/test/assets/test_cable_object.py +++ b/source/isaaclab_newton/test/assets/test_cable_object.py @@ -14,7 +14,7 @@ pytest.importorskip("newton") from isaaclab_newton.assets import CableObject as NewtonCableObject -from isaaclab_newton.physics import NewtonCfg, XPBDSolverCfg +from isaaclab_newton.physics import NewtonCfg, VBDSolverCfg, XPBDSolverCfg from isaaclab_newton.physics import NewtonManager as SimulationManager import isaaclab.sim as sim_utils @@ -28,7 +28,6 @@ from isaaclab.utils.configclass import configclass from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg -from isaaclab_contrib.deformable import VBDSolverCfg @configclass diff --git a/source/isaaclab_newton/test/physics/test_newton_fabric_body_sync.py b/source/isaaclab_newton/test/physics/test_newton_fabric_body_sync.py index 865e9e725c47..ce2f20015fec 100644 --- a/source/isaaclab_newton/test/physics/test_newton_fabric_body_sync.py +++ b/source/isaaclab_newton/test/physics/test_newton_fabric_body_sync.py @@ -15,7 +15,7 @@ import pytest import torch import warp as wp -from isaaclab_newton.physics import NewtonCfg, NewtonManager, XPBDSolverCfg +from isaaclab_newton.physics import NewtonCfg, NewtonManager, VBDSolverCfg, XPBDSolverCfg from pxr import UsdGeom from usdrt import Gf, Rt @@ -29,8 +29,6 @@ from isaaclab.utils import math as math_utils from isaaclab.utils.configclass import configclass -from isaaclab_contrib.deformable import VBDSolverCfg - @configclass class _RenderSceneCfg(InteractiveSceneCfg): diff --git a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py index 2c14d7162186..01b7720e5e18 100644 --- a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py +++ b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py @@ -135,6 +135,7 @@ RIGID_BODY_FORCE_INPUT_SUPPORT = { NewtonMJWarpManager: True, + NewtonVBDManager: True, NewtonXPBDManager: True, NewtonFeatherstoneManager: True, NewtonKaminoManager: True, @@ -994,6 +995,7 @@ def test_clear_resets_rigid_body_force_capability(monkeypatch): for manager in ( NewtonMJWarpManager, NewtonXPBDManager, + NewtonVBDManager, NewtonFeatherstoneManager, NewtonKaminoManager, NewtonMPMManager, diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index e2bb6e91b3e8..ba006f9877ca 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -8,24 +8,13 @@ from __future__ import annotations import importlib -import sys from types import SimpleNamespace -import pytest -from isaaclab_newton.physics import NewtonCfg, NewtonManager, NewtonSolverCfg - -from isaaclab.utils.configclass import configclass - - -@configclass -class _LegacyModelSolverCfg(NewtonSolverCfg): - """Non-VBD solver config carrying the legacy model field.""" - - model_cfg: object | None = None +from isaaclab_newton.physics import NewtonCfg, NewtonManager def test_vbd_symbols_are_exported_from_core(): - """Core exports the VBD manager and config with resolvable names.""" + """Core exports the VBD manager and configuration.""" physics = importlib.import_module("isaaclab_newton.physics") assert physics.NewtonVBDManager.__name__ == "NewtonVBDManager" @@ -39,351 +28,58 @@ def test_soft_contact_cfg_defaults_match_newton(): physics = importlib.import_module("isaaclab_newton.physics") cfg = physics.NewtonSoftContactCfg() - assert cfg.soft_contact_ke == pytest.approx(1.0e3) - assert cfg.soft_contact_kd == pytest.approx(10.0) - assert cfg.soft_contact_mu == pytest.approx(0.5) + assert cfg.soft_contact_ke == 1.0e3 + assert cfg.soft_contact_kd == 10.0 + assert cfg.soft_contact_mu == 0.5 assert NewtonCfg().soft_contact_cfg is None -def test_legacy_vbd_model_cfg_is_promoted(): - """The core VBD legacy field is promoted with a warning.""" - physics = importlib.import_module("isaaclab_newton.physics") - legacy_cfg = physics.NewtonSoftContactCfg() - - with pytest.warns(DeprecationWarning, match=r"NewtonCfg\.soft_contact_cfg"): - cfg = NewtonCfg(solver_cfg=physics.VBDSolverCfg(model_cfg=legacy_cfg)) - - assert cfg.soft_contact_cfg == cfg.solver_cfg.model_cfg - - -def test_duck_typed_legacy_model_cfg_is_promoted(): - """Legacy promotion is not restricted to VBD config types.""" - physics = importlib.import_module("isaaclab_newton.physics") - legacy_cfg = physics.NewtonSoftContactCfg(soft_contact_ke=321.0) - - with pytest.warns(DeprecationWarning, match=r"NewtonCfg\.soft_contact_cfg"): - cfg = NewtonCfg(solver_cfg=_LegacyModelSolverCfg(model_cfg=legacy_cfg)) - - assert cfg.soft_contact_cfg == cfg.solver_cfg.model_cfg - - -def test_outer_and_legacy_soft_contact_cfg_are_rejected(): - """Setting both soft-contact paths is ambiguous.""" - physics = importlib.import_module("isaaclab_newton.physics") - - with pytest.raises(ValueError, match="soft-contact configuration"): - NewtonCfg( - soft_contact_cfg=physics.NewtonSoftContactCfg(), - solver_cfg=physics.VBDSolverCfg(model_cfg=physics.NewtonSoftContactCfg()), - ) - - -def test_vbd_usd_ignore_paths_include_registered_meshes(monkeypatch): +def test_vbd_excludes_registered_deformable_meshes(monkeypatch): """VBD excludes registered simulation and visual meshes from USD import.""" physics = importlib.import_module("isaaclab_newton.physics") + events = [] registry = [ SimpleNamespace(sim_mesh_prim_path="/World/cloth/sim", vis_mesh_prim_path="/World/cloth/visual"), SimpleNamespace(sim_mesh_prim_path="/World/soft/sim", vis_mesh_prim_path="/World/soft/visual"), ] - monkeypatch.setattr(physics.NewtonVBDManager, "_deformable_registry", registry) - - assert physics.NewtonVBDManager._get_usd_ignore_paths() == [ - "/World/cloth/sim", - "/World/cloth/visual", - "/World/soft/sim", - "/World/soft/visual", - ] - -def test_vbd_pre_physics_step_calls_base_and_guards_rebuild(monkeypatch): - """VBD calls the base hook and rebuilds only when supported.""" - physics = importlib.import_module("isaaclab_newton.physics") - base_calls = [] - rebuild_calls = [] - state = object() - model = SimpleNamespace(particle_count=1) - - def base_hook(cls): - base_calls.append(cls) - - class SolverWithRebuild: - def rebuild_bvh(self, solver_state): - rebuild_calls.append(solver_state) - - monkeypatch.setattr(NewtonManager, "_pre_physics_step", classmethod(base_hook)) - monkeypatch.setattr(physics.NewtonVBDManager, "_state_0", state) - monkeypatch.setattr(physics.NewtonVBDManager, "_model", model) - monkeypatch.setattr(physics.NewtonVBDManager, "_solver", SolverWithRebuild()) - - physics.NewtonVBDManager._pre_physics_step() - - model.particle_count = 0 - physics.NewtonVBDManager._pre_physics_step() - - model.particle_count = 1 - monkeypatch.setattr(physics.NewtonVBDManager, "_solver", object()) - physics.NewtonVBDManager._pre_physics_step() - - assert base_calls == [physics.NewtonVBDManager] * 3 - assert rebuild_calls == [state] - - -class _FakeNewtonModel: - def __init__(self, events): - self._events = events - self.soft_contact_ke = 7.0 - self.soft_contact_kd = 8.0 - self.soft_contact_mu = 9.0 - self.body_label = () - self.world_count = 0 - self.articulation_count = 0 - - def set_gravity(self, gravity): - self.gravity = gravity - - def state(self): - self._events.append(("state", self.soft_contact_ke, self.soft_contact_kd, self.soft_contact_mu)) - return object() - - def control(self): - self._events.append("control") - return object() - - -class _FakeNewtonBuilder: - def __init__(self, model): - self._model = model - self.up_axis = None - - def finalize(self, device): - self._model._events.append("finalize") - return self._model - - -def _start_with_fake_model(monkeypatch, soft_contact_cfg, hook=None): - from isaaclab.physics import PhysicsManager - - physics = importlib.import_module("isaaclab_newton.physics") - events = [] - model = _FakeNewtonModel(events) - builder = _FakeNewtonBuilder(model) - - monkeypatch.setattr( - PhysicsManager, - "_cfg", - NewtonCfg(solver_cfg=physics.VBDSolverCfg(), soft_contact_cfg=soft_contact_cfg), - raising=False, - ) - monkeypatch.setattr(PhysicsManager, "_device", "cpu", raising=False) - monkeypatch.setattr(NewtonManager, "_builder", builder, raising=False) - monkeypatch.setattr(NewtonManager, "_up_axis", "Z", raising=False) - monkeypatch.setattr(NewtonManager, "_gravity_vector", (0.0, 0.0, -9.81), raising=False) - monkeypatch.setattr(NewtonManager, "_num_envs", 1, raising=False) - monkeypatch.setattr(NewtonManager, "_clone_physics_only", True, raising=False) - monkeypatch.setattr(NewtonManager, "_pending_extended_state_attributes", set(), raising=False) - monkeypatch.setattr(NewtonManager, "_pending_extended_contact_attributes", set(), raising=False) - monkeypatch.setattr(NewtonManager, "_drain_stale_cuda_error", classmethod(lambda cls: None)) - monkeypatch.setattr(NewtonManager, "_register_builder_attributes", classmethod(lambda cls, value: None)) - monkeypatch.setattr(NewtonManager, "_cl_inject_sites_fallback", classmethod(lambda cls: None)) - monkeypatch.setattr(NewtonManager, "dispatch_event", classmethod(lambda cls, event: None)) - monkeypatch.setattr(NewtonManager, "_post_start_simulation_hooks", [], raising=False) - if hook is not None: - NewtonManager._post_start_simulation_hooks.append(hook) - - NewtonManager.start_simulation() - return model, events - - -@pytest.mark.parametrize( - "soft_contact_cfg, expected", - [ - pytest.param(None, (7.0, 8.0, 9.0), id="preserve_newton_defaults"), - pytest.param( - SimpleNamespace(soft_contact_ke=11.0, soft_contact_kd=12.0, soft_contact_mu=13.0), - (11.0, 12.0, 13.0), - id="apply_outer_cfg", - ), - ], -) -def test_soft_contact_cfg_is_applied_before_state_allocation(monkeypatch, soft_contact_cfg, expected): - """Soft-contact values are finalized before Newton state allocation.""" - model, events = _start_with_fake_model(monkeypatch, soft_contact_cfg) - - assert (model.soft_contact_ke, model.soft_contact_kd, model.soft_contact_mu) == expected - assert events[0] == "finalize" - assert events[1] == ("state", *expected) - assert events[2] == ("state", *expected) - - -def test_post_start_hooks_run_and_clear(monkeypatch): - """Post-start hooks run after allocation and are cleared globally.""" - hook_calls = [] - - def hook(manager): - hook_calls.append(manager) - - _, events = _start_with_fake_model(monkeypatch, None, hook) - - assert hook_calls == [NewtonManager] - assert events[-1] == "control" - NewtonManager.clear() - assert NewtonManager._post_start_simulation_hooks == [] + def instantiate_builder(cls, ignore_paths=()): + events.append(("import", cls, list(ignore_paths))) + NewtonManager._builder = SimpleNamespace(color=lambda: events.append(("color",))) + monkeypatch.setattr(NewtonManager, "instantiate_builder_from_stage", classmethod(instantiate_builder)) + monkeypatch.setattr(physics.NewtonVBDManager, "_deformable_registry", registry) -def test_pre_step_hook_precedes_collision_in_both_paths(monkeypatch): - """The pre-step hook runs before collision in both stepping paths.""" - events = [] - monkeypatch.setattr(NewtonManager, "_pre_physics_step", classmethod(lambda cls: events.append("pre"))) - monkeypatch.setattr( - NewtonManager, - "_collision_pipeline", - SimpleNamespace(collide=lambda state, contacts: events.append("collision")), - raising=False, - ) - monkeypatch.setattr( - NewtonManager, "_run_solver_substeps", classmethod(lambda cls, contacts: events.append("solver")) - ) - monkeypatch.setattr(NewtonManager, "_update_sensors", classmethod(lambda cls, contacts: events.append("sensors"))) - monkeypatch.setattr(NewtonManager, "_needs_collision_pipeline", True, raising=False) - monkeypatch.setattr(NewtonManager, "_state_0", object(), raising=False) - monkeypatch.setattr(NewtonManager, "_contacts", object(), raising=False) - monkeypatch.setattr(NewtonManager, "_solver_dt", 0.01, raising=False) - monkeypatch.setattr(NewtonManager, "_num_substeps", 1, raising=False) - monkeypatch.setattr(NewtonManager, "_decimation", 2, raising=False) - monkeypatch.setattr(NewtonManager, "_adapter", None, raising=False) - monkeypatch.setattr(NewtonManager, "_post_actuator_callbacks", [], raising=False) - monkeypatch.setattr(NewtonManager, "_post_step_callbacks", [], raising=False) + physics.NewtonVBDManager.instantiate_builder_from_stage() - NewtonManager._simulate_full() assert events == [ - "pre", - "collision", - "solver", - "pre", - "collision", - "solver", - "sensors", + ( + "import", + physics.NewtonVBDManager, + ["/World/cloth/sim", "/World/cloth/visual", "/World/soft/sim", "/World/soft/visual"], + ), + ("color",), ] - events.clear() - NewtonManager._simulate_physics_only() - assert events == ["pre", "collision", "solver", "sensors"] - - -class _FakePath: - def __init__(self, value): - self.pathString = value - - -class _FakePrim: - def __init__(self, name): - self._name = name - - def GetName(self): - return self._name - - def GetPath(self): - return _FakePath(f"/World/{self._name}") - - -class _FakeWorldPrim: - def __init__(self, children): - self._children = children - def IsValid(self): - return True - - def GetChildren(self): - return self._children - - -class _FakeStage: - def __init__(self, env_names): - self._world = _FakeWorldPrim([_FakePrim(name) for name in env_names]) - - def GetPrimAtPath(self, path): - if path == "/World": - return self._world - return _FakePrim(path.rsplit("/", 1)[-1]) - - -class _FakeRotation: - def GetImaginary(self): - return (0.0, 0.0, 0.0) - - def GetReal(self): - return 1.0 - - -class _FakeMatrix: - def ExtractTranslation(self): - return (0.0, 0.0, 0.0) - - def ExtractRotationQuat(self): - return _FakeRotation() - - -class _FakeXformCache: - def GetLocalToWorldTransform(self, prim): - return _FakeMatrix() - - -class _RecordingUsdBuilder: - def __init__(self): - self.calls = [] - - def add_usd(self, stage, **kwargs): - self.calls.append(kwargs) - return {"path_shape_map": {}} - - -@pytest.mark.parametrize("env_names", [[], ["Env_0", "Env_1"]], ids=["flat", "replicated"]) -def test_usd_ignore_paths_are_forwarded_to_all_importers(monkeypatch, env_names): - """Solver ignore paths reach flat, global, and prototype USD imports.""" - manager_module = importlib.import_module("isaaclab_newton.physics.newton_manager") +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") - stage = _FakeStage(env_names) - builders = [] - ignore_paths = ["/World/cloth/sim", "/World/cloth/visual"] - - class FakeUsdGeom: - XformCache = _FakeXformCache + events = [] + state = object() - @staticmethod - def GetStageUpAxis(stage): - return "Z" + class Solver: + def rebuild_bvh(self, solver_state): + events.append(("rebuild", solver_state)) - def create_builder(cls, up_axis=None, **kwargs): - builder = _RecordingUsdBuilder() - builders.append(builder) - return builder + def simulate_physics_only(cls): + events.append(("step", cls)) - monkeypatch.setitem(sys.modules, "pxr", SimpleNamespace(UsdGeom=FakeUsdGeom)) - monkeypatch.setattr(manager_module, "get_current_stage", lambda: stage) - monkeypatch.setattr(manager_module, "replace_newton_builder_shape_colors", lambda builder, stage: None) - monkeypatch.setattr(manager_module, "replicate_builder_mapping", lambda *args, **kwargs: ({}, [])) - monkeypatch.setattr(physics.NewtonVBDManager, "create_builder", classmethod(create_builder)) - monkeypatch.setattr( - physics.NewtonVBDManager, "_inject_terrain_heightfields", classmethod(lambda cls, stage, builder: []) - ) - monkeypatch.setattr( - physics.NewtonVBDManager, - "_get_usd_ignore_paths", - classmethod(lambda cls: ignore_paths), - ) - monkeypatch.setattr( - physics.NewtonVBDManager, - "_cl_inject_sites", - classmethod(lambda cls, builder, sources: ({}, {}, {})), - ) - monkeypatch.setattr(physics.NewtonVBDManager, "_per_world_builder_hooks", [], raising=False) + monkeypatch.setattr(NewtonManager, "_simulate_physics_only", classmethod(simulate_physics_only)) + monkeypatch.setattr(physics.NewtonVBDManager, "_model", SimpleNamespace(particle_count=1)) + monkeypatch.setattr(physics.NewtonVBDManager, "_solver", Solver()) + monkeypatch.setattr(physics.NewtonVBDManager, "_state_0", state) - physics.NewtonVBDManager.instantiate_builder_from_stage() + physics.NewtonVBDManager._simulate_physics_only() - if not env_names: - assert builders[0].calls[0]["ignore_paths"] == ignore_paths - assert "schema_resolvers" in builders[0].calls[0] - else: - assert builders[0].calls[0]["ignore_paths"] == ["/World/Env_0", "/World/Env_1", *ignore_paths] - assert builders[1].calls[0]["root_path"] == "/World/Env_0" - assert builders[1].calls[0]["ignore_paths"] == ignore_paths + assert events == [("rebuild", state), ("step", physics.NewtonVBDManager)] diff --git a/source/isaaclab_tasks/changelog.d/mmichelis-vbd-core.rst b/source/isaaclab_tasks/changelog.d/mmichelis-vbd-core.rst index 11b4aa9ebe6f..802e069f1571 100644 --- a/source/isaaclab_tasks/changelog.d/mmichelis-vbd-core.rst +++ b/source/isaaclab_tasks/changelog.d/mmichelis-vbd-core.rst @@ -3,6 +3,4 @@ Changed * Changed the Franka soft-body task configurations to use the core :class:`~isaaclab_newton.physics.VBDSolverCfg` and - :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. Custom - overrides should move nested ``solver_cfg.model_cfg`` values to the outer - Newton configuration. + :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. From 94fc67326ecb2dd3404d7cfe332d0310153ae077 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Wed, 5 Aug 2026 18:31:55 +0200 Subject: [PATCH 03/16] Restore custom VBD coupling parameters --- .../newton/using-vbd-solver.rst | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst b/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst index c8bb530f4408..cf9a5c4a487a 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst @@ -218,6 +218,41 @@ 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. +Custom MJWarp + VBD Parameters +------------------------------ + +The opt-in +:class:`~isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg` runs +MJWarp and VBD over one shared model. Import +:mod:`isaaclab_contrib.custom_coupling.tasks` explicitly before using its +registered task. + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Parameter + - Description + * - ``rigid_solver_cfg`` + - MJWarp configuration for rigid bodies. + * - ``soft_solver_cfg`` + - VBD configuration. Set ``integrate_with_external_rigid_solver=True`` + so VBD advances only particles. + * - ``coupling_mode="one_way"`` + - Advance rigid bodies first, then particles without rigid reaction + forces. + * - ``coupling_mode="two_way"`` + - Inject particle reactions before MJWarp, then advance VBD with the same + contacts. + +MJWarp ``nconmax`` and ``njmax`` must cover the rigid contacts and constraints +in the scene. ``ccd_iterations`` can affect fast rigid contacts near +deformables. See :doc:`mjwarp-solver` for the rigid-solver parameters. + +Use the custom manager for direct shared-model substep ordering. Use proxy +coupling when deformable contact is localized to selected rigid bodies. + + .. _newton-vbd-proxy-coupling: Coupled MJWarp + VBD From 6ed1ec160515cec938ceff08ca9d65ff304a7519 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Wed, 5 Aug 2026 19:04:26 +0200 Subject: [PATCH 04/16] Localize VBD USD exclusions --- .../isaaclab_newton/physics/newton_manager.py | 17 +-- .../isaaclab_newton/physics/vbd_manager.py | 98 +++++++++++++++- .../test/physics/test_vbd_core.py | 109 ++++++++++++++---- 3 files changed, 190 insertions(+), 34 deletions(-) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index ec2c82b6cf2e..8582be5b8a32 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -1765,15 +1765,13 @@ def _inject_terrain_heightfields(cls, stage: Usd.Stage, builder: ModelBuilder) - return ignore_paths @classmethod - def instantiate_builder_from_stage(cls, ignore_paths: Sequence[str] = ()): + def instantiate_builder_from_stage(cls): """Create builder from USD stage. Detects env Xforms (e.g. ``/World/Env_0``, ``/World/Env_1``) and builds each as a separate Newton world via ``begin_world``/``end_world``. Falls back to a flat ``add_usd`` when no env Xforms are found. - Args: - ignore_paths: Additional USD prim paths excluded from import. """ import re @@ -1796,6 +1794,7 @@ def instantiate_builder_from_stage(cls, ignore_paths: Sequence[str] = ()): builder = cls.create_builder(up_axis=up_axis) schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] + # NOTE: None of the add_usd calls below pass joint_ordering or # bodies_follow_joint_ordering, so the live articulation's native # joint/body order comes from Newton's ModelBuilder.add_usd defaults @@ -1810,8 +1809,7 @@ def instantiate_builder_from_stage(cls, ignore_paths: Sequence[str] = ()): if not env_paths: # No env Xforms — flat loading - import_ignore_paths = [*hf_ignore_paths, *ignore_paths] - import_result = builder.add_usd(stage, ignore_paths=import_ignore_paths, schema_resolvers=schema_resolvers) + import_result = builder.add_usd(stage, ignore_paths=hf_ignore_paths, schema_resolvers=schema_resolvers) _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) replace_newton_builder_shape_colors(builder, stage) NewtonManager._world_xforms = [wp.transform()] @@ -1820,18 +1818,15 @@ def instantiate_builder_from_stage(cls, ignore_paths: Sequence[str] = ()): else: # Load everything except the env subtrees (ground plane, lights, etc.) # and any terrain colliders already added as heightfields above. - import_ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + list(ignore_paths) - import_result = builder.add_usd(stage, ignore_paths=import_ignore_paths, schema_resolvers=schema_resolvers) + ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) replace_newton_builder_shape_colors(builder, stage) _, proto_path = env_paths[0] source_builders = {proto_path: cls.create_builder(up_axis=up_axis)} import_result = source_builders[proto_path].add_usd( - stage, - root_path=proto_path, - ignore_paths=ignore_paths, - schema_resolvers=schema_resolvers, + stage, root_path=proto_path, schema_resolvers=schema_resolvers ) _restore_visible_colliders_without_visual_shapes( source_builders[proto_path], stage, import_result["path_shape_map"] diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py index e1a50f252bbe..a66ef63719ff 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py @@ -7,10 +7,24 @@ from __future__ import annotations +import re from typing import TYPE_CHECKING +import torch +import warp as wp from newton import Model from newton.solvers import SolverVBD +from newton.usd import SchemaResolverNewton, SchemaResolverPhysx + +from pxr import UsdGeom + +from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors +from isaaclab.sim.utils.stage import get_current_stage + +from isaaclab_newton.cloner.newton_clone_utils import ( + _restore_visible_colliders_without_visual_shapes, + replicate_builder_mapping, +) from .newton_manager import NewtonManager from .vbd_manager_cfg import VBDSolverCfg @@ -51,11 +65,89 @@ def start_simulation(cls) -> None: @classmethod def instantiate_builder_from_stage(cls): """Create a builder while excluding registered deformable meshes from USD import.""" - ignore_paths = [ + stage = get_current_stage() + up_axis = UsdGeom.GetStageUpAxis(stage) + + env_pattern = re.compile(r"^[Ee]nv_(\d+)$") + world_prim = stage.GetPrimAtPath("/World") + env_paths: list[tuple[int, str]] = [] + if world_prim and world_prim.IsValid(): + for child in world_prim.GetChildren(): + match = env_pattern.match(child.GetName()) + if match: + env_paths.append((int(match.group(1)), child.GetPath().pathString)) + env_paths.sort(key=lambda x: x[0]) + + builder = cls.create_builder(up_axis=up_axis) + schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] + deformable_ignore_paths = [ path for entry in cls._deformable_registry for path in (entry.sim_mesh_prim_path, entry.vis_mesh_prim_path) ] - super().instantiate_builder_from_stage(ignore_paths) - cls._builder.color() + hf_ignore_paths = cls._inject_terrain_heightfields(stage, builder) + + if not env_paths: + ignore_paths = [*hf_ignore_paths, *deformable_ignore_paths] + import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) + _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) + replace_newton_builder_shape_colors(builder, stage) + NewtonManager._world_xforms = [wp.transform()] + for hook in cls._per_world_builder_hooks: + hook(builder, 0, [0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]) + else: + ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + deformable_ignore_paths + import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) + _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) + replace_newton_builder_shape_colors(builder, stage) + + _, proto_path = env_paths[0] + source_builders = {proto_path: cls.create_builder(up_axis=up_axis)} + import_result = source_builders[proto_path].add_usd( + stage, + root_path=proto_path, + ignore_paths=deformable_ignore_paths, + schema_resolvers=schema_resolvers, + ) + _restore_visible_colliders_without_visual_shapes( + source_builders[proto_path], stage, import_result["path_shape_map"] + ) + replace_newton_builder_shape_colors(source_builders[proto_path], stage) + cls._cl_protos = source_builders + + global_site_indices, source_site_indices, env_root_sites = cls._cl_inject_sites(builder, source_builders) + xform_cache = UsdGeom.XformCache() + poses = [] + for _, env_path in env_paths: + world_xform = xform_cache.GetLocalToWorldTransform(stage.GetPrimAtPath(env_path)) + translation = world_xform.ExtractTranslation() + rotation = world_xform.ExtractRotationQuat() + imag = rotation.GetImaginary() + poses.append( + ( + (translation[0], translation[1], translation[2]), + (imag[0], imag[1], imag[2], rotation.GetReal()), + ) + ) + + positions = torch.tensor([pos for pos, _ in poses], dtype=torch.float32) + quaternions = torch.tensor([quat for _, quat in poses], dtype=torch.float32) + mapping = torch.ones((1, len(env_paths)), dtype=torch.bool) + replicate_args = (builder, (proto_path,), mapping, positions, quaternions, source_builders) + local_site_map, world_xforms = replicate_builder_mapping( + *replicate_args, + source_site_indices=source_site_indices, + env_root_sites=env_root_sites, + per_world_builder_hooks=cls._per_world_builder_hooks, + ) + + NewtonManager._cl_site_index_map = {label: (idx, None) for label, idx in global_site_indices.items()} + NewtonManager._cl_site_index_map.update( + (label, (None, per_world)) for label, per_world in local_site_map.items() + ) + NewtonManager._world_xforms = world_xforms + NewtonManager._num_envs = len(env_paths) + + builder.color() + cls.set_builder(builder) @classmethod def _create_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> SolverVBD: diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index ba006f9877ca..413f214bae2c 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -10,6 +10,7 @@ import importlib from types import SimpleNamespace +import pytest from isaaclab_newton.physics import NewtonCfg, NewtonManager @@ -34,32 +35,100 @@ def test_soft_contact_cfg_defaults_match_newton(): assert NewtonCfg().soft_contact_cfg is None -def test_vbd_excludes_registered_deformable_meshes(monkeypatch): +@pytest.mark.parametrize("env_paths", [(), ("/World/Env_0", "/World/Env_1")], ids=["flat", "replicated"]) +def test_vbd_excludes_registered_deformable_meshes(monkeypatch, env_paths): """VBD excludes registered simulation and visual meshes from USD import.""" physics = importlib.import_module("isaaclab_newton.physics") - events = [] - registry = [ - SimpleNamespace(sim_mesh_prim_path="/World/cloth/sim", vis_mesh_prim_path="/World/cloth/visual"), - SimpleNamespace(sim_mesh_prim_path="/World/soft/sim", vis_mesh_prim_path="/World/soft/visual"), + vbd_module = importlib.import_module("isaaclab_newton.physics.vbd_manager") + builders = [] + hook_calls = [] + replicate_calls = [] + selected_builders = [] + + class Builder: + def __init__(self): + self.imports = [] + self.colored = False + + def add_usd(self, stage, *, root_path=None, ignore_paths=(), schema_resolvers=()): + self.imports.append((root_path, list(ignore_paths))) + return {"path_shape_map": {}} + + def color(self): + self.colored = True + + children = [ + SimpleNamespace( + GetName=lambda path=path: path.rsplit("/", 1)[-1], + GetPath=lambda path=path: SimpleNamespace(pathString=path), + ) + for path in env_paths ] - - def instantiate_builder(cls, ignore_paths=()): - events.append(("import", cls, list(ignore_paths))) - NewtonManager._builder = SimpleNamespace(color=lambda: events.append(("color",))) - - monkeypatch.setattr(NewtonManager, "instantiate_builder_from_stage", classmethod(instantiate_builder)) - monkeypatch.setattr(physics.NewtonVBDManager, "_deformable_registry", registry) + world_prim = SimpleNamespace(IsValid=lambda: True, GetChildren=lambda: children) + stage = SimpleNamespace(GetPrimAtPath=lambda path: world_prim if path == "/World" else path) + rotation = SimpleNamespace(GetImaginary=lambda: (0.0, 0.0, 0.0), GetReal=lambda: 1.0) + matrix = SimpleNamespace(ExtractTranslation=lambda: (0.0, 0.0, 0.0), ExtractRotationQuat=lambda: rotation) + usd_geom = SimpleNamespace( + GetStageUpAxis=lambda stage: "Z", + XformCache=lambda: SimpleNamespace(GetLocalToWorldTransform=lambda prim: matrix), + ) + + def create_builder(cls, *, up_axis): + builder = Builder() + builders.append(builder) + return builder + + def replicate(*args, **kwargs): + replicate_calls.append(kwargs) + return {}, [object() for _ in env_paths] + + monkeypatch.setattr(vbd_module, "get_current_stage", lambda: stage) + monkeypatch.setattr(vbd_module, "UsdGeom", usd_geom) + monkeypatch.setattr(vbd_module, "_restore_visible_colliders_without_visual_shapes", lambda *args: None) + monkeypatch.setattr(vbd_module, "replace_newton_builder_shape_colors", lambda *args: None) + monkeypatch.setattr(vbd_module, "replicate_builder_mapping", replicate) + monkeypatch.setattr(physics.NewtonVBDManager, "create_builder", classmethod(create_builder)) + monkeypatch.setattr( + physics.NewtonVBDManager, + "_inject_terrain_heightfields", + classmethod(lambda cls, stage, builder: ["/World/terrain"]), + ) + monkeypatch.setattr( + physics.NewtonVBDManager, + "_cl_inject_sites", + classmethod(lambda cls, builder, source_builders: ({}, {}, {})), + ) + monkeypatch.setattr( + physics.NewtonVBDManager, + "set_builder", + classmethod(lambda cls, builder: selected_builders.append(builder)), + ) + + def hook(builder, world_idx, position, rotation): + hook_calls.append(world_idx) + + monkeypatch.setattr(physics.NewtonVBDManager, "_per_world_builder_hooks", [hook]) + monkeypatch.setattr( + physics.NewtonVBDManager, + "_deformable_registry", + [SimpleNamespace(sim_mesh_prim_path="/World/soft/sim", vis_mesh_prim_path="/World/soft/visual")], + ) + monkeypatch.setattr(NewtonManager, "_cl_site_index_map", {}) + monkeypatch.setattr(NewtonManager, "_world_xforms", []) + monkeypatch.setattr(NewtonManager, "_num_envs", 0) physics.NewtonVBDManager.instantiate_builder_from_stage() - assert events == [ - ( - "import", - physics.NewtonVBDManager, - ["/World/cloth/sim", "/World/cloth/visual", "/World/soft/sim", "/World/soft/visual"], - ), - ("color",), - ] + deformable_paths = ["/World/soft/sim", "/World/soft/visual"] + if env_paths: + assert builders[0].imports == [(None, [*env_paths, "/World/terrain", *deformable_paths])] + assert builders[1].imports == [("/World/Env_0", deformable_paths)] + assert replicate_calls[0]["per_world_builder_hooks"] == [hook] + else: + assert builders[0].imports == [(None, ["/World/terrain", *deformable_paths])] + assert hook_calls == [0] + assert selected_builders == [builders[0]] + assert builders[0].colored def test_vbd_rebuilds_particle_bvh_before_physics_step(monkeypatch): From d6d94044c1530d6263d6bad356f3103e5dc68200 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Thu, 6 Aug 2026 10:40:13 +0200 Subject: [PATCH 05/16] Restore VBD feature documentation --- docs/source/experimental-features/bleeding-edge.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/experimental-features/bleeding-edge.rst b/docs/source/experimental-features/bleeding-edge.rst index 6332767f62e8..48e0937f74a1 100644 --- a/docs/source/experimental-features/bleeding-edge.rst +++ b/docs/source/experimental-features/bleeding-edge.rst @@ -62,10 +62,10 @@ The following features are currently available in ``isaaclab_contrib``: - Geometric controllers for multirotor attitude, velocity, acceleration, and position tracking on SO(3) (Lee et al.). Suitable for both trajectory following and RL baselines. - API reference: :mod:`~isaaclab_contrib.controllers` - * - **Newton Rigid-Deformable Coupling** - - Proxy and ADMM coupling between Newton solvers, including MJWarp and VBD. - Custom coupling examples are also available in contrib. - - API reference: :mod:`~isaaclab_contrib.coupling` + * - **Newton VBD Deformable Objects** + - Extended deformable object support using the Newton physics backend with Vertex Block Descent (VBD), + including proxy coupling between MJWarp and VBD. + - API reference: :mod:`~isaaclab_contrib.deformable`, :mod:`~isaaclab_contrib.coupling` Contributing ------------ From 29c627b2c187dcf4c02a166a693f17b23b3f4584 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Thu, 6 Aug 2026 11:18:24 +0200 Subject: [PATCH 06/16] Clarify VBD migration changelog --- .../changelog.d/mmichelis-vbd-core.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst index 4c24c044e3c7..e36f80b106a0 100644 --- a/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst +++ b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst @@ -1,6 +1,12 @@ -Removed +Changed ^^^^^^^ -* Removed the contributed VBD solver implementation. Use +* **Breaking:** Moved the standalone VBD solver from + ``isaaclab_contrib.deformable`` to :mod:`isaaclab_newton.physics`. Import :class:`~isaaclab_newton.physics.NewtonVBDManager` and - :class:`~isaaclab_newton.physics.VBDSolverCfg` instead. + :class:`~isaaclab_newton.physics.VBDSolverCfg` from their new location, and + move ``NewtonModelCfg`` and ``NewtonModelSolverCfg`` soft-contact settings to + :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. +* **Breaking:** Moved ``CoupledMJWarpVBDSolverCfg`` from + ``isaaclab_contrib.deformable`` to + :class:`~isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg`. From 4da34d5cadb227d079ac03bde5ef415c3a1af5fb Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Thu, 6 Aug 2026 11:21:36 +0200 Subject: [PATCH 07/16] Correct coupled VBD migration note --- source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst index e36f80b106a0..c7cd90e35e9c 100644 --- a/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst +++ b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst @@ -7,6 +7,7 @@ Changed :class:`~isaaclab_newton.physics.VBDSolverCfg` from their new location, and move ``NewtonModelCfg`` and ``NewtonModelSolverCfg`` soft-contact settings to :attr:`~isaaclab_newton.physics.NewtonCfg.soft_contact_cfg`. -* **Breaking:** Moved ``CoupledMJWarpVBDSolverCfg`` from - ``isaaclab_contrib.deformable`` to - :class:`~isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg`. +* **Breaking:** Removed the + ``isaaclab_contrib.deformable.CoupledMJWarpVBDSolverCfg`` compatibility + alias. Import + :class:`~isaaclab_contrib.custom_coupling.CoupledMJWarpVBDSolverCfg` instead. From 123c00c70f537f1258137ad77f57ee1a56265d90 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Wed, 12 Aug 2026 16:15:14 +0200 Subject: [PATCH 08/16] Share Newton stage import flow Add a narrow solver hook for USD import exclusions so VBD can reuse the generic stage importer. Move VBD coloring to finalization preparation and retain focused import-path coverage. --- .../isaaclab_newton/physics/newton_manager.py | 17 ++- .../isaaclab_newton/physics/vbd_manager.py | 106 ++---------------- .../test/physics/test_vbd_core.py | 34 ++++-- 3 files changed, 45 insertions(+), 112 deletions(-) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 8582be5b8a32..17eface9b9ea 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -1764,6 +1764,11 @@ def _inject_terrain_heightfields(cls, stage: Usd.Stage, builder: ModelBuilder) - ignore_paths.append(prim.GetPath().pathString) return ignore_paths + @classmethod + def _get_usd_import_ignore_paths(cls) -> list[str]: + """Return solver-specific prim paths excluded from USD import.""" + return [] + @classmethod def instantiate_builder_from_stage(cls): """Create builder from USD stage. @@ -1806,10 +1811,13 @@ def instantiate_builder_from_stage(cls): # constants in lockstep or MJWarp resolution will silently diverge # from the live backend. hf_ignore_paths = cls._inject_terrain_heightfields(stage, builder) + solver_ignore_paths = cls._get_usd_import_ignore_paths() if not env_paths: # No env Xforms — flat loading - import_result = builder.add_usd(stage, ignore_paths=hf_ignore_paths, schema_resolvers=schema_resolvers) + import_result = builder.add_usd( + stage, ignore_paths=[*hf_ignore_paths, *solver_ignore_paths], schema_resolvers=schema_resolvers + ) _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) replace_newton_builder_shape_colors(builder, stage) NewtonManager._world_xforms = [wp.transform()] @@ -1818,7 +1826,7 @@ def instantiate_builder_from_stage(cls): else: # Load everything except the env subtrees (ground plane, lights, etc.) # and any terrain colliders already added as heightfields above. - ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + solver_ignore_paths import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) replace_newton_builder_shape_colors(builder, stage) @@ -1826,7 +1834,10 @@ def instantiate_builder_from_stage(cls): _, proto_path = env_paths[0] source_builders = {proto_path: cls.create_builder(up_axis=up_axis)} import_result = source_builders[proto_path].add_usd( - stage, root_path=proto_path, schema_resolvers=schema_resolvers + stage, + root_path=proto_path, + ignore_paths=solver_ignore_paths, + schema_resolvers=schema_resolvers, ) _restore_visible_colliders_without_visual_shapes( source_builders[proto_path], stage, import_result["path_shape_map"] diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py index a66ef63719ff..70e56f913fbd 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py @@ -7,24 +7,10 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING -import torch -import warp as wp -from newton import Model +from newton import Model, ModelBuilder from newton.solvers import SolverVBD -from newton.usd import SchemaResolverNewton, SchemaResolverPhysx - -from pxr import UsdGeom - -from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors -from isaaclab.sim.utils.stage import get_current_stage - -from isaaclab_newton.cloner.newton_clone_utils import ( - _restore_visible_colliders_without_visual_shapes, - replicate_builder_mapping, -) from .newton_manager import NewtonManager from .vbd_manager_cfg import VBDSolverCfg @@ -51,8 +37,6 @@ def initialize(cls, sim_context: SimulationContext) -> None: @classmethod def start_simulation(cls) -> None: """Start simulation and bind registered deformables to Fabric.""" - if cls._builder is not None: - cls._builder.color() super().start_simulation() try: from isaaclab_contrib.deformable.deformable_object import setup_registered_deformable_fabric_sync @@ -63,91 +47,17 @@ def start_simulation(cls) -> None: setup_registered_deformable_fabric_sync(cls) @classmethod - def instantiate_builder_from_stage(cls): - """Create a builder while excluding registered deformable meshes from USD import.""" - stage = get_current_stage() - up_axis = UsdGeom.GetStageUpAxis(stage) - - env_pattern = re.compile(r"^[Ee]nv_(\d+)$") - world_prim = stage.GetPrimAtPath("/World") - env_paths: list[tuple[int, str]] = [] - if world_prim and world_prim.IsValid(): - for child in world_prim.GetChildren(): - match = env_pattern.match(child.GetName()) - if match: - env_paths.append((int(match.group(1)), child.GetPath().pathString)) - env_paths.sort(key=lambda x: x[0]) - - builder = cls.create_builder(up_axis=up_axis) - schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] - deformable_ignore_paths = [ + def _get_usd_import_ignore_paths(cls) -> list[str]: + """Return registered deformable mesh paths excluded from USD import.""" + return [ path for entry in cls._deformable_registry for path in (entry.sim_mesh_prim_path, entry.vis_mesh_prim_path) ] - hf_ignore_paths = cls._inject_terrain_heightfields(stage, builder) - - if not env_paths: - ignore_paths = [*hf_ignore_paths, *deformable_ignore_paths] - import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) - _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) - replace_newton_builder_shape_colors(builder, stage) - NewtonManager._world_xforms = [wp.transform()] - for hook in cls._per_world_builder_hooks: - hook(builder, 0, [0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]) - else: - ignore_paths = [path for _, path in env_paths] + hf_ignore_paths + deformable_ignore_paths - import_result = builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) - _restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"]) - replace_newton_builder_shape_colors(builder, stage) - - _, proto_path = env_paths[0] - source_builders = {proto_path: cls.create_builder(up_axis=up_axis)} - import_result = source_builders[proto_path].add_usd( - stage, - root_path=proto_path, - ignore_paths=deformable_ignore_paths, - schema_resolvers=schema_resolvers, - ) - _restore_visible_colliders_without_visual_shapes( - source_builders[proto_path], stage, import_result["path_shape_map"] - ) - replace_newton_builder_shape_colors(source_builders[proto_path], stage) - cls._cl_protos = source_builders - - global_site_indices, source_site_indices, env_root_sites = cls._cl_inject_sites(builder, source_builders) - xform_cache = UsdGeom.XformCache() - poses = [] - for _, env_path in env_paths: - world_xform = xform_cache.GetLocalToWorldTransform(stage.GetPrimAtPath(env_path)) - translation = world_xform.ExtractTranslation() - rotation = world_xform.ExtractRotationQuat() - imag = rotation.GetImaginary() - poses.append( - ( - (translation[0], translation[1], translation[2]), - (imag[0], imag[1], imag[2], rotation.GetReal()), - ) - ) - - positions = torch.tensor([pos for pos, _ in poses], dtype=torch.float32) - quaternions = torch.tensor([quat for _, quat in poses], dtype=torch.float32) - mapping = torch.ones((1, len(env_paths)), dtype=torch.bool) - replicate_args = (builder, (proto_path,), mapping, positions, quaternions, source_builders) - local_site_map, world_xforms = replicate_builder_mapping( - *replicate_args, - source_site_indices=source_site_indices, - env_root_sites=env_root_sites, - per_world_builder_hooks=cls._per_world_builder_hooks, - ) - - NewtonManager._cl_site_index_map = {label: (idx, None) for label, idx in global_site_indices.items()} - NewtonManager._cl_site_index_map.update( - (label, (None, per_world)) for label, per_world in local_site_map.items() - ) - NewtonManager._world_xforms = world_xforms - NewtonManager._num_envs = len(env_paths) + @classmethod + def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None: + """Color the completed VBD builder before finalization.""" + super()._prepare_builder_for_finalize(builder) builder.color() - cls.set_builder(builder) @classmethod def _create_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> SolverVBD: diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index 413f214bae2c..4b0b097c3764 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -39,7 +39,8 @@ def test_soft_contact_cfg_defaults_match_newton(): def test_vbd_excludes_registered_deformable_meshes(monkeypatch, env_paths): """VBD excludes registered simulation and visual meshes from USD import.""" physics = importlib.import_module("isaaclab_newton.physics") - vbd_module = importlib.import_module("isaaclab_newton.physics.vbd_manager") + pxr = importlib.import_module("pxr") + newton_module = importlib.import_module("isaaclab_newton.physics.newton_manager") builders = [] hook_calls = [] replicate_calls = [] @@ -48,15 +49,11 @@ def test_vbd_excludes_registered_deformable_meshes(monkeypatch, env_paths): class Builder: def __init__(self): self.imports = [] - self.colored = False def add_usd(self, stage, *, root_path=None, ignore_paths=(), schema_resolvers=()): self.imports.append((root_path, list(ignore_paths))) return {"path_shape_map": {}} - def color(self): - self.colored = True - children = [ SimpleNamespace( GetName=lambda path=path: path.rsplit("/", 1)[-1], @@ -82,11 +79,11 @@ def replicate(*args, **kwargs): replicate_calls.append(kwargs) return {}, [object() for _ in env_paths] - monkeypatch.setattr(vbd_module, "get_current_stage", lambda: stage) - monkeypatch.setattr(vbd_module, "UsdGeom", usd_geom) - monkeypatch.setattr(vbd_module, "_restore_visible_colliders_without_visual_shapes", lambda *args: None) - monkeypatch.setattr(vbd_module, "replace_newton_builder_shape_colors", lambda *args: None) - monkeypatch.setattr(vbd_module, "replicate_builder_mapping", replicate) + monkeypatch.setattr(newton_module, "get_current_stage", lambda: stage) + monkeypatch.setattr(pxr, "UsdGeom", usd_geom) + monkeypatch.setattr(newton_module, "_restore_visible_colliders_without_visual_shapes", lambda *args: None) + monkeypatch.setattr(newton_module, "replace_newton_builder_shape_colors", lambda *args: None) + monkeypatch.setattr(newton_module, "replicate_builder_mapping", replicate) monkeypatch.setattr(physics.NewtonVBDManager, "create_builder", classmethod(create_builder)) monkeypatch.setattr( physics.NewtonVBDManager, @@ -128,7 +125,22 @@ def hook(builder, world_idx, position, rotation): assert builders[0].imports == [(None, ["/World/terrain", *deformable_paths])] assert hook_calls == [0] assert selected_builders == [builders[0]] - assert builders[0].colored + + +def test_vbd_colors_builder_before_finalize(): + """VBD colors the completed builder during finalization preparation.""" + physics = importlib.import_module("isaaclab_newton.physics") + + class Builder: + color_calls = 0 + + def color(self): + self.color_calls += 1 + + builder = Builder() + physics.NewtonVBDManager._prepare_builder_for_finalize(builder) + + assert builder.color_calls == 1 def test_vbd_rebuilds_particle_bvh_before_physics_step(monkeypatch): From 38039fe6c56bde5fc7c76a65da824c2697c73d79 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Wed, 12 Aug 2026 17:00:12 +0200 Subject: [PATCH 09/16] Restore VBD builder coloring flow Keep stage import shared while preserving separate coloring for stage-created and prebuilt VBD builders. --- .../isaaclab_newton/physics/vbd_manager.py | 18 +++++---- .../test/physics/test_vbd_core.py | 40 +++++++++++++------ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py index 70e56f913fbd..e20100d709e7 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING -from newton import Model, ModelBuilder +from newton import Model from newton.solvers import SolverVBD from .newton_manager import NewtonManager @@ -37,6 +37,8 @@ def initialize(cls, sim_context: SimulationContext) -> None: @classmethod def start_simulation(cls) -> None: """Start simulation and bind registered deformables to Fabric.""" + if cls._builder is not None: + cls._builder.color() super().start_simulation() try: from isaaclab_contrib.deformable.deformable_object import setup_registered_deformable_fabric_sync @@ -46,6 +48,14 @@ def start_simulation(cls) -> None: else: setup_registered_deformable_fabric_sync(cls) + @classmethod + def instantiate_builder_from_stage(cls) -> None: + """Create and color the VBD builder from the USD stage.""" + super().instantiate_builder_from_stage() + if cls._builder is None: + raise RuntimeError("Newton stage import did not create a builder.") + cls._builder.color() + @classmethod def _get_usd_import_ignore_paths(cls) -> list[str]: """Return registered deformable mesh paths excluded from USD import.""" @@ -53,12 +63,6 @@ def _get_usd_import_ignore_paths(cls) -> list[str]: path for entry in cls._deformable_registry for path in (entry.sim_mesh_prim_path, entry.vis_mesh_prim_path) ] - @classmethod - def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None: - """Color the completed VBD builder before finalization.""" - super()._prepare_builder_for_finalize(builder) - builder.color() - @classmethod def _create_solver(cls, model: Model, solver_cfg: VBDSolverCfg) -> SolverVBD: """Construct the configured VBD solver.""" diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index 4b0b097c3764..f63aa618895c 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -49,11 +49,15 @@ def test_vbd_excludes_registered_deformable_meshes(monkeypatch, env_paths): class Builder: def __init__(self): self.imports = [] + self.color_calls = 0 def add_usd(self, stage, *, root_path=None, ignore_paths=(), schema_resolvers=()): self.imports.append((root_path, list(ignore_paths))) return {"path_shape_map": {}} + def color(self): + self.color_calls += 1 + children = [ SimpleNamespace( GetName=lambda path=path: path.rsplit("/", 1)[-1], @@ -95,11 +99,12 @@ def replicate(*args, **kwargs): "_cl_inject_sites", classmethod(lambda cls, builder, source_builders: ({}, {}, {})), ) - monkeypatch.setattr( - physics.NewtonVBDManager, - "set_builder", - classmethod(lambda cls, builder: selected_builders.append(builder)), - ) + + def set_builder(cls, builder): + selected_builders.append(builder) + cls._builder = builder + + monkeypatch.setattr(physics.NewtonVBDManager, "set_builder", classmethod(set_builder)) def hook(builder, world_idx, position, rotation): hook_calls.append(world_idx) @@ -110,6 +115,7 @@ def hook(builder, world_idx, position, rotation): "_deformable_registry", [SimpleNamespace(sim_mesh_prim_path="/World/soft/sim", vis_mesh_prim_path="/World/soft/visual")], ) + monkeypatch.setattr(physics.NewtonVBDManager, "_builder", None) monkeypatch.setattr(NewtonManager, "_cl_site_index_map", {}) monkeypatch.setattr(NewtonManager, "_world_xforms", []) monkeypatch.setattr(NewtonManager, "_num_envs", 0) @@ -126,21 +132,31 @@ def hook(builder, world_idx, position, rotation): assert hook_calls == [0] assert selected_builders == [builders[0]] + assert builders[0].color_calls == 1 -def test_vbd_colors_builder_before_finalize(): - """VBD colors the completed builder during finalization preparation.""" + +def test_vbd_colors_prebuilt_builder_before_start(monkeypatch): + """VBD colors a prebuilt builder before starting simulation.""" physics = importlib.import_module("isaaclab_newton.physics") + deformable_module = importlib.import_module("isaaclab_contrib.deformable.deformable_object") + events = [] class Builder: - color_calls = 0 - def color(self): - self.color_calls += 1 + events.append("color") builder = Builder() - physics.NewtonVBDManager._prepare_builder_for_finalize(builder) + monkeypatch.setattr(physics.NewtonVBDManager, "_builder", builder) + monkeypatch.setattr(NewtonManager, "start_simulation", classmethod(lambda cls: events.append("start"))) + monkeypatch.setattr( + deformable_module, + "setup_registered_deformable_fabric_sync", + lambda manager_cls: events.append("sync"), + ) + + physics.NewtonVBDManager.start_simulation() - assert builder.color_calls == 1 + assert events == ["color", "start", "sync"] def test_vbd_rebuilds_particle_bvh_before_physics_step(monkeypatch): From 32dfdc4b0505b1252990cd7d0d6f345f3eb39df4 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Wed, 12 Aug 2026 18:28:49 +0200 Subject: [PATCH 10/16] Clarify VBD collision interval Document the accepted interval values and trim redundant VBD coloring test scaffolding. --- .../physics/vbd_manager_cfg.py | 6 ++++- .../test/physics/test_vbd_core.py | 23 +++++-------------- 2 files changed, 11 insertions(+), 18 deletions(-) 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 271c20c117f8..f3afea053f2d 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py @@ -40,7 +40,11 @@ class VBDSolverCfg(NewtonSolverCfg): """Self-contact detection margin [m].""" particle_collision_detection_interval: int = -1 - """How often particle self-contact detection runs.""" + """How often particle self-contact detection is applied. + + ``< 0``: once before initialization. ``0``: once before and once after + initialization. ``k >= 1``: before every ``k`` VBD iterations. + """ particle_vertex_contact_buffer_size: int = 32 """Preallocation size for each vertex contact buffer.""" diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index f63aa618895c..4455ca59a7d4 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -44,7 +44,6 @@ def test_vbd_excludes_registered_deformable_meshes(monkeypatch, env_paths): builders = [] hook_calls = [] replicate_calls = [] - selected_builders = [] class Builder: def __init__(self): @@ -99,12 +98,9 @@ def replicate(*args, **kwargs): "_cl_inject_sites", classmethod(lambda cls, builder, source_builders: ({}, {}, {})), ) - - def set_builder(cls, builder): - selected_builders.append(builder) - cls._builder = builder - - monkeypatch.setattr(physics.NewtonVBDManager, "set_builder", classmethod(set_builder)) + monkeypatch.setattr( + physics.NewtonVBDManager, "set_builder", classmethod(lambda cls, builder: setattr(cls, "_builder", builder)) + ) def hook(builder, world_idx, position, rotation): hook_calls.append(world_idx) @@ -130,8 +126,6 @@ def hook(builder, world_idx, position, rotation): else: assert builders[0].imports == [(None, ["/World/terrain", *deformable_paths])] assert hook_calls == [0] - assert selected_builders == [builders[0]] - assert builders[0].color_calls == 1 @@ -145,18 +139,13 @@ class Builder: def color(self): events.append("color") - builder = Builder() - monkeypatch.setattr(physics.NewtonVBDManager, "_builder", builder) + monkeypatch.setattr(physics.NewtonVBDManager, "_builder", Builder()) monkeypatch.setattr(NewtonManager, "start_simulation", classmethod(lambda cls: events.append("start"))) - monkeypatch.setattr( - deformable_module, - "setup_registered_deformable_fabric_sync", - lambda manager_cls: events.append("sync"), - ) + monkeypatch.setattr(deformable_module, "setup_registered_deformable_fabric_sync", lambda manager_cls: None) physics.NewtonVBDManager.start_simulation() - assert events == ["color", "start", "sync"] + assert events == ["color", "start"] def test_vbd_rebuilds_particle_bvh_before_physics_step(monkeypatch): From 5c516a9d17f1c777ba6363b78073ef799c6ba2e3 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Wed, 12 Aug 2026 18:54:57 +0200 Subject: [PATCH 11/16] Document Newton contact mixing Restore the effective stiffness and friction formulas for body-particle soft contacts. --- .../isaaclab_newton/physics/newton_manager_cfg.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py index aaa1ee53777f..e97f604e16de 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager_cfg.py @@ -58,13 +58,21 @@ class NewtonSoftContactCfg: """Global soft-contact parameters applied to the finalized Newton model.""" soft_contact_ke: float = 1.0e3 - """Body-particle and particle self-contact stiffness [N/m].""" + """Body-particle and particle self-contact stiffness [N/m]. + + Effective body-particle stiffness is ``0.5 * (soft_contact_ke + shape_ke)``, + where ``shape_ke`` is the rigid shape's material stiffness. + """ soft_contact_kd: float = 10.0 """Body-particle contact damping [N*s/m].""" soft_contact_mu: float = 0.5 - """Body-particle contact friction coefficient [dimensionless].""" + """Body-particle contact friction coefficient [dimensionless]. + + Effective body-particle friction is ``sqrt(soft_contact_mu * shape_mu)``, + where ``shape_mu`` is the rigid shape's material friction coefficient. + """ @configclass From 891d4ae06add637385b2f384694dd2101268438e Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Thu, 13 Aug 2026 10:59:08 +0200 Subject: [PATCH 12/16] Remove duplicate VBD export test Rely on the Newton manager abstraction suite for public exports, class resolution, and subclass contracts. --- source/isaaclab_newton/test/physics/test_vbd_core.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index 4455ca59a7d4..a825a2de5234 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -14,16 +14,6 @@ from isaaclab_newton.physics import NewtonCfg, NewtonManager -def test_vbd_symbols_are_exported_from_core(): - """Core exports the VBD manager and configuration.""" - physics = importlib.import_module("isaaclab_newton.physics") - - assert physics.NewtonVBDManager.__name__ == "NewtonVBDManager" - assert physics.VBDSolverCfg.__name__ == "VBDSolverCfg" - assert physics.VBDSolverCfg().class_type.__name__ == "NewtonVBDManager" - assert issubclass(physics.NewtonVBDManager, NewtonManager) - - def test_soft_contact_cfg_defaults_match_newton(): """Soft-contact defaults match the pinned Newton model.""" physics = importlib.import_module("isaaclab_newton.physics") From d4bb58f631b8a24e39db6e397a24c01ab36aedd0 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Thu, 13 Aug 2026 11:53:02 +0200 Subject: [PATCH 13/16] Test Newton soft-contact propagation Exercise configured overrides and preservation of finalized model defaults through the startup lifecycle. --- .../test/physics/test_vbd_core.py | 84 ++++++++++++++++--- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index a825a2de5234..f92bb4d79b33 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -11,18 +11,80 @@ from types import SimpleNamespace import pytest -from isaaclab_newton.physics import NewtonCfg, NewtonManager +from isaaclab_newton.physics import NewtonCfg, NewtonManager, NewtonSoftContactCfg + +from isaaclab.physics import PhysicsManager + + +@pytest.mark.parametrize( + ("soft_contact_cfg", "expected"), + [ + pytest.param(None, (7.0, 8.0, 9.0), id="preserve"), + pytest.param( + NewtonSoftContactCfg(soft_contact_ke=11.0, soft_contact_kd=12.0, soft_contact_mu=13.0), + (11.0, 12.0, 13.0), + id="override", + ), + ], +) +def test_soft_contact_cfg_updates_finalized_model(monkeypatch, soft_contact_cfg, expected): + """Soft-contact configuration updates the finalized model when provided.""" + state_values = [] + + class Model: + soft_contact_ke = 7.0 + soft_contact_kd = 8.0 + soft_contact_mu = 9.0 + world_count = 0 + articulation_count = 0 + + def set_gravity(self, gravity): + pass + + def state(self): + state_values.append((self.soft_contact_ke, self.soft_contact_kd, self.soft_contact_mu)) + return object() + + def control(self): + return object() - -def test_soft_contact_cfg_defaults_match_newton(): - """Soft-contact defaults match the pinned Newton model.""" - physics = importlib.import_module("isaaclab_newton.physics") - cfg = physics.NewtonSoftContactCfg() - - assert cfg.soft_contact_ke == 1.0e3 - assert cfg.soft_contact_kd == 10.0 - assert cfg.soft_contact_mu == 0.5 - assert NewtonCfg().soft_contact_cfg is None + class Builder: + body_label = () + up_axis = None + + def finalize(self, *, device): + return model + + model = Model() + monkeypatch.setattr(PhysicsManager, "_cfg", NewtonCfg(soft_contact_cfg=soft_contact_cfg), raising=False) + monkeypatch.setattr(PhysicsManager, "_device", "cpu", raising=False) + monkeypatch.setattr(NewtonManager, "_builder", Builder(), raising=False) + monkeypatch.setattr(NewtonManager, "_up_axis", "Z", raising=False) + monkeypatch.setattr(NewtonManager, "_gravity_vector", (0.0, 0.0, -9.81), raising=False) + monkeypatch.setattr(NewtonManager, "_num_envs", 0, raising=False) + monkeypatch.setattr(NewtonManager, "_clone_physics_only", True, raising=False) + monkeypatch.setattr(NewtonManager, "_pending_extended_state_attributes", set(), raising=False) + monkeypatch.setattr(NewtonManager, "_pending_extended_contact_attributes", set(), raising=False) + for attr in ( + "_model", + "_state_0", + "_state_1", + "_control", + "_adapter", + "_use_newton_actuators_active", + "_world_reset_mask", + "_fk_reset_mask", + ): + monkeypatch.setattr(NewtonManager, attr, getattr(NewtonManager, attr, None), raising=False) + monkeypatch.setattr(NewtonManager, "_cl_pending_sites", {}, raising=False) + monkeypatch.setattr(NewtonManager, "_drain_stale_cuda_error", classmethod(lambda cls: None)) + monkeypatch.setattr(NewtonManager, "dispatch_event", classmethod(lambda cls, event: None)) + + NewtonManager.start_simulation() + + assert (model.soft_contact_ke, model.soft_contact_kd, model.soft_contact_mu) == expected + + assert state_values == [expected, expected] @pytest.mark.parametrize("env_paths", [(), ("/World/Env_0", "/World/Env_1")], ids=["flat", "replicated"]) From 088cf3735c9c6b17ee1e6f6d08a5cc86989b351d Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Thu, 13 Aug 2026 13:04:32 +0200 Subject: [PATCH 14/16] Clarify VBD coupling overview Scope the overview list to named-entry coupling while retaining the custom shared-model path in its dedicated section. --- .../physical-backends/newton/using-vbd-solver.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst b/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst index cf9a5c4a487a..702c7bb2a5e0 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/using-vbd-solver.rst @@ -14,8 +14,8 @@ with VBD. VBD is usually exposed through a task-specific physics preset rather than a general ``newton_vbd`` preset. Deformable-only scenes can use -:class:`~isaaclab_newton.physics.VBDSolverCfg` directly. Robot or -rigid-body scenes can use: +:class:`~isaaclab_newton.physics.VBDSolverCfg` directly. For robot or +rigid-body scenes, the named-entry coupling options are: * :class:`~isaaclab_contrib.coupling.CouplerProxyCfg` for the proxy coupling used by the core Franka tasks. From 051c66a95ee99932387598a92f42ed38d0210b01 Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Thu, 13 Aug 2026 15:37:09 +0200 Subject: [PATCH 15/16] Preserve latest VBD solver contracts Carry the new rigid-particle buffer option into the core VBD config and keep rigid-force capability coverage after removing the contrib manager. --- .../test/coupling/test_coupler.py | 1 + .../test_deformable_builder_hooks.py | 17 ----------------- .../isaaclab_newton/physics/vbd_manager_cfg.py | 6 +++++- .../test/physics/test_vbd_core.py | 18 ++++++++++++++++++ .../config/franka_soft/franka_soft_env_cfg.py | 1 + 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/source/isaaclab_contrib/test/coupling/test_coupler.py b/source/isaaclab_contrib/test/coupling/test_coupler.py index c3c28fcc1ba6..53c47c873105 100644 --- a/source/isaaclab_contrib/test/coupling/test_coupler.py +++ b/source/isaaclab_contrib/test/coupling/test_coupler.py @@ -25,6 +25,7 @@ MJWarpSolverCfg, MPMSolverCfg, NewtonCollisionPipelineCfg, + NewtonVBDManager, VBDSolverCfg, XPBDSolverCfg, ) diff --git a/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py b/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py index ee068dbbce26..124c9881f0bc 100644 --- a/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py +++ b/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py @@ -19,7 +19,6 @@ add_deformable_entry_to_builder, setup_registered_deformable_fabric_sync, ) -from isaaclab_contrib.deformable.vbd_manager import NewtonVBDManager class _FakeBuilder: @@ -84,22 +83,6 @@ def test_deformable_package_exports_public_symbols(): assert DeformableObject.__name__ == "DeformableObject" -@pytest.mark.parametrize("external_rigid_solver", [False, True]) -def test_vbd_solver_force_input_capability(monkeypatch, external_rigid_solver: bool): - """VBD consumes rigid forces only when it owns AVBD rigid integration.""" - solver = object() - monkeypatch.setattr(NewtonVBDManager, "_create_solver", lambda model, cfg: solver) - monkeypatch.setattr(NewtonManager, "_solver", None) - monkeypatch.setattr(NewtonManager, "_use_single_state", True) - monkeypatch.setattr(NewtonManager, "_needs_collision_pipeline", False) - monkeypatch.setattr(NewtonManager, "_supports_rigid_body_force_input", False) - - NewtonVBDManager._build_solver(object(), VBDSolverCfg(integrate_with_external_rigid_solver=external_rigid_solver)) - - assert NewtonManager._solver is solver - assert NewtonManager._supports_rigid_body_force_input is not external_rigid_solver - - def test_newton_material_defaults_match_registry_defaults(): """Test that Newton material cfg defaults match the deformable registry defaults.""" material_cfg = NewtonDeformableMaterialCfg() 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 f3afea053f2d..2923f8c7e6bc 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/vbd_manager_cfg.py @@ -62,4 +62,8 @@ class VBDSolverCfg(NewtonSolverCfg): """Initial stiffness seed for rigid-body contacts [N/m].""" rigid_body_particle_contact_buffer_size: int = 256 - """Initial capacity of the rigid-body particle-contact buffer.""" + """Per-body capacity of the particle, edge, and face soft-contact list. + + Increase this value when Newton reports a per-body particle contact buffer overflow. + Only used when :attr:`integrate_with_external_rigid_solver` is ``False``. + """ diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index f92bb4d79b33..a493a854e8ea 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -200,6 +200,24 @@ def color(self): assert events == ["color", "start"] +@pytest.mark.parametrize("external_rigid_solver", [False, True]) +def test_vbd_solver_force_input_capability(monkeypatch, external_rigid_solver): + """VBD accepts rigid forces only when it integrates rigid bodies.""" + physics = importlib.import_module("isaaclab_newton.physics") + solver = object() + monkeypatch.setattr(physics.NewtonVBDManager, "_create_solver", lambda model, cfg: solver) + monkeypatch.setattr(NewtonManager, "_solver", None) + monkeypatch.setattr(NewtonManager, "_use_single_state", True) + monkeypatch.setattr(NewtonManager, "_needs_collision_pipeline", False) + monkeypatch.setattr(NewtonManager, "_supports_rigid_body_force_input", False) + + solver_cfg = physics.VBDSolverCfg(integrate_with_external_rigid_solver=external_rigid_solver) + physics.NewtonVBDManager._build_solver(object(), solver_cfg) + + assert NewtonManager._solver is solver + assert NewtonManager._supports_rigid_body_force_input is not external_rigid_solver + + 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") diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py index 13c14489a1b0..97185161b8d1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py @@ -49,6 +49,7 @@ CouplerProxyCfg, CouplerProxyMappingCfg, ) + from isaaclab_tasks.utils import PresetCfg from isaaclab_tasks.utils.presets import MultiBackendRendererCfg From ab29bc3e8319acc7c8c924b9b63ed24e94025bdd Mon Sep 17 00:00:00 2001 From: Mike Yan Michelis Date: Thu, 13 Aug 2026 18:10:56 +0200 Subject: [PATCH 16/16] Fix rebased VBD consumers Update the cable examples to use the core VBD configuration and mark the contrib API removal as a major change. --- scripts/demos/newton_viewer_block_and_tackle.py | 4 +--- .../{mmichelis-vbd-core.rst => mmichelis-vbd-core.major.rst} | 0 .../core/lift/config/franka_soft/franka_cable_env_cfg.py | 3 +-- 3 files changed, 2 insertions(+), 5 deletions(-) rename source/isaaclab_contrib/changelog.d/{mmichelis-vbd-core.rst => mmichelis-vbd-core.major.rst} (100%) diff --git a/scripts/demos/newton_viewer_block_and_tackle.py b/scripts/demos/newton_viewer_block_and_tackle.py index 5ce4ec1acd3e..616aa7a7af77 100644 --- a/scripts/demos/newton_viewer_block_and_tackle.py +++ b/scripts/demos/newton_viewer_block_and_tackle.py @@ -24,13 +24,11 @@ import newton import newton.utils import warp as wp -from isaaclab_newton.physics import NewtonCfg, NewtonManager, NewtonShapeCfg +from isaaclab_newton.physics import NewtonCfg, NewtonManager, NewtonShapeCfg, VBDSolverCfg import isaaclab.sim as sim_utils from isaaclab.utils.configclass import configclass -from isaaclab_contrib.deformable import VBDSolverCfg - MECHANICAL_ADVANTAGE = 4 LOAD_MASS = 5.0 HANDLE_MASS = 0.5 # Compensated in the load mass; raises Newton's picking-force limit. diff --git a/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst b/source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.major.rst similarity index 100% rename from source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.rst rename to source/isaaclab_contrib/changelog.d/mmichelis-vbd-core.major.rst diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py index 91685db4169d..3bcb623b9294 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py @@ -7,7 +7,7 @@ from __future__ import annotations -from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg, NewtonShapeCfg +from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg, NewtonShapeCfg, VBDSolverCfg import isaaclab.sim as sim_utils from isaaclab.assets import AssetBaseCfg, CableObjectCfg @@ -23,7 +23,6 @@ from isaaclab.utils.configclass import configclass from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg -from isaaclab_contrib.deformable import VBDSolverCfg from isaaclab_tasks.utils import PresetCfg