Skip to content
Merged
2 changes: 1 addition & 1 deletion source/isaaclab/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "0.45.15"
version = "0.46.0"

# Description
title = "Isaac Lab framework for Robot Learning"
Expand Down
20 changes: 20 additions & 0 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
Changelog
---------

0.46.0 (2025-09-06)
~~~~~~~~~~~~~~~~~~~

Added
^^^^^

* Added argument :attr:`traverse_instance_prims` to :meth:`~isaaclab.sim.utils.get_all_matching_child_prims` and
:meth:`~isaaclab.sim.utils.get_first_matching_child_prim` to control whether to traverse instance prims
during the traversal. Earlier, instanced prims were skipped since :meth:`Usd.Prim.GetChildren` did not return
instanced prims, which is now fixed.


Changed
^^^^^^^

* Made parsing of instanced prims in :meth:`~isaaclab.sim.utils.get_all_matching_child_prims` and
:meth:`~isaaclab.sim.utils.get_first_matching_child_prim` as the default behavior.
* Added parsing of instanced prims in :meth:`~isaaclab.sim.utils.make_uninstanceable` to make all prims uninstanceable.


0.45.15 (2025-09-05)
~~~~~~~~~~~~~~~~~~~~

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,7 @@ def _initialize_impl(self):
first_env_root_prims = sim_utils.get_all_matching_child_prims(
first_env_matching_prim_path,
predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI),
traverse_instance_prims=False,
)
if len(first_env_root_prims) == 0:
raise RuntimeError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,9 @@ def _initialize_impl(self):

# find deformable root prims
root_prims = sim_utils.get_all_matching_child_prims(
template_prim_path, predicate=lambda prim: prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI)
template_prim_path,
predicate=lambda prim: prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI),
traverse_instance_prims=False,
)
if len(root_prims) == 0:
raise RuntimeError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,9 @@ def _initialize_impl(self):

# find rigid root prims
root_prims = sim_utils.get_all_matching_child_prims(
template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI)
template_prim_path,
predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI),
traverse_instance_prims=False,
)
if len(root_prims) == 0:
raise RuntimeError(
Expand All @@ -479,7 +481,9 @@ def _initialize_impl(self):
)

articulation_prims = sim_utils.get_all_matching_child_prims(
template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI)
template_prim_path,
predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI),
traverse_instance_prims=False,
)
if len(articulation_prims) != 0:
if articulation_prims[0].GetAttribute("physxArticulation:articulationEnabled").Get():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,9 @@ def _initialize_impl(self):

# find rigid root prims
root_prims = sim_utils.get_all_matching_child_prims(
template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI)
template_prim_path,
predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI),
traverse_instance_prims=False,
)
if len(root_prims) == 0:
raise RuntimeError(
Expand All @@ -618,7 +620,9 @@ def _initialize_impl(self):

# check that no rigid object has an articulation root API, which decreases simulation performance
articulation_prims = sim_utils.get_all_matching_child_prims(
template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI)
template_prim_path,
predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI),
traverse_instance_prims=False,
)
if len(articulation_prims) != 0:
if articulation_prims[0].GetAttribute("physxArticulation:articulationEnabled").Get():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,9 @@ def _initialize_impl(self) -> None:

# find surface gripper prims
gripper_prims = sim_utils.get_all_matching_child_prims(
template_prim_path, predicate=lambda prim: prim.GetTypeName() == "IsaacSurfaceGripper"
template_prim_path,
predicate=lambda prim: prim.GetTypeName() == "IsaacSurfaceGripper",
traverse_instance_prims=False,
)
if len(gripper_prims) == 0:
raise RuntimeError(
Expand Down
52 changes: 47 additions & 5 deletions source/isaaclab/isaaclab/sim/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ def make_uninstanceable(prim_path: str | Sdf.Path, stage: Usd.Stage | None = Non
# make the prim uninstanceable
child_prim.SetInstanceable(False)
# add children to list
all_prims += child_prim.GetChildren()
all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies())


"""
Expand All @@ -577,14 +577,32 @@ def make_uninstanceable(prim_path: str | Sdf.Path, stage: Usd.Stage | None = Non


def get_first_matching_child_prim(
prim_path: str | Sdf.Path, predicate: Callable[[Usd.Prim], bool], stage: Usd.Stage | None = None
prim_path: str | Sdf.Path,
predicate: Callable[[Usd.Prim], bool],
stage: Usd.Stage | None = None,
traverse_instance_prims: bool = True,
) -> Usd.Prim | None:
"""Recursively get the first USD Prim at the path string that passes the predicate function
"""Recursively get the first USD Prim at the path string that passes the predicate function.

This function performs a depth-first traversal of the prim hierarchy starting from
:attr:`prim_path`, returning the first prim that satisfies the provided :attr:`predicate`.
It optionally supports traversal through instance prims, which are normally skipped in standard USD
traversals.

USD instance prims are lightweight copies of prototype scene structures and are not included
in default traversals unless explicitly handled. This function allows traversing into instances
when :attr:`traverse_instance_prims` is set to :attr:`True`.

.. versionchanged:: 2.3.0

Added :attr:`traverse_instance_prims` to control whether to traverse instance prims.
By default, instance prims are now traversed.

Args:
prim_path: The path of the prim in the stage.
predicate: The function to test the prims against. It takes a prim as input and returns a boolean.
stage: The stage where the prim exists. Defaults to None, in which case the current stage is used.
traverse_instance_prims: Whether to traverse instance prims. Defaults to True.

Returns:
The first prim on the path that passes the predicate. If no prim passes the predicate, it returns None.
Expand Down Expand Up @@ -615,7 +633,10 @@ def get_first_matching_child_prim(
if predicate(child_prim):
return child_prim
# add children to list
all_prims += child_prim.GetChildren()
if traverse_instance_prims:
all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies())
else:
all_prims += child_prim.GetChildren()
return None


Expand All @@ -624,16 +645,31 @@ def get_all_matching_child_prims(
predicate: Callable[[Usd.Prim], bool] = lambda _: True,
depth: int | None = None,
stage: Usd.Stage | None = None,
traverse_instance_prims: bool = True,
) -> list[Usd.Prim]:
"""Performs a search starting from the root and returns all the prims matching the predicate.

This function performs a depth-first traversal of the prim hierarchy starting from
:attr:`prim_path`, returning all prims that satisfy the provided :attr:`predicate`. It optionally
supports traversal through instance prims, which are normally skipped in standard USD traversals.

USD instance prims are lightweight copies of prototype scene structures and are not included
in default traversals unless explicitly handled. This function allows traversing into instances
when :attr:`traverse_instance_prims` is set to :attr:`True`.

.. versionchanged:: 2.3.0

Added :attr:`traverse_instance_prims` to control whether to traverse instance prims.
By default, instance prims are now traversed.

Args:
prim_path: The root prim path to start the search from.
predicate: The predicate that checks if the prim matches the desired criteria. It takes a prim as input
and returns a boolean. Defaults to a function that always returns True.
depth: The maximum depth for traversal, should be bigger than zero if specified.
Defaults to None (i.e: traversal happens till the end of the tree).
stage: The stage where the prim exists. Defaults to None, in which case the current stage is used.
traverse_instance_prims: Whether to traverse instance prims. Defaults to True.

Returns:
A list containing all the prims matching the predicate.
Expand Down Expand Up @@ -671,7 +707,13 @@ def get_all_matching_child_prims(
output_prims.append(child_prim)
# add children to list
if depth is None or current_depth < depth:
all_prims_queue += [(child, current_depth + 1) for child in child_prim.GetChildren()]
# resolve prims under the current prim
if traverse_instance_prims:
children = child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies())
else:
children = child_prim.GetChildren()
# add children to list
all_prims_queue += [(child, current_depth + 1) for child in children]

return output_prims

Expand Down
56 changes: 51 additions & 5 deletions source/isaaclab/test/sim/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import isaacsim.core.utils.prims as prim_utils
import isaacsim.core.utils.stage as stage_utils
import pytest
from pxr import Sdf, Usd, UsdGeom
from pxr import Sdf, Usd, UsdGeom, UsdPhysics

import isaaclab.sim as sim_utils
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR
Expand All @@ -41,21 +41,67 @@ def test_get_all_matching_child_prims():
"""Test get_all_matching_child_prims() function."""
# create scene
prim_utils.create_prim("/World/Floor")
prim_utils.create_prim(
"/World/Floor/thefloor", "Cube", position=np.array([75, 75, -150.1]), attributes={"size": 300}
)
prim_utils.create_prim("/World/Room", "Sphere", attributes={"radius": 1e3})
prim_utils.create_prim("/World/Floor/Box", "Cube", position=np.array([75, 75, -150.1]), attributes={"size": 300})
prim_utils.create_prim("/World/Wall", "Sphere", attributes={"radius": 1e3})

# test
isaac_sim_result = prim_utils.get_all_matching_child_prims("/World")
isaaclab_result = sim_utils.get_all_matching_child_prims("/World")
assert isaac_sim_result == isaaclab_result

# add articulation root prim -- this asset has instanced prims
# note: isaac sim function does not support instanced prims so we add it here
# after the above test for the above test to still pass.
prim_utils.create_prim(
"/World/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd"
)

# test with predicate
isaaclab_result = sim_utils.get_all_matching_child_prims("/World", predicate=lambda x: x.GetTypeName() == "Cube")
assert len(isaaclab_result) == 1
assert isaaclab_result[0].GetPrimPath() == "/World/Floor/Box"

# test with predicate and instanced prims
isaaclab_result = sim_utils.get_all_matching_child_prims(
"/World/Franka/panda_hand/visuals", predicate=lambda x: x.GetTypeName() == "Mesh"
)
assert len(isaaclab_result) == 1
assert isaaclab_result[0].GetPrimPath() == "/World/Franka/panda_hand/visuals/panda_hand"

# test valid path
with pytest.raises(ValueError):
sim_utils.get_all_matching_child_prims("World/Room")


def test_get_first_matching_child_prim():
"""Test get_first_matching_child_prim() function."""
# create scene
prim_utils.create_prim("/World/Floor")
prim_utils.create_prim(
"/World/env_1/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd"
)
prim_utils.create_prim(
"/World/env_2/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd"
)
prim_utils.create_prim(
"/World/env_0/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd"
)

# test
isaaclab_result = sim_utils.get_first_matching_child_prim(
"/World", predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI)
)
assert isaaclab_result is not None
assert isaaclab_result.GetPrimPath() == "/World/env_1/Franka"

# test with instanced prims
isaaclab_result = sim_utils.get_first_matching_child_prim(
"/World/env_1/Franka", predicate=lambda prim: prim.GetTypeName() == "Mesh"
)
assert isaaclab_result is not None
assert isaaclab_result.GetPrimPath() == "/World/env_1/Franka/panda_link0/visuals/panda_link0"


def test_find_matching_prim_paths():
"""Test find_matching_prim_paths() function."""
# create scene
Expand Down
Loading