Repository: isaac-sim/IsaacLab (Python)

Issue:
[Bug Report] randomize_joint_parameters only updates static friction and may violate PhysX constraint (static < dynamic friction)
### Describe the bug

The function `randomize_joint_parameters` can be used to randomize joint friction.
Starting from Isaac Sim **5.0.0**, it is possible to set **static**, **dynamic (Coulomb)**, and **viscous** friction separately.

However, the main constraint that must be respected is:

**static friction ≥ dynamic friction**

Currently, calling `randomize_joint_parameters` randomizes **only the static friction**, as shown here:

https://github.com/isaac-sim/IsaacLab/blob/3a1a65bd942121b059ca4356819c8353a6840af8/source/isaaclab/isaaclab/envs/mdp/events.py#L704-L717

This can result in static friction being set lower than dynamic friction, which violates PhysX constraints.

### Problems Identified

1. Only the **static friction** is updated.
2. Nothing prevents **static friction < dynamic friction**, which triggers PhysX errors.

### Proposed Solution

#### Problem 1: Only static friction is randomized

* Update **dynamic friction** (available in Isaac Sim ≥ 5) calling 
  
   https://github.com/isaac-sim/IsaacLab/blob/3a1a65bd942121b059ca4356819c8353a6840af8/source/isaaclab/isaaclab/assets/articulation/articulation.py#L871

* Update **viscous friction** (available in Isaac Sim ≥ 5) calling 
  
   https://github.com/isaac-sim/IsaacLab/blob/3a1a65bd942121b059ca4356819c8353a6840af8/source/isaaclab/isaaclab/assets/articulation/articulation.py#L871

#### Problem 2: Constraint violation (static friction < dynamic friction)

During randomization, enforce the condition:

```python
if static_friction < dynamic_friction:
    # Option A: Increase static friction
    static_friction = dynamic_friction
    # Option B: Decrease dynamic friction
    dynamic_friction = static_friction
```

A consistent choice should be made (e.g., always clamp static friction to be at least dynamic friction).

---

### Steps to Reproduce

1. Define an articulation with:

   ```python
   friction = {" .*": 10.0},
   dynamic_friction = {" .*": 10.0},
   viscous_friction = {" .*": 0.0},
   ```

2. Add the event:

   ```python
   randomize_friction = EventTerm(
       func=mdp.randomize_joint_parameters,
       mode="reset",
       params={
           "asset_cfg": SceneEntityCfg("robot"),
           "friction_distribution_params": (0.5, 1.5),
           "operation": "scale",
           "distribution": "uniform",
       },
   )
   ```

The following error will be obtained
```
2025-08-25T09:13:33Z [118,688ms] [Error] [omni.physx.plugin] 
PhysX error: Static friction effort must be greater than or equal to dynamic friction effort.,
FILE /builds/omniverse/physics/physx/source/physx/src/NpArticulationJointReducedCoordinate.cpp, LINE 209
```


### System Info

* Commit: 9d6321463067c541ce1a24531ff87f99a18fd8f7
* Isaac Sim Version: 5.0
* OS: Ubuntu 22.04
* GPU: NVIDIA L40S
* CUDA: 12.4
* GPU Driver: 550.107.02


### Additional Context

N/A


### Checklist

* [x] I have checked that there is no similar issue in the repo (**required**)
* [x] I have confirmed this issue is not in Isaac Sim itself but in this repo



### Acceptance Criteria

* [ ] All friction types (static, dynamic, viscous) can be randomized
* [ ] Randomization ensures **static friction ≥ dynamic friction**




Discussion: Looking at the code, I noticed a potential issue when calling
`write_joint_friction_coefficient_to_sim` and `write_joint_dynamic_friction_coefficient_to_sim` one after the other.

Nothing prevents the randomization event from setting a **dynamic friction** value lower than the previously set **static friction**.

This happens because each time friction is updated—whether static, dynamic, or viscous—Isaac Lab’s utility functions (`write_joint_friction_coefficient_to_sim` and `write_joint_dynamic_friction_coefficient_to_sim`) internally call

