diff --git a/source/isaaclab/config/extension.toml b/source/isaaclab/config/extension.toml index a53b7e970cb..9d22bb9f692 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.45.15" +version = "0.46.0" # Description title = "Isaac Lab framework for Robot Learning" diff --git a/source/isaaclab/docs/CHANGELOG.rst b/source/isaaclab/docs/CHANGELOG.rst index 91d5e1ab1ed..c8eaee1d724 100644 --- a/source/isaaclab/docs/CHANGELOG.rst +++ b/source/isaaclab/docs/CHANGELOG.rst @@ -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) ~~~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab/isaaclab/assets/articulation/articulation.py b/source/isaaclab/isaaclab/assets/articulation/articulation.py index 55f3e650ed0..3a720de5573 100644 --- a/source/isaaclab/isaaclab/assets/articulation/articulation.py +++ b/source/isaaclab/isaaclab/assets/articulation/articulation.py @@ -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( diff --git a/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py index 05211af0d2a..982b2f72c81 100644 --- a/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py +++ b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py @@ -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( diff --git a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py index ac76326116e..9de2a137636 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py +++ b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py @@ -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( @@ -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(): diff --git a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py index 363dca41b95..b607f06d088 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py +++ b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py @@ -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( @@ -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(): diff --git a/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py b/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py index 1702dbf90e2..dfa816602f8 100644 --- a/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py +++ b/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py @@ -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( diff --git a/source/isaaclab/isaaclab/sim/utils.py b/source/isaaclab/isaaclab/sim/utils.py index debda3ec807..3d4b8fddb75 100644 --- a/source/isaaclab/isaaclab/sim/utils.py +++ b/source/isaaclab/isaaclab/sim/utils.py @@ -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()) """ @@ -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. @@ -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 @@ -624,9 +645,23 @@ 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 @@ -634,6 +669,7 @@ def get_all_matching_child_prims( 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. @@ -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 diff --git a/source/isaaclab/test/sim/test_utils.py b/source/isaaclab/test/sim/test_utils.py index e4f23438622..62188d8a73a 100644 --- a/source/isaaclab/test/sim/test_utils.py +++ b/source/isaaclab/test/sim/test_utils.py @@ -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 @@ -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