Source code for isaaclab.sim.utils.queries

# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Utilities for querying the USD stage."""

from __future__ import annotations

import logging
import re
from collections.abc import Callable

import omni
import omni.kit.app
from pxr import Sdf, Usd, UsdPhysics

from .stage import get_current_stage

# import logger
logger = logging.getLogger(__name__)


[docs]def get_next_free_prim_path(path: str, stage: Usd.Stage | None = None) -> str: """Gets a new prim path that doesn't exist in the stage given a base path. If the given path doesn't exist in the stage already, it returns the given path. Otherwise, it appends a suffix with an incrementing number to the given path. Args: path: The base prim path to check. stage: The stage to check. Defaults to the current stage. Returns: A new path that is guaranteed to not exist on the current stage Example: >>> import isaaclab.sim as sim_utils >>> >>> # given the stage: /World/Cube, /World/Cube_01. >>> # Get the next available path for /World/Cube >>> sim_utils.get_next_free_prim_path("/World/Cube") /World/Cube_02 """ # get current stage stage = get_current_stage() if stage is None else stage # get next free path return omni.usd.get_stage_next_free_path(stage, path, True)
[docs]def get_first_matching_ancestor_prim( prim_path: str | Sdf.Path, predicate: Callable[[Usd.Prim], bool], stage: Usd.Stage | None = None, ) -> Usd.Prim | None: """Gets the first ancestor prim that passes the predicate function. This function walks up the prim hierarchy starting from the target prim and returns the first ancestor prim that passes the predicate function. This includes the prim itself if it passes the predicate. 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. Returns: The first ancestor prim that passes the predicate. If no ancestor prim passes the predicate, it returns None. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """ # get stage handle if stage is None: stage = get_current_stage() # make paths str type if they aren't already prim_path = str(prim_path) # check if prim path is global if not prim_path.startswith("/"): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") # get prim prim = stage.GetPrimAtPath(prim_path) # check if prim is valid if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") # walk up to find the first matching ancestor prim ancestor_prim = prim while ancestor_prim and ancestor_prim.IsValid(): # check if prim passes predicate if predicate(ancestor_prim): return ancestor_prim # get parent prim ancestor_prim = ancestor_prim.GetParent() # If no ancestor prim passes the predicate, return None return None
[docs]def get_first_matching_child_prim( 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. 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. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """ # get stage handle if stage is None: stage = get_current_stage() # make paths str type if they aren't already prim_path = str(prim_path) # check if prim path is global if not prim_path.startswith("/"): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") # get prim prim = stage.GetPrimAtPath(prim_path) # check if prim is valid if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") # iterate over all prims under prim-path all_prims = [prim] while len(all_prims) > 0: # get current prim child_prim = all_prims.pop(0) # check if prim passes predicate if predicate(child_prim): return child_prim # add children to list if traverse_instance_prims: all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) else: all_prims += child_prim.GetChildren() return None
[docs]def get_all_matching_child_prims( prim_path: str | Sdf.Path, 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. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """ # get stage handle if stage is None: stage = get_current_stage() # make paths str type if they aren't already prim_path = str(prim_path) # check if prim path is global if not prim_path.startswith("/"): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") # get prim prim = stage.GetPrimAtPath(prim_path) # check if prim is valid if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") # check if depth is valid if depth is not None and depth <= 0: raise ValueError(f"Depth must be bigger than zero, got {depth}.") # iterate over all prims under prim-path # list of tuples (prim, current_depth) all_prims_queue = [(prim, 0)] output_prims = [] while len(all_prims_queue) > 0: # get current prim child_prim, current_depth = all_prims_queue.pop(0) # check if prim passes predicate if predicate(child_prim): output_prims.append(child_prim) # add children to list if depth is None or current_depth < depth: # 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
[docs]def find_first_matching_prim(prim_path_regex: str, stage: Usd.Stage | None = None) -> Usd.Prim | None: """Find the first matching prim in the stage based on input regex expression. Args: prim_path_regex: The regex expression for prim path. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: The first prim that matches input expression. If no prim matches, returns None. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """ # get stage handle if stage is None: stage = get_current_stage() # check prim path is global if not prim_path_regex.startswith("/"): raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") prim_path_regex = _normalize_legacy_wildcard_pattern(prim_path_regex) # need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the string pattern = f"^{prim_path_regex}$" compiled_pattern = re.compile(pattern) # obtain matching prim (depth-first search) for prim in stage.Traverse(): # check if prim passes predicate if compiled_pattern.match(prim.GetPath().pathString) is not None: return prim return None
def _normalize_legacy_wildcard_pattern(prim_path_regex: str) -> str: """Convert legacy '*' wildcard usage to '.*' and warn users.""" fixed_regex = re.sub(r"(?<![\\\.])\*", ".*", prim_path_regex) if fixed_regex != prim_path_regex: logger.warning( "Using '*' as a wildcard in prim path regex is deprecated; automatically converting '%s' to '%s'. " "Please update your pattern to use '.*' explicitly.", prim_path_regex, fixed_regex, ) return fixed_regex
[docs]def find_matching_prims(prim_path_regex: str, stage: Usd.Stage | None = None) -> list[Usd.Prim]: """Find all the matching prims in the stage based on input regex expression. Args: prim_path_regex: The regex expression for prim path. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: A list of prims that match input expression. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """ # get stage handle if stage is None: stage = get_current_stage() # normalize legacy wildcard pattern prim_path_regex = _normalize_legacy_wildcard_pattern(prim_path_regex) # check prim path is global if not prim_path_regex.startswith("/"): raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") # need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the string tokens = prim_path_regex.split("/")[1:] tokens = [f"^{token}$" for token in tokens] # iterate over all prims in stage (breath-first search) all_prims = [stage.GetPseudoRoot()] output_prims = [] for index, token in enumerate(tokens): token_compiled = re.compile(token) for prim in all_prims: for child in prim.GetAllChildren(): if token_compiled.match(child.GetName()) is not None: output_prims.append(child) if index < len(tokens) - 1: all_prims = output_prims output_prims = [] return output_prims
[docs]def find_matching_prim_paths(prim_path_regex: str, stage: Usd.Stage | None = None) -> list[str]: """Find all the matching prim paths in the stage based on input regex expression. Args: prim_path_regex: The regex expression for prim path. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: A list of prim paths that match input expression. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """ # obtain matching prims output_prims = find_matching_prims(prim_path_regex, stage) # convert prims to prim paths output_prim_paths = [] for prim in output_prims: output_prim_paths.append(prim.GetPath().pathString) return output_prim_paths
[docs]def find_global_fixed_joint_prim( prim_path: str | Sdf.Path, check_enabled_only: bool = False, stage: Usd.Stage | None = None ) -> UsdPhysics.Joint | None: """Find the fixed joint prim under the specified prim path that connects the target to the simulation world. A joint is a connection between two bodies. A fixed joint is a joint that does not allow relative motion between the two bodies. When a fixed joint has only one target body, it is considered to attach the body to the simulation world. This function finds the fixed joint prim that has only one target under the specified prim path. If no such fixed joint prim exists, it returns None. Args: prim_path: The prim path to search for the fixed joint prim. check_enabled_only: Whether to consider only enabled fixed joints. Defaults to False. If False, then all joints (enabled or disabled) are considered. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: The fixed joint prim that has only one target. If no such fixed joint prim exists, it returns None. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). ValueError: If the prim path does not exist on the stage. """ # get stage handle if stage is None: stage = get_current_stage() # check prim path is global if not prim_path.startswith("/"): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") # check if prim exists prim = stage.GetPrimAtPath(prim_path) if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") fixed_joint_prim = None # we check all joints under the root prim and classify the asset as fixed base if there exists # a fixed joint that has only one target (i.e. the root link). for prim in Usd.PrimRange(prim): # note: ideally checking if it is FixedJoint would have been enough, but some assets use "Joint" as the # schema name which makes it difficult to distinguish between the two. joint_prim = UsdPhysics.Joint(prim) if joint_prim: # if check_enabled_only is True, we only consider enabled joints if check_enabled_only and not joint_prim.GetJointEnabledAttr().Get(): continue # check body 0 and body 1 exist body_0_exist = joint_prim.GetBody0Rel().GetTargets() != [] body_1_exist = joint_prim.GetBody1Rel().GetTargets() != [] # if either body 0 or body 1 does not exist, we have a fixed joint that connects to the world if not (body_0_exist and body_1_exist): fixed_joint_prim = joint_prim break return fixed_joint_prim