```python
self.root_physx_view.set_dof_friction_properties
```

which overwrites the joint’s friction properties on every call.

In the end, I came out with the following patch. Let me know if it is fine with you. It is compatible with IsaacSim 5.0 and previous versions
```patch
diff --git a/source/isaaclab/isaaclab/assets/articulation/articulation.py b/source/isaaclab/isaaclab/assets/articulation/articulation.py
index 55f3e650ed..a614cb1882 100644
--- a/source/isaaclab/isaaclab/assets/articulation/articulation.py
+++ b/source/isaaclab/isaaclab/assets/articulation/articulation.py
@@ -824,6 +824,41 @@ class Articulation(AssetBase):
         # set into simulation
         self.root_physx_view.set_dof_armatures(self._data.joint_armature.cpu(), indices=physx_env_ids.cpu())
 
+    def write_joint_friction_coefficients_to_sim(
+        self,
+        joint_friction_coeff: torch.Tensor | float,
+        joint_dynamic_friction_coeff: torch.Tensor | float,
+        joint_viscous_friction_coeff: torch.Tensor | float,
+        joint_ids: Sequence[int] | slice | None = None,
+        env_ids: Sequence[int] | None = None,
+    ):
+        if int(get_version()[2]) < 5:
+            omni.log.warn("Setting joint dynamic and, viscous friction coefficients are not supported in Isaac Sim < 5.0")
+            return
+        # resolve indices
+        physx_env_ids = env_ids
+        if env_ids is None:
+            env_ids = slice(None)
+            physx_env_ids = self._ALL_INDICES
+        if joint_ids is None:
+            joint_ids = slice(None)
+        # broadcast env_ids if needed to allow double indexing
+        if env_ids != slice(None) and joint_ids != slice(None):
+            env_ids = env_ids[:, None]
+        # set into internal buffers
+        self._data.joint_friction_coeff[env_ids, joint_ids] = joint_friction_coeff
+        self._data.joint_dynamic_friction_coeff[env_ids, joint_ids] = joint_dynamic_friction_coeff
+        self._data.joint_viscous_friction_coeff[env_ids, joint_ids] = joint_viscous_friction_coeff
+        # set into simulation
+        friction_props = self.root_physx_view.get_dof_friction_properties()
+        physx_env_ids_cpu = physx_env_ids.cpu()
+
+        friction_props[physx_env_ids_cpu, :, 0] = self._data.joint_friction_coeff[physx_env_ids, :].cpu()
+        friction_props[physx_env_ids_cpu, :, 1] = self._data.joint_dynamic_friction_coeff[physx_env_ids, :].cpu()
+        friction_props[physx_env_ids_cpu, :, 2] = self._data.joint_viscous_friction_coeff[physx_env_ids, :].cpu()
+        self.root_physx_view.set_dof_friction_properties(friction_props, indices=physx_env_ids_cpu)
+
+
     def write_joint_friction_coefficient_to_sim(
         self,
         joint_friction_coeff: torch.Tensor | float,
diff --git a/source/isaaclab/isaaclab/envs/mdp/events.py b/source/isaaclab/isaaclab/envs/mdp/events.py
index 637baa9d72..16cd217763 100644
--- a/source/isaaclab/isaaclab/envs/mdp/events.py
+++ b/source/isaaclab/isaaclab/envs/mdp/events.py
@@ -23,6 +23,7 @@ import carb
 import omni.physics.tensors.impl.api as physx
 from isaacsim.core.utils.extensions import enable_extension
 from isaacsim.core.utils.stage import get_current_stage
+from isaacsim.core.version import get_version
 from pxr import Gf, Sdf, UsdGeom, Vt
 
 import isaaclab.sim as sim_utils
@@ -712,9 +713,50 @@ class randomize_joint_parameters(ManagerTermBase):
                 operation=operation,
                 distribution=distribution,
             )
-            self.asset.write_joint_friction_coefficient_to_sim(
-                friction_coeff[env_ids[:, None], joint_ids], joint_ids=joint_ids, env_ids=env_ids
+
+            # ensure the friction coefficient is non-negative
+            friction_coeff = torch.clamp(friction_coeff, min=0.0)
+
+            # if isaacsim version is lower than 5.0.0 we can set only the static friction coefficient
+            if int(get_version()[2]) < 5:
+                self.asset.write_joint_friction_coefficient_to_sim(
+                    joint_friction_coeff=friction_coeff[env_ids[:, None], joint_ids],
+                    joint_ids=joint_ids, env_ids=env_ids
             )
+            # for isaacsim version 5.0.0 and above we can set static, dynamic, and viscous friction coefficients
+            else:
+                dynamic_friction_coeff = _randomize_prop_by_op(
+                    self.asset.data.default_joint_dynamic_friction_coeff.clone(),
+                    friction_distribution_params,
+                    env_ids,
+                    joint_ids,
+                    operation=operation,
+                    distribution=distribution,
+                )
+
+                viscous_friction_coeff = _randomize_prop_by_op(
+                    self.asset.data.default_joint_viscous_friction_coeff.clone(),
+                    friction_distribution_params,
+                    env_ids,
+                    joint_ids,
+                    operation=operation,
+                    distribution=distribution,
+                )
+
+                # ensure the friction coefficients are non-negative
+                dynamic_friction_coeff = torch.clamp(dynamic_friction_coeff, min=0.0)
+                viscous_friction_coeff = torch.clamp(viscous_friction_coeff, min=0.0)
+
+                # ensure the dynamic friction is less than or equal to static friction
+                dynamic_friction_coeff = torch.min(dynamic_friction_coeff, friction_coeff)  
+
+                # set the friction coefficients into the physics simulation
+                self.asset.write_joint_friction_coefficients_to_sim(
+                    joint_friction_coeff=friction_coeff[env_ids[:, None], joint_ids],
+                    joint_dynamic_friction_coeff=dynamic_friction_coeff[env_ids[:, None], joint_ids],
+                    joint_viscous_friction_coeff=viscous_friction_coeff[env_ids[:, None], joint_ids],
+                    joint_ids=joint_ids, env_ids=env_ids
+                )
 
         # joint armature
         if armature_distribution_params is not None:
```
Associated PR: https://github.com/isaac-sim/IsaacLab/pull/3318



Fix (patch):
diff --git a/source/isaaclab/config/extension.toml b/source/isaaclab/config/extension.toml
index fde6cb9bf91..731d14c2941 100644
--- a/source/isaaclab/config/extension.toml
+++ b/source/isaaclab/config/extension.toml
@@ -1,7 +1,7 @@
 [package]
 
 # Note: Semantic Versioning is used: https://semver.org/
-version = "0.46.2"
+version = "0.46.3"
 
 # Description
 title = "Isaac Lab framework for Robot Learning"
diff --git a/source/isaaclab/docs/CHANGELOG.rst b/source/isaaclab/docs/CHANGELOG.rst
index c33e307eebe..1e1a61ed67e 100644
--- a/source/isaaclab/docs/CHANGELOG.rst
+++ b/source/isaaclab/docs/CHANGELOG.rst
@@ -1,6 +1,14 @@
 Changelog
 ---------
 
+0.46.3 (2025-09-17)
+~~~~~~~~~~~~~~~~~~~
+
+Added
+^^^^^
+
+* Modified setter to support for viscous and dynamic joint friction coefficients in articulation based on IsaacSim 5.0.
+* Added randomization of viscous and dynamic joint friction coefficients in event term.
 
 0.46.2 (2025-09-13)
 ~~~~~~~~~~~~~~~~~~~
@@ -31,7 +39,6 @@ Added
   during the traversal. Earlier, instanced prims were skipped since :meth:`Usd.Prim.GetChildren` did not return
   instanced prims, which is now fixed.
 
-
 Changed
 ^^^^^^^
 
