Unverified Commit aa0e7135 authored by lgulich's avatar lgulich Committed by GitHub

Allows selecting articulation root prim explicitly (#2228)

# Description

Normally the `Articulation` class would search for an articulation root
below `ArticulationCfg.prim_path`. However this fails when multiple
articulation roots are present. This PR allows to explicitly specify an
articulation root prim path with
`ArticulationCfg.articulation_root_prim_path`. If not set it will fall
back to the old search approach.

## Type of change

<!-- As you go through the list, delete the ones that are not
applicable. -->

- New feature (non-breaking change which adds functionality)

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

---------
Co-authored-by: 's avatarKelly Guo <kellyg@nvidia.com>
parent 73664673
Changelog Changelog
--------- ---------
0.39.1 (2025-05-14)
~~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Added a new attribute :attr:`articulation_root_prim_path` to the :class:`~isaaclab.assets.ArticulationCfg` class
to allow explicitly specifying the prim path of the articulation root.
0.39.0 (2025-05-03) 0.39.0 (2025-05-03)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
......
...@@ -1147,37 +1147,48 @@ class Articulation(AssetBase): ...@@ -1147,37 +1147,48 @@ class Articulation(AssetBase):
def _initialize_impl(self): def _initialize_impl(self):
# obtain global simulation view # obtain global simulation view
self._physics_sim_view = SimulationManager.get_physics_sim_view() self._physics_sim_view = SimulationManager.get_physics_sim_view()
# obtain the first prim in the regex expression (all others are assumed to be a copy of this)
template_prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) if self.cfg.articulation_root_prim_path is not None:
if template_prim is None: # The articulation root prim path is specified explicitly, so we can just use this.
root_prim_path_expr = self.cfg.prim_path + self.cfg.articulation_root_prim_path
else:
# No articulation root prim path was specified, so we need to search
# for it. We search for this in the first environment and then
# create a regex that matches all environments.
first_env_matching_prim = sim_utils.find_first_matching_prim(self.cfg.prim_path)
if first_env_matching_prim is None:
raise RuntimeError(f"Failed to find prim for expression: '{self.cfg.prim_path}'.") raise RuntimeError(f"Failed to find prim for expression: '{self.cfg.prim_path}'.")
template_prim_path = template_prim.GetPath().pathString first_env_matching_prim_path = first_env_matching_prim.GetPath().pathString
# find articulation root prims # Find all articulation root prims in the first environment.
root_prims = sim_utils.get_all_matching_child_prims( first_env_root_prims = sim_utils.get_all_matching_child_prims(
template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) first_env_matching_prim_path,
predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI),
) )
if len(root_prims) == 0: if len(first_env_root_prims) == 0:
raise RuntimeError( raise RuntimeError(
f"Failed to find an articulation when resolving '{self.cfg.prim_path}'." f"Failed to find an articulation when resolving '{first_env_matching_prim_path}'."
" Please ensure that the prim has 'USD ArticulationRootAPI' applied." " Please ensure that the prim has 'USD ArticulationRootAPI' applied."
) )
if len(root_prims) > 1: if len(first_env_root_prims) > 1:
raise RuntimeError( raise RuntimeError(
f"Failed to find a single articulation when resolving '{self.cfg.prim_path}'." f"Failed to find a single articulation when resolving '{first_env_matching_prim_path}'."
f" Found multiple '{root_prims}' under '{template_prim_path}'." f" Found multiple '{first_env_root_prims}' under '{first_env_matching_prim_path}'."
" Please ensure that there is only one articulation in the prim path tree." " Please ensure that there is only one articulation in the prim path tree."
) )
# resolve articulation root prim back into regex expression # Now we convert the found articulation root from the first
root_prim_path = root_prims[0].GetPath().pathString # environment back into a regex that matches all environments.
root_prim_path_expr = self.cfg.prim_path + root_prim_path[len(template_prim_path) :] first_env_root_prim_path = first_env_root_prims[0].GetPath().pathString
root_prim_path_relative_to_prim_path = first_env_root_prim_path[len(first_env_matching_prim_path) :]
root_prim_path_expr = self.cfg.prim_path + root_prim_path_relative_to_prim_path
# -- articulation # -- articulation
self._root_physx_view = self._physics_sim_view.create_articulation_view(root_prim_path_expr.replace(".*", "*")) self._root_physx_view = self._physics_sim_view.create_articulation_view(root_prim_path_expr.replace(".*", "*"))
# check if the articulation was created # check if the articulation was created
if self._root_physx_view._backend is None: if self._root_physx_view._backend is None:
raise RuntimeError(f"Failed to create articulation at: {self.cfg.prim_path}. Please check PhysX logs.") raise RuntimeError(f"Failed to create articulation at: {root_prim_path_expr}. Please check PhysX logs.")
# log information about the articulation # log information about the articulation
omni.log.info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") omni.log.info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.")
......
...@@ -38,6 +38,12 @@ class ArticulationCfg(AssetBaseCfg): ...@@ -38,6 +38,12 @@ class ArticulationCfg(AssetBaseCfg):
class_type: type = Articulation class_type: type = Articulation
articulation_root_prim_path: str | None = None
"""Path to the articulation root prim in the USD file.
If not provided will search for a prim with the ArticulationRootAPI. Should start with a slash.
"""
init_state: InitialStateCfg = InitialStateCfg() init_state: InitialStateCfg = InitialStateCfg()
"""Initial state of the articulated object. Defaults to identity pose with zero velocity and zero joint state.""" """Initial state of the articulated object. Defaults to identity pose with zero velocity and zero joint state."""
......
...@@ -1578,6 +1578,42 @@ def test_body_incoming_joint_wrench_b_single_joint(sim, num_articulations, devic ...@@ -1578,6 +1578,42 @@ def test_body_incoming_joint_wrench_b_single_joint(sim, num_articulations, devic
rtol=1e-3, rtol=1e-3,
) )
def test_setting_articulation_root_prim_path(self):
"""Test that the articulation root prim path can be set explicitly."""
with build_simulation_context(device="cuda:0", add_ground_plane=False, auto_add_lighting=True) as sim:
sim._app_control_on_stop_handle = None
# Create articulation
articulation_cfg = generate_articulation_cfg(articulation_type="humanoid")
print(articulation_cfg.spawn.usd_path)
articulation_cfg.articulation_root_prim_path = "/torso"
articulation, _ = generate_articulation(articulation_cfg, 1, "cuda:0")
# Check that boundedness of articulation is correct
self.assertEqual(ctypes.c_long.from_address(id(articulation)).value, 1)
# Play sim
sim.reset()
# Check if articulation is initialized
self.assertTrue(articulation._is_initialized)
def test_setting_invalid_articulation_root_prim_path(self):
"""Test that the articulation root prim path can be set explicitly."""
with build_simulation_context(device="cuda:0", add_ground_plane=False, auto_add_lighting=True) as sim:
sim._app_control_on_stop_handle = None
# Create articulation
articulation_cfg = generate_articulation_cfg(articulation_type="humanoid")
print(articulation_cfg.spawn.usd_path)
articulation_cfg.articulation_root_prim_path = "/non_existing_prim_path"
articulation, _ = generate_articulation(articulation_cfg, 1, "cuda:0")
# Check that boundedness of articulation is correct
self.assertEqual(ctypes.c_long.from_address(id(articulation)).value, 1)
# Play sim
sim.reset()
# Check if articulation is initialized
self.assertFalse(articulation._is_initialized)
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__, "-v", "--maxfail=1"]) pytest.main([__file__, "-v", "--maxfail=1"])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment