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
---------
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)
~~~~~~~~~~~~~~~~~~~
......
......@@ -1147,37 +1147,48 @@ class Articulation(AssetBase):
def _initialize_impl(self):
# obtain global simulation 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 template_prim is None:
raise RuntimeError(f"Failed to find prim for expression: '{self.cfg.prim_path}'.")
template_prim_path = template_prim.GetPath().pathString
# find articulation root prims
root_prims = sim_utils.get_all_matching_child_prims(
template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI)
)
if len(root_prims) == 0:
raise RuntimeError(
f"Failed to find an articulation when resolving '{self.cfg.prim_path}'."
" Please ensure that the prim has 'USD ArticulationRootAPI' applied."
)
if len(root_prims) > 1:
raise RuntimeError(
f"Failed to find a single articulation when resolving '{self.cfg.prim_path}'."
f" Found multiple '{root_prims}' under '{template_prim_path}'."
" Please ensure that there is only one articulation in the prim path tree."
if self.cfg.articulation_root_prim_path is not 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}'.")
first_env_matching_prim_path = first_env_matching_prim.GetPath().pathString
# Find all articulation root prims in the first environment.
first_env_root_prims = sim_utils.get_all_matching_child_prims(
first_env_matching_prim_path,
predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI),
)
if len(first_env_root_prims) == 0:
raise RuntimeError(
f"Failed to find an articulation when resolving '{first_env_matching_prim_path}'."
" Please ensure that the prim has 'USD ArticulationRootAPI' applied."
)
if len(first_env_root_prims) > 1:
raise RuntimeError(
f"Failed to find a single articulation when resolving '{first_env_matching_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."
)
# Now we convert the found articulation root from the first
# environment back into a regex that matches all environments.
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
# resolve articulation root prim back into regex expression
root_prim_path = root_prims[0].GetPath().pathString
root_prim_path_expr = self.cfg.prim_path + root_prim_path[len(template_prim_path) :]
# -- articulation
self._root_physx_view = self._physics_sim_view.create_articulation_view(root_prim_path_expr.replace(".*", "*"))
# check if the articulation was created
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
omni.log.info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.")
......
......@@ -38,6 +38,12 @@ class ArticulationCfg(AssetBaseCfg):
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()
"""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
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__":
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