diff --git a/source/isaaclab/isaaclab/assets/articulation/articulation.py b/source/isaaclab/isaaclab/assets/articulation/articulation.py
index 3a720de5573..deb8b6479f5 100644
--- a/source/isaaclab/isaaclab/assets/articulation/articulation.py
+++ b/source/isaaclab/isaaclab/assets/articulation/articulation.py
@@ -827,24 +827,33 @@ def write_joint_armature_to_sim(
     def write_joint_friction_coefficient_to_sim(
         self,
         joint_friction_coeff: torch.Tensor | float,
+        joint_dynamic_friction_coeff: torch.Tensor | float | None = None,
+        joint_viscous_friction_coeff: torch.Tensor | float | None = None,
         joint_ids: Sequence[int] | slice | None = None,
         env_ids: Sequence[int] | None = None,
     ):
-        r"""Write joint static friction coefficients into the simulation.
+        r"""Write joint friction coefficients into the simulation.
 
-        The joint static friction is a unitless quantity. It relates the magnitude of the spatial force transmitted
-        from the parent body to the child body to the maximal static friction force that may be applied by the solver
-        to resist the joint motion.
+        For Isaac Sim versions below 5.0, only the static friction coefficient is set.
+        This limits the resisting force or torque up to a maximum proportional to the transmitted
+        spatial force: :math:`\|F_{resist}\| \leq \mu_s \, \|F_{spatial}\|`.
 
-        Mathematically, this means that: :math:`F_{resist} \leq \mu F_{spatial}`, where :math:`F_{resist}`
-        is the resisting force applied by the solver and :math:`F_{spatial}` is the spatial force
-        transmitted from the parent body to the child body. The simulated static friction effect is therefore
-        similar to static and Coulomb static friction.
+        For Isaac Sim versions 5.0 and above, the static, dynamic, and viscous friction coefficients
+        are set. The model combines Coulomb (static & dynamic) friction with a viscous term:
+
+        - Static friction :math:`\mu_s` defines the maximum effort that prevents motion at rest.
+        - Dynamic friction :math:`\mu_d` applies once motion begins and remains constant during motion.
+        - Viscous friction :math:`c_v` is a velocity-proportional resistive term.
 
         Args:
-            joint_friction_coeff: Joint static friction coefficient. Shape is (len(env_ids), len(joint_ids)).
-            joint_ids: The joint indices to set the joint torque limits for. Defaults to None (all joints).
-            env_ids: The environment indices to set the joint torque limits for. Defaults to None (all environments).
+            joint_friction_coeff: Static friction coefficient :math:`\mu_s`.
+                Shape is (len(env_ids), len(joint_ids)). Scalars are broadcast to all selections.
+            joint_dynamic_friction_coeff: Dynamic (Coulomb) friction coefficient :math:`\mu_d`.
+                Same shape as above. If None, the dynamic coefficient is not updated.
+            joint_viscous_friction_coeff: Viscous friction coefficient :math:`c_v`.
+                Same shape as above. If None, the viscous coefficient is not updated.
+            joint_ids: The joint indices to set the friction coefficients for. Defaults to None (all joints).
+            env_ids: The environment indices to set the friction coefficients for. Defaults to None (all environments).
         """
         # resolve indices
         physx_env_ids = env_ids
@@ -858,15 +867,38 @@ def write_joint_friction_coefficient_to_sim(
             env_ids = env_ids[:, None]
         # set into internal buffers
         self._data.joint_friction_coeff[env_ids, joint_ids] = joint_friction_coeff
+
+        # if dynamic or viscous friction coeffs are provided, set them too
+        if joint_dynamic_friction_coeff is not None:
+            self._data.joint_dynamic_friction_coeff[env_ids, joint_ids] = joint_dynamic_friction_coeff
+        if joint_viscous_friction_coeff is not None:
+            self._data.joint_viscous_friction_coeff[env_ids, joint_ids] = joint_viscous_friction_coeff
+
+        # move the indices to cpu
+        physx_envs_ids_cpu = physx_env_ids.cpu()
+
         # set into simulation
         if int(get_version()[2]) < 5:
             self.root_physx_view.set_dof_friction_coefficients(
-                self._data.joint_friction_coeff.cpu(), indices=physx_env_ids.cpu()
+                self._data.joint_friction_coeff.cpu(), indices=physx_envs_ids_cpu
             )
         else:
             friction_props = self.root_physx_view.get_dof_friction_properties()
-            friction_props[physx_env_ids.cpu(), :, 0] = self._data.joint_friction_coeff[physx_env_ids, :].cpu()
-            self.root_physx_view.set_dof_friction_properties(friction_props, indices=physx_env_ids.cpu())
+            friction_props[physx_envs_ids_cpu, :, 0] = self._data.joint_friction_coeff[physx_envs_ids_cpu, :].cpu()
+
+            # only set dynamic and viscous friction if provided
+            if joint_dynamic_friction_coeff is not None:
+                friction_props[physx_envs_ids_cpu, :, 1] = self._data.joint_dynamic_friction_coeff[
+                    physx_envs_ids_cpu, :
+                ].cpu()
+
+            # only set viscous friction if provided
+            if joint_viscous_friction_coeff is not None:
+                friction_props[physx_envs_ids_cpu, :, 2] = self._data.joint_viscous_friction_coeff[
+                    physx_envs_ids_cpu, :
+                ].cpu()
+
+            self.root_physx_view.set_dof_friction_properties(friction_props, indices=physx_envs_ids_cpu)
 
     def write_joint_dynamic_friction_coefficient_to_sim(
         self,
diff --git a/source/isaaclab/isaaclab/envs/mdp/events.py b/source/isaaclab/isaaclab/envs/mdp/events.py
index 5c0c967e840..923fd1597ab 100644
--- a/source/isaaclab/isaaclab/envs/mdp/events.py
+++ b/source/isaaclab/isaaclab/envs/mdp/events.py
@@ -714,8 +714,56 @@ def __call__(
                 operation=operation,
                 distribution=distribution,
             )
+
+            # ensure the friction coefficient is non-negative
+            friction_coeff = torch.clamp(friction_coeff, min=0.0)
+
+            # Always set static friction (indexed once)
+            static_friction_coeff = friction_coeff[env_ids[:, None], joint_ids]
+
+            # if isaacsim version is lower than 5.0.0 we can set only the static friction coefficient
+            major_version = int(env.sim.get_version()[0])
+            if major_version >= 5:
+                # Randomize raw tensors
+                dynamic_friction_coeff = _randomize_prop_by_op(
+                    self.asset.data.default_joint_dynamic_friction_coeff.clone(),
+                    friction_distribution_params,
+                    env_ids,
+                    joint_ids,
+                    operation=operation,
+                    distribution=distribution,
+                )
+                viscous_friction_coeff = _randomize_prop_by_op(
+                    self.asset.data.default_joint_viscous_friction_coeff.clone(),
+                    friction_distribution_params,
+                    env_ids,
+                    joint_ids,
+                    operation=operation,
+                    distribution=distribution,
+                )
+
+                # Clamp to non-negative
+                dynamic_friction_coeff = torch.clamp(dynamic_friction_coeff, min=0.0)
+                viscous_friction_coeff = torch.clamp(viscous_friction_coeff, min=0.0)
+
+                # Ensure dynamic ≤ static (same shape before indexing)
+                dynamic_friction_coeff = torch.minimum(dynamic_friction_coeff, friction_coeff)
+
+                # Index once at the end
+                dynamic_friction_coeff = dynamic_friction_coeff[env_ids[:, None], joint_ids]
+                viscous_friction_coeff = viscous_friction_coeff[env_ids[:, None], joint_ids]
+            else:
+                # For versions < 5.0.0, we do not set these values
+                dynamic_friction_coeff = None
+                viscous_friction_coeff = None
+
+            # Single write call for all versions
             self.asset.write_joint_friction_coefficient_to_sim(
-                friction_coeff[env_ids[:, None], joint_ids], joint_ids=joint_ids, env_ids=env_ids
+                joint_friction_coeff=static_friction_coeff,
+                joint_dynamic_friction_coeff=dynamic_friction_coeff,
+                joint_viscous_friction_coeff=viscous_friction_coeff,
+                joint_ids=joint_ids,
+                env_ids=env_ids,
             )
 
         # joint armature