diff --git a/source/isaaclab/changelog.d/octi-prim-path-real-regex-matcher.minor.rst b/source/isaaclab/changelog.d/octi-prim-path-real-regex-matcher.minor.rst new file mode 100644 index 000000000000..f06968d43458 --- /dev/null +++ b/source/isaaclab/changelog.d/octi-prim-path-real-regex-matcher.minor.rst @@ -0,0 +1,62 @@ +Added +^^^^^ + +* Added :attr:`~isaaclab.cloner.CloneCfg.clone_template` for the replicated environment prim path, + with ``{}`` marking the environment index. It replaces ``CloneCfg.clone_regex``, whose value is + now ``clone_template.format("[^/]+")``. +* Added an ``env_template`` argument to :func:`~isaaclab.cloner.make_clone_plan` and + :class:`~isaaclab.cloner.ReplicateSession`. +* Added :func:`~isaaclab.sim.utils.path_expr_to_glob` and + :func:`~isaaclab.sim.utils.split_path_expr`, for converting a prim path expression to the glob + the physics engines accept and for splitting one without cutting a character class in half. +* Added :func:`~isaaclab.cloner.expand_env_regex_ns`, and applied it when an asset or a sensor is + constructed. ``{ENV_REGEX_NS}`` previously only resolved for assets a + :class:`~isaaclab.scene.InteractiveScene` collected, so a direct environment -- which builds its + own -- had to spell the namespace out. Either kind may now use the macro, and no configuration + has to name the wildcard that selects one environment. + +Changed +^^^^^^^ + +* **Breaking:** Changed :func:`~isaaclab.sim.utils.find_matching_prims` to match the whole prim + path as a plain regular expression instead of one token per path segment. ``.`` now matches + ``/``, so ``/World/Robot/.*`` selects descendants at any depth; use ``[^/]+`` for a single + segment. Unscoped queries test every authored prim, including inactive and undefined prims and + instance proxies, without inferring a traversal root or depth limit from the expression. + Clone-aware discovery instead rebases the expression through the active clone plan and searches + only its concrete source subtree, never every cloned destination environment. +* Changed :func:`~isaaclab.sim.utils.find_first_matching_prim` to delegate to + :func:`~isaaclab.sim.utils.find_matching_prims`, so both read an expression the same way. +* Changed the environment namespace to spell its slot ``[^/]+`` rather than ``.*``, so + ``{ENV_REGEX_NS}/Robot`` no longer also selects a ``Robot`` nested deeper under an environment. +* Changed :func:`~isaaclab.cloner.path.match` to accept a character class in the clone slot, so a + segment-safe namespace resolves against a destination template. + +* Changed prim path expressions throughout the repository to spell a single path segment + ``[^/]`` rather than ``.``, so each pattern selects what it selected before now that ``.`` + matches ``/``. + +Removed +^^^^^^^ + +* Removed the legacy glob-wildcard rewrite from prim path expressions. A bare ``*`` is a regular + expression quantifier and is no longer rewritten to ``.*``; the rewrite could not tell a glob + star from a quantifier and corrupted ``[^/]*`` into ``[^/].*``. Patterns relying on ``*`` as a + standalone wildcard should spell it ``.*`` (any depth) or ``[^/]*`` (one path segment). + +Fixed +^^^^^ + +* Fixed :func:`~isaaclab.cloner.make_clone_plan` raising ``IndexError`` for a prim path holding + more than one wildcard, and ignoring a non-default environment namespace. +* Fixed :class:`~isaaclab.sensors.MultiMeshRayCaster` expanding ``{ENV_REGEX_NS}`` with a + hardcoded namespace instead of the shared default. +* Fixed callers that split a prim path expression on ``/`` cutting a ``[^/]`` character class in + half, which raised ``re.error: unterminated character set`` or produced a truncated body name. +* Fixed :func:`~isaaclab.sim.spawn_multi_asset` rejecting an index slot spelled ``[^/]*``; the + slot is now any segment wildcard rather than a literal ``.*``. +* Fixed callers that substituted a concrete environment index into a path expression by matching + one spelling of the environment slot, so a namespace written with a different quantifier was + left unresolved: the visualizer camera view, and the deformable render bindings. +* Fixed :func:`~isaaclab.cloner.query.path_to_source` reporting its destination as a glob, which + matched nothing when a caller used it as the path expression its name promises. diff --git a/source/isaaclab/isaaclab/assets/asset_base.py b/source/isaaclab/isaaclab/assets/asset_base.py index dc927ff5b835..bda87b386a35 100644 --- a/source/isaaclab/isaaclab/assets/asset_base.py +++ b/source/isaaclab/isaaclab/assets/asset_base.py @@ -17,6 +17,7 @@ import isaaclab.sim as sim_utils from isaaclab.cloner import queue_replication +from isaaclab.cloner.cloner_cfg import expand_env_regex_ns from isaaclab.physics import PhysicsEvent, PhysicsManager from isaaclab.sim.simulation_context import SimulationContext from isaaclab.sim.utils.stage import get_current_stage @@ -97,6 +98,10 @@ def __init__(self, cfg: AssetBaseCfg): """ # check that the config is valid cfg.validate() + # expand the namespace macro before the cfg is queued, so the clone plan keys its rows + # by a real path expression. The scene has already done this for the assets it collects; + # this covers the ones a direct environment builds itself. + cfg.prim_path = expand_env_regex_ns(cfg.prim_path) # register the original cfg object for cloning: the clone plan keys rows by the # cfg identity the scene collected; contexts and policy resolve at replication time queue_replication(cfg) diff --git a/source/isaaclab/isaaclab/assets/asset_base_cfg.py b/source/isaaclab/isaaclab/assets/asset_base_cfg.py index 69ae8a6733b3..de1409ee6843 100644 --- a/source/isaaclab/isaaclab/assets/asset_base_cfg.py +++ b/source/isaaclab/isaaclab/assets/asset_base_cfg.py @@ -64,7 +64,7 @@ class InitialStateCfg: The expression can contain the environment namespace regex ``{ENV_REGEX_NS}`` which will be replaced with the environment namespace. - Example: ``{ENV_REGEX_NS}/Robot`` will be replaced with ``/World/envs/env_.*/Robot``. + Example: ``{ENV_REGEX_NS}/Robot`` will be replaced with ``/World/envs/env_[^/]+/Robot``. """ spawn: SpawnerCfg | None = None diff --git a/source/isaaclab/isaaclab/cloner/__init__.pyi b/source/isaaclab/isaaclab/cloner/__init__.pyi index 1347936cacd2..35608216594b 100644 --- a/source/isaaclab/isaaclab/cloner/__init__.pyi +++ b/source/isaaclab/isaaclab/cloner/__init__.pyi @@ -10,6 +10,7 @@ __all__ = [ "add", "clone_plan_from_env_0", "disabled_fabric_change_notifies", + "expand_env_regex_ns", "filter_collisions", "grid_transforms", "make_clone_plan", @@ -37,7 +38,7 @@ from .clone_plan import ( make_valid_clone_combinations, num_spawn_variants, ) -from .cloner_cfg import CloneCfg, InclusionSet, add +from .cloner_cfg import CloneCfg, InclusionSet, add, expand_env_regex_ns from .cloner_strategies import random, sequential from .collision_filter import filter_collisions from .replicate_session import ( diff --git a/source/isaaclab/isaaclab/cloner/clone_plan.py b/source/isaaclab/isaaclab/cloner/clone_plan.py index 18e47aebea2c..98e04ed311d7 100644 --- a/source/isaaclab/isaaclab/cloner/clone_plan.py +++ b/source/isaaclab/isaaclab/cloner/clone_plan.py @@ -30,9 +30,9 @@ import isaaclab.sim as sim_utils -from .cloner_cfg import InclusionSet +from .cloner_cfg import DEFAULT_ENV_TEMPLATE, InclusionSet from .cloner_strategies import sequential -from .path import split +from .path import match @dataclass(frozen=True, eq=False) @@ -224,6 +224,7 @@ def make_clone_plan( *, clone_strategy: Callable = sequential, valid_set: torch.Tensor | None = None, + env_template: str = DEFAULT_ENV_TEMPLATE, ) -> ClonePlan: """Build a :class:`ClonePlan` from asset cfgs. @@ -266,22 +267,18 @@ def set_spawn_paths(spawn_cfg: Any, paths: list[str | None]) -> None: raise ValueError("Single spawner expects exactly one planned source path.") spawn_cfg.spawn_path = active[0] - env_root_marker = "/World/envs/" - env_template = "/World/envs/env_{}" - # 1) Build per-group records: (cfg, spawn_cfg, destination_template, num_variants). groups: list[tuple[Any, Any, str, int]] = [] for cfg in cfgs: if not hasattr(cfg, "prim_path") or not hasattr(cfg, "spawn") or cfg.spawn is None: continue prim_path = cfg.prim_path - if env_root_marker not in prim_path: + if (matched := match(prim_path, env_template)) is None: continue count = num_spawn_variants(cfg.spawn) if count <= 0: raise ValueError(f"Spawner at '{prim_path}' must have at least one variant.") - destination = prim_path.replace(".*", "{}") - groups.append((cfg, cfg.spawn, destination, count)) + groups.append((cfg, cfg.spawn, env_template + matched.suffix, count)) env_ids = torch.arange(num_clones, dtype=torch.long, device=device) positions, _ = grid_transforms(num_clones, env_spacing, device=device) @@ -408,9 +405,8 @@ def clone_plan_from_env_0( """ from .replicate_session import REPLICATION_QUEUE # noqa: PLC0415 - prefix, _ = split(destination) cfg_rows: dict[int, tuple[int, ...]] = { - id(cfg): (0,) for cfg in REPLICATION_QUEUE if cfg.prim_path.startswith(prefix) + id(cfg): (0,) for cfg in REPLICATION_QUEUE if match(cfg.prim_path, destination) is not None } return ClonePlan( sources=(source,), diff --git a/source/isaaclab/isaaclab/cloner/cloner_cfg.py b/source/isaaclab/isaaclab/cloner/cloner_cfg.py index b1cb2d11ffcf..b8f1c5c2fe1a 100644 --- a/source/isaaclab/isaaclab/cloner/cloner_cfg.py +++ b/source/isaaclab/isaaclab/cloner/cloner_cfg.py @@ -12,6 +12,28 @@ from .cloner_strategies import sequential +DEFAULT_ENV_TEMPLATE = "/World/envs/env_{}" +"""Default path template for a replicated env prim; ``{}`` marks the environment index.""" + + +def expand_env_regex_ns(path_expr: str, env_template: str = DEFAULT_ENV_TEMPLATE) -> str: + """Replace the ``{ENV_REGEX_NS}`` macro with the environment namespace it stands for. + + The macro spares a configuration from spelling the namespace, and with it the segment + wildcard that names one environment. :class:`~isaaclab.scene.InteractiveScene` expands it + against its own template for the assets it collects; assets built outside the scene (a + direct environment builds its own) go through here instead. + + Args: + path_expr: Prim path expression, with or without the macro. + env_template: Environment path template whose ``{}`` marks the environment index. + + Returns: + ``path_expr`` with the macro replaced, unchanged when it holds no macro. + """ + # a plain replace, not str.format: the rest of the expression may hold braces of its own + return path_expr.replace("{ENV_REGEX_NS}", env_template.format("[^/]+")) + @configclass class InclusionSet: @@ -46,8 +68,12 @@ class CloneCfg: device: str = "cpu" """Torch device on which mapping buffers are allocated.""" - clone_regex: str = "/World/envs/env_.*" - """Regex matching every replicated env prim. Used to expand ``{ENV_REGEX_NS}`` cfg macros.""" + clone_template: str = DEFAULT_ENV_TEMPLATE + """Path template for every replicated env prim, where ``{}`` is the environment index. + + The regex form used to expand ``{ENV_REGEX_NS}`` cfg macros is + ``clone_template.format("[^/]+")``, which confines the slot to one path segment. + """ replicate_physics: bool = True """Whether physics replication clones each environment. Default is True. diff --git a/source/isaaclab/isaaclab/cloner/path.py b/source/isaaclab/isaaclab/cloner/path.py index 26fe21708f3e..42e0f4f8a92a 100644 --- a/source/isaaclab/isaaclab/cloner/path.py +++ b/source/isaaclab/isaaclab/cloner/path.py @@ -56,8 +56,8 @@ def split(template: str) -> tuple[str, str]: def match(path_expr: str, template: str) -> TemplateMatch | None: """Match ``path_expr`` against a destination template, capturing the instance slot. - The ``"{}"`` slot matches one path segment's worth of text, whether a concrete id (``3``) - or a wildcard (``.*``). Recovering that text is the only way to tell which instance a + The ``"{}"`` slot matches one path segment's worth of text: a concrete id (``3``) or a + wildcard standing for one segment (``.*``, ``[^/]+``). Recovering that text is the only way to tell which instance a concrete clone path belongs to without slicing the string by hand. Args: @@ -73,7 +73,10 @@ def match(path_expr: str, template: str) -> TemplateMatch | None: TemplateMatch(instance='3', suffix='/base') """ prefix, template_suffix = split(template) - pattern = re.compile(re.escape(prefix) + r"([^/]+)" + re.escape(template_suffix)) + # the slot holds one segment's worth of text: a concrete id, or a wildcard standing for one. + # A segment-safe wildcard is written as a character class, whose text contains a '/' that is + # not a separator, so it is matched as a class rather than by the one-segment alternative. + pattern = re.compile(re.escape(prefix) + r"(\[\^?[^]]*\][*+?]?|[^/]+)" + re.escape(template_suffix)) matched = pattern.match(path_expr) if matched is None: return None diff --git a/source/isaaclab/isaaclab/cloner/query.py b/source/isaaclab/isaaclab/cloner/query.py index a47619075ccc..e36df47614fa 100644 --- a/source/isaaclab/isaaclab/cloner/query.py +++ b/source/isaaclab/isaaclab/cloner/query.py @@ -162,25 +162,26 @@ def path_to_source(plan: ClonePlan, path_expr: str, env_id: int | None = None) - A *concrete* clone path names its environment in the template's clone slot, and that environment selects which variant to report — which is what lets this undo :func:`path_to_clone` for a heterogeneous asset. A *wildcard* expression - (``.../env_.*/...``) names no environment and stands for all of them, so it resolves to + (``.../env_[^/]+/...``) names no environment and stands for all of them, so it resolves to the first populated variant unless ``env_id`` says which one to take. Args: plan: Active clone plan. - path_expr: Clone-side path expression (e.g. a sensor's ``prim_path``, with ``.*`` env - wildcard) or a concrete clone path. + path_expr: Clone-side path expression (e.g. a sensor's ``prim_path``, with a segment + wildcard in the env slot) or a concrete clone path. env_id: Environment whose variant to resolve. Defaults to the one ``path_expr`` names when it is concrete, and to no particular environment otherwise. Returns: - A ``(source_path, destination_glob, asset_suffix)`` tuple, where ``asset_suffix`` is - the part of ``path_expr`` below the owning template. ``None`` when ``path_expr`` - matches no row, or no matching row populates the requested environment, letting - callers fall back to direct stage resolution. + A ``(source_path, destination_expr, asset_suffix)`` tuple, where ``destination_expr`` + spells the clone slot ``[^/]+`` so it reads as a path expression like every other one, + and ``asset_suffix`` is the part of ``path_expr`` below the owning template. ``None`` + when ``path_expr`` matches no row, or no matching row populates the requested + environment, letting callers fall back to direct stage resolution. Partial-env coverage is supported: when the matching rows cover only a subset of envs (an asset present in some envs but not others, as in heterogeneous scenes), the - returned glob resolves to just those envs. + returned expression resolves to just those envs. Raises: ValueError: When ``path_expr`` is owned by multiple distinct, equally near templates. @@ -202,7 +203,7 @@ def path_to_source(plan: ClonePlan, path_expr: str, env_id: int | None = None) - rows = [row for row in rows if bool(plan.clone_mask[row][column])] if not rows: return None - return plan.sources[rows[0]], template.replace("{}", "*"), matched.suffix + return plan.sources[rows[0]], template.format("[^/]+"), matched.suffix def iter_sources(plan: ClonePlan, path_expr: str) -> Iterator[tuple[str, str, str, tuple[int, ...]]]: @@ -214,7 +215,7 @@ def iter_sources(plan: ClonePlan, path_expr: str) -> Iterator[tuple[str, str, st Example: For a row with prototype root ``"/World/source/Robot"``, destination template ``"/World/scenes/{}/Robot"`` and env ids ``(0, 2)``, querying - ``"/World/scenes/.*/Robot/base"`` yields ``("/World/source/Robot", + ``"/World/scenes/[^/]+/Robot/base"`` yields ``("/World/source/Robot", "/World/scenes/{}/Robot", "/World/source/Robot/base", (0, 2))``. Args: diff --git a/source/isaaclab/isaaclab/cloner/replicate_session.py b/source/isaaclab/isaaclab/cloner/replicate_session.py index 35bccdeae7b5..7efe316d1ce0 100644 --- a/source/isaaclab/isaaclab/cloner/replicate_session.py +++ b/source/isaaclab/isaaclab/cloner/replicate_session.py @@ -16,6 +16,7 @@ from isaaclab.utils.version import has_kit from .clone_plan import make_clone_plan +from .cloner_cfg import DEFAULT_ENV_TEMPLATE from .cloner_strategies import sequential from .usd import UsdReplicateContext @@ -141,6 +142,7 @@ def __init__( clone_strategy: Callable = sequential, valid_set: torch.Tensor | None = None, replicate_physics: bool = True, + env_template: str = DEFAULT_ENV_TEMPLATE, ): """Capture arguments for :func:`make_clone_plan` and :func:`replicate`. @@ -155,6 +157,7 @@ def __init__( prototype combinations; ``None`` uses the full cartesian product. replicate_physics: Whether physics replication clones each environment; forwarded to :func:`replicate`. + env_template: Path template for a replicated env prim, ``{}`` marking the env index. """ self._cfgs = cfgs self._stage = stage @@ -165,6 +168,7 @@ def __init__( device=device, clone_strategy=clone_strategy, valid_set=valid_set, + env_template=env_template, ) self._plan: ClonePlan | None = None diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py index 51437849afc9..ddc994ec52b0 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py @@ -355,7 +355,7 @@ def has_rigid_body_api(prim) -> bool: if not rigid_matches: raise ValueError(f"No descendant rigid body found under the expression: '{self._asset.cfg.prim_path}'.") _, root_rigidbody_path = rigid_matches[0] - task_frame_transformer_path = "/World/envs/env_.*/" + self.cfg.task_frame_rel_path + task_frame_transformer_path = f"{self._env.scene.env_regex_ns}/{self.cfg.task_frame_rel_path}" task_frame_transformer_cfg = FrameTransformerCfg( prim_path=root_rigidbody_path, target_frames=[ diff --git a/source/isaaclab/isaaclab/envs/mdp/events.py b/source/isaaclab/isaaclab/envs/mdp/events.py index 2377bae80c9b..3c6b5de833aa 100644 --- a/source/isaaclab/isaaclab/envs/mdp/events.py +++ b/source/isaaclab/isaaclab/envs/mdp/events.py @@ -2560,12 +2560,8 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): # join all bodies in the asset body_names = asset_cfg.body_names - if isinstance(body_names, str): - body_names_regex = body_names - elif isinstance(body_names, list): - body_names_regex = "|".join(body_names) - else: - body_names_regex = ".*" + body_names_regex = "|".join(body_names) if isinstance(body_names, list) else body_names + body_names_regex = f"(?:{body_names_regex})" if isinstance(body_names_regex, str) else ".*" # create the affected prim path # Check if the pattern with '/visuals' yields results when matching `body_names_regex`. @@ -2573,7 +2569,7 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): asset_main_prim_path = asset.cfg.prim_path pattern_with_visuals = f"{asset_main_prim_path}/{body_names_regex}/visuals" # Use sim_utils to check if any prims currently match this pattern - matching_prims = sim_utils.find_matching_prim_paths(pattern_with_visuals) + matching_prims = sim_utils.resolve_matching_prims_from_source(pattern_with_visuals, raise_if_no_matches=False) if matching_prims: # If matches are found, use the pattern with /visuals prim_path = pattern_with_visuals @@ -2751,14 +2747,10 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): else: # default: the configured bodies' visual meshes body_names = asset_cfg.body_names - if isinstance(body_names, str): - body_names_regex = body_names - elif isinstance(body_names, list): - body_names_regex = "|".join(body_names) - else: - body_names_regex = ".*" + body_names_regex = "|".join(body_names) if isinstance(body_names, list) else body_names + body_names_regex = f"(?:{body_names_regex})" if isinstance(body_names_regex, str) else ".*" pattern_with_visuals = f"{asset.cfg.prim_path}/{body_names_regex}/visuals" - if sim_utils.find_matching_prim_paths(pattern_with_visuals): + if sim_utils.resolve_matching_prims_from_source(pattern_with_visuals, raise_if_no_matches=False): mesh_prim_path = pattern_with_visuals else: # fall back to any descendant if the asset has no ".../visuals" layout diff --git a/source/isaaclab/isaaclab/envs/utils/camera_view.py b/source/isaaclab/isaaclab/envs/utils/camera_view.py index 923895aaa639..ef20cc22cfa0 100644 --- a/source/isaaclab/isaaclab/envs/utils/camera_view.py +++ b/source/isaaclab/isaaclab/envs/utils/camera_view.py @@ -10,6 +10,7 @@ import logging import math import random +import re from typing import Any import numpy as np @@ -72,6 +73,9 @@ def resolve_mono_env_index(num_envs: int) -> list[int]: return [0] if num_envs > 0 else [] +_ENV_SLOT_WILDCARD = re.compile(r"env_(?:\[\^/\][*+]|\.\*)") + + def env_path_from_template(path_template: str, env_id: int) -> str: """Resolve common env wildcard/template spellings to a concrete env path.""" path = path_template @@ -80,9 +84,9 @@ def env_path_from_template(path_template: str, env_id: int) -> str: if "{}" in path: return path.format(env_id) path = path.replace("/World/envs/*", f"/World/envs/env_{env_id}") - path = path.replace("/World/envs/env_.*", f"/World/envs/env_{env_id}") - path = path.replace("/World/envs/env_.*/", f"/World/envs/env_{env_id}/") - return path + # the env slot is a segment wildcard; match every spelling rather than one, so a namespace + # written with a different quantifier still resolves to a concrete env. + return _ENV_SLOT_WILDCARD.sub(f"env_{env_id}", path) def _camera_concrete_paths(camera: Camera) -> list[str]: @@ -382,7 +386,7 @@ def create_visualizer_camera( attr = cam_prim.CreateAttribute("omni:scenePartition", Sdf.ValueTypeNames.Token) attr.Set(path.split("/")[-2]) cfg = CameraCfg( - prim_path=f"/World/envs/env_.*/{camera_name}", + prim_path=f"/World/envs/env_[^/]+/{camera_name}", update_period=0.0, height=int(height), width=int(width), diff --git a/source/isaaclab/isaaclab/scene/interactive_scene.py b/source/isaaclab/isaaclab/scene/interactive_scene.py index a22a79e091e9..f3a8b04a050a 100644 --- a/source/isaaclab/isaaclab/scene/interactive_scene.py +++ b/source/isaaclab/isaaclab/scene/interactive_scene.py @@ -162,9 +162,8 @@ def __init__(self, cfg: InteractiveSceneCfg): self.cloner_cfg = copy.deepcopy(self.cfg.clone_cfg) self.cloner_cfg.device = self.device self.cloner_cfg.replicate_physics = self.cfg.replicate_physics - self._env_regex_ns = self.cloner_cfg.clone_regex - self._env_fmt = self._env_regex_ns.replace(".*", "{}") - self._env_ns = self._env_regex_ns.rsplit("/", 1)[0] + # the template is authoritative; the regex form is the same namespace spelled for matching + self._env_fmt = self.cloner_cfg.clone_template self.env_prim_paths = [self._env_fmt.format(i) for i in range(self.cfg.num_envs)] self._scene_asset_names: list[str] = [] self._clone_valid_set: torch.Tensor | None = None @@ -192,6 +191,7 @@ def __init__(self, cfg: InteractiveSceneCfg): num_clones=self.num_envs, env_spacing=self.cfg.env_spacing, device=self.device, + env_template=self._env_fmt, stage=self.stage, clone_strategy=self.cloner_cfg.clone_strategy, valid_set=self._clone_valid_set, @@ -228,8 +228,8 @@ def _collect_asset_cfgs(self) -> list[Any]: ) for child in children: if hasattr(child, "prim_path"): - child.prim_path = child.prim_path.format(ENV_REGEX_NS=self.cloner_cfg.clone_regex) - if hasattr(child, "spawn") and child.spawn is not None and self.env_ns in child.prim_path: + child.prim_path = cloner.expand_env_regex_ns(child.prim_path, self._env_fmt) + if getattr(child, "spawn", None) is not None and cloner.path.match(child.prim_path, self._env_fmt): clone_asset_names.append(asset_name) variant_counts.append(cloner.num_spawn_variants(child.spawn)) cfgs.append(child) @@ -390,12 +390,12 @@ def device(self) -> str: @property def env_ns(self) -> str: """The namespace ``/World/envs`` in which all environments are created.""" - return self._env_ns + return self._env_fmt.rsplit("/", 1)[0] @property def env_regex_ns(self) -> str: - """The namespace ``/World/envs/env_.*`` in which all environments are created.""" - return self._env_regex_ns + """The namespace ``/World/envs/env_[^/]+`` in which all environments are created.""" + return self._env_fmt.format("[^/]+") @property def num_envs(self) -> int: @@ -836,15 +836,12 @@ def _add_entities_from_cfg(self): # noqa: C901 ] for asset_name, asset_cfg in ordered_items: - # resolve prim_path with env regex - if hasattr(asset_cfg, "prim_path"): - asset_cfg.prim_path = asset_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) # set spawn_path on spawner if cloning is needed if hasattr(asset_cfg, "spawn") and asset_cfg.spawn is not None: is_multi_spawner = isinstance( asset_cfg.spawn, (sim_utils.MultiAssetSpawnerCfg, sim_utils.MultiUsdFileCfg) ) - if self.env_ns not in asset_cfg.prim_path: + if cloner.path.match(asset_cfg.prim_path, self._env_fmt) is None: asset_cfg.spawn.spawn_path = asset_cfg.prim_path elif is_multi_spawner and not asset_cfg.spawn.spawn_paths: raise RuntimeError(f"Clone planning did not assign spawn_paths for '{asset_cfg.prim_path}'.") @@ -866,13 +863,12 @@ def _add_entities_from_cfg(self): # noqa: C901 self._rigid_objects[asset_name] = asset_cfg.class_type(asset_cfg) elif isinstance(asset_cfg, RigidObjectCollectionCfg): for rigid_object_cfg in asset_cfg.rigid_objects.values(): - rigid_object_cfg.prim_path = rigid_object_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) # set spawn_path on spawner if cloning is needed if hasattr(rigid_object_cfg, "spawn") and rigid_object_cfg.spawn is not None: is_multi_spawner = isinstance( rigid_object_cfg.spawn, (sim_utils.MultiAssetSpawnerCfg, sim_utils.MultiUsdFileCfg) ) - if self.env_ns not in rigid_object_cfg.prim_path: + if cloner.path.match(rigid_object_cfg.prim_path, self._env_fmt) is None: rigid_object_cfg.spawn.spawn_path = rigid_object_cfg.prim_path elif is_multi_spawner and not rigid_object_cfg.spawn.spawn_paths: raise RuntimeError( @@ -893,34 +889,31 @@ def _add_entities_from_cfg(self): # noqa: C901 elif isinstance(asset_cfg, SensorBaseCfg): # Update target frame path(s)' regex name space for FrameTransformer if isinstance(asset_cfg, FrameTransformerCfg): - updated_target_frames = [] for target_frame in asset_cfg.target_frames: - target_frame.prim_path = target_frame.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) - updated_target_frames.append(target_frame) - asset_cfg.target_frames = updated_target_frames + target_frame.prim_path = cloner.expand_env_regex_ns(target_frame.prim_path, self._env_fmt) elif isinstance(asset_cfg, ContactSensorCfg): asset_cfg.filter_prim_paths_expr = [ - p.format(ENV_REGEX_NS=self.env_regex_ns) for p in asset_cfg.filter_prim_paths_expr + cloner.expand_env_regex_ns(p, self._env_fmt) for p in asset_cfg.filter_prim_paths_expr ] if hasattr(asset_cfg, "sensor_shape_prim_expr") and asset_cfg.sensor_shape_prim_expr: asset_cfg.sensor_shape_prim_expr = [ - p.format(ENV_REGEX_NS=self.env_regex_ns) for p in asset_cfg.sensor_shape_prim_expr + cloner.expand_env_regex_ns(p, self._env_fmt) for p in asset_cfg.sensor_shape_prim_expr ] if hasattr(asset_cfg, "filter_shape_prim_expr") and asset_cfg.filter_shape_prim_expr: asset_cfg.filter_shape_prim_expr = [ - p.format(ENV_REGEX_NS=self.env_regex_ns) for p in asset_cfg.filter_shape_prim_expr + cloner.expand_env_regex_ns(p, self._env_fmt) for p in asset_cfg.filter_shape_prim_expr ] elif isinstance(asset_cfg, VisuoTactileSensorCfg): if hasattr(asset_cfg, "camera_cfg") and asset_cfg.camera_cfg is not None: - asset_cfg.camera_cfg.prim_path = asset_cfg.camera_cfg.prim_path.format( - ENV_REGEX_NS=self.env_regex_ns + asset_cfg.camera_cfg.prim_path = cloner.expand_env_regex_ns( + asset_cfg.camera_cfg.prim_path, self._env_fmt ) if ( hasattr(asset_cfg, "contact_object_prim_path_expr") and asset_cfg.contact_object_prim_path_expr is not None ): - asset_cfg.contact_object_prim_path_expr = asset_cfg.contact_object_prim_path_expr.format( - ENV_REGEX_NS=self.env_regex_ns + asset_cfg.contact_object_prim_path_expr = cloner.expand_env_regex_ns( + asset_cfg.contact_object_prim_path_expr, self._env_fmt ) self._sensors[asset_name] = asset_cfg.class_type(asset_cfg) diff --git a/source/isaaclab/isaaclab/scene_data/deformable_discovery.py b/source/isaaclab/isaaclab/scene_data/deformable_discovery.py index 3c16359b5cc8..bca6ebe26fb2 100644 --- a/source/isaaclab/isaaclab/scene_data/deformable_discovery.py +++ b/source/isaaclab/isaaclab/scene_data/deformable_discovery.py @@ -326,8 +326,8 @@ def path_to_env_wildcard(path: str) -> str: def path_to_env_regex(path: str) -> str: - """Rewrite ``env_`` segments to ``env_.*`` for Isaac Lab asset regex paths.""" - return re.sub(r"/World/envs/env_\d+", "/World/envs/env_.*", path) + """Rewrite ``env_`` segments to ``env_[^/]+`` for Isaac Lab asset regex paths.""" + return re.sub(r"/World/envs/env_\d+", "/World/envs/env_[^/]+", path) def build_deformable_vertex_count_lookup(entries: list[DeformableStageEntry]) -> dict[str, int]: diff --git a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py index 2e1b0156e005..09a7c0228ff8 100644 --- a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py +++ b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py @@ -83,7 +83,7 @@ class ContactSensorCfg(SensorBaseCfg): Expressions can contain the environment namespace regex ``{ENV_REGEX_NS}``, which is replaced with the environment namespace. - Example: ``{ENV_REGEX_NS}/Object`` becomes ``/World/envs/env_.*/Object``. + Example: ``{ENV_REGEX_NS}/Object`` becomes ``/World/envs/env_[^/]+/Object``. .. attention:: Filtered contact reporting only works when :attr:`SensorBaseCfg.prim_path` matches a diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/base_multi_mesh_ray_caster.py b/source/isaaclab/isaaclab/sensors/ray_caster/base_multi_mesh_ray_caster.py index 2b1206906b70..ac058c22e6d0 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/base_multi_mesh_ray_caster.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/base_multi_mesh_ray_caster.py @@ -6,6 +6,7 @@ from __future__ import annotations import logging +import re from typing import TYPE_CHECKING import numpy as np @@ -84,10 +85,10 @@ class BaseMultiMeshRayCaster(BaseRayCaster): prim_path="{ENV_REGEX_NS}/Robot", mesh_prim_paths=[ "/World/Ground", - MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LF_.*/visuals"), - MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RF_.*/visuals"), - MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LH_.*/visuals"), - MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RH_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LF_[^/]*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RF_[^/]*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LH_[^/]*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RH_[^/]*/visuals"), MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/base/visuals"), ], ray_alignment="world", @@ -115,7 +116,7 @@ def __init__(self, cfg: MultiMeshRayCasterCfg): target_cfg = cfg.RaycastTargetCfg(prim_expr=target, track_mesh_transforms=False) else: target_cfg = target - target_cfg.prim_expr = target_cfg.prim_expr.format(ENV_REGEX_NS="/World/envs/env_.*") + target_cfg.prim_expr = cloner.expand_env_regex_ns(target_cfg.prim_expr) self._raycast_targets_cfg.append(target_cfg) self._data = MultiMeshRayCasterData() @@ -215,7 +216,10 @@ def _build_mesh_records( target_in_plan = True # Load meshes from the authored source entry. - source_prims = sim_utils.find_matching_prims(source_path) + source_pattern = re.compile(source_path) + source_prims = sim_utils.get_all_matching_child_prims( + source_root, lambda prim: source_pattern.fullmatch(prim.GetPath().pathString) is not None + ) if not source_prims: raise RuntimeError(f"No ClonePlan source prims matched '{source_path}'.") @@ -255,7 +259,7 @@ def _build_mesh_records( f"Tracked target owner '{owner_path}' is not under ClonePlan source root " f"'{source_root}'." ) - row_tracked_target_exprs.append(destination_template.format(".*") + owner_suffix) + row_tracked_target_exprs.append(destination_template.format("[^/]+") + owner_suffix) if len(row_tracked_target_exprs) > len(plan_tracked_target_exprs): plan_tracked_target_exprs = row_tracked_target_exprs diff --git a/source/isaaclab/isaaclab/sensors/sensor_base.py b/source/isaaclab/isaaclab/sensors/sensor_base.py index 544f3874f3a3..c67bce439af7 100644 --- a/source/isaaclab/isaaclab/sensors/sensor_base.py +++ b/source/isaaclab/isaaclab/sensors/sensor_base.py @@ -22,6 +22,7 @@ import isaaclab.sim as sim_utils from isaaclab import cloner +from isaaclab.cloner.cloner_cfg import expand_env_regex_ns from isaaclab.physics import PhysicsEvent, PhysicsManager from isaaclab.sim.utils.queries import get_first_matching_ancestor_prim from isaaclab.sim.utils.transforms import resolve_prim_pose @@ -56,6 +57,9 @@ def __init__(self, cfg: SensorBaseCfg): """ # check that the config is valid cfg.validate() + # expand the namespace macro for sensors built outside the scene, which has already + # expanded it for the ones it collects + cfg.prim_path = expand_env_regex_ns(cfg.prim_path) # store inputs self._source_cfg = cfg self.cfg = cfg.copy() @@ -236,11 +240,11 @@ def _initialize_impl(self): self._parent_prims = [] self._num_envs = int(clone_plan.clone_mask.shape[1]) elif clone_plan is not None: - env_prim_path_expr = self.cfg.prim_path.rsplit("/", 1)[0] + env_prim_path_expr = "/".join(sim_utils.split_path_expr(self.cfg.prim_path)[:-1]) self._parent_prims = sim_utils.find_matching_prims(env_prim_path_expr) self._num_envs = int(clone_plan.env_ids.numel()) else: - env_prim_path_expr = self.cfg.prim_path.rsplit("/", 1)[0] + env_prim_path_expr = "/".join(sim_utils.split_path_expr(self.cfg.prim_path)[:-1]) self._parent_prims = sim_utils.find_matching_prims(env_prim_path_expr) self._num_envs = len(self._parent_prims) # Create warp env mask arrays for "all envs" cases and resets. @@ -447,7 +451,7 @@ def _resolve_rigid_body_ancestor_expr( The returned expression may still contain regex-style wildcards (e.g. ``.*``); callers are responsible for converting to glob form for their - physics view (e.g. ``.replace(".*", "*")``). + physics view (e.g. via :func:`~isaaclab.sim.utils.path_expr_to_glob`). Returns: A tuple of: diff --git a/source/isaaclab/isaaclab/sensors/sensor_base_cfg.py b/source/isaaclab/isaaclab/sensors/sensor_base_cfg.py index 08bd20df507c..85ca01aab4eb 100644 --- a/source/isaaclab/isaaclab/sensors/sensor_base_cfg.py +++ b/source/isaaclab/isaaclab/sensors/sensor_base_cfg.py @@ -38,7 +38,7 @@ class SensorBaseCfg: The expression can contain the environment namespace regex ``{ENV_REGEX_NS}`` which will be replaced with the environment namespace. - Example: ``{ENV_REGEX_NS}/Robot/sensor`` will be replaced with ``/World/envs/env_.*/Robot/sensor``. + Example: ``{ENV_REGEX_NS}/Robot/sensor`` will be replaced with ``/World/envs/env_[^/]+/Robot/sensor``. """ diff --git a/source/isaaclab/isaaclab/sim/__init__.pyi b/source/isaaclab/isaaclab/sim/__init__.pyi index 1ac7aed7907f..bc31fef54968 100644 --- a/source/isaaclab/isaaclab/sim/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/__init__.pyi @@ -192,6 +192,8 @@ __all__ = [ "find_first_matching_prim", "find_matching_prims", "matches_path_expr_prefix", + "path_expr_to_glob", + "split_path_expr", "resolve_matching_prims_from_source", "find_matching_prim_paths", "find_global_fixed_joint_prim", @@ -438,6 +440,8 @@ from .utils import ( is_prim_path_valid, make_uninstanceable, matches_path_expr_prefix, + path_expr_to_glob, + split_path_expr, open_stage, remove_labels, resolve_matching_prims_from_source, diff --git a/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py b/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py index f6f087cfa129..b80f2f8fe25b 100644 --- a/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py +++ b/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py @@ -51,14 +51,19 @@ def spawn_multi_asset( ) asset_prim_paths = list(cfg.spawn_paths) else: - split_path = prim_path.split("/") + # split on separators only: a segment wildcard is written as a character class whose + # text contains a '/' that is not a separator. + split_path = sim_utils.split_path_expr(prim_path) prefix_path, base_name = "/".join(split_path[:-1]), split_path[-1] - if ".*" not in base_name: + # the base name carries the index slot as a segment wildcard, in any of its spellings. + # Normalizing to glob collapses them to the single '*' that the index replaces. + base_glob = sim_utils.path_expr_to_glob(base_name) + if "*" not in base_glob: raise ValueError( - f" The base name '{base_name}' in the prim path '{prim_path}' must contain '.*' to indicate" - " the path each individual multiple-asset to be spawned." + f" The base name '{base_name}' in the prim path '{prim_path}' must contain a segment wildcard" + " (e.g. '.*' or '[^/]*') to indicate the path each individual multiple-asset to be spawned." ) - asset_prim_paths = [f"{prefix_path}/{base_name.replace('.*', str(i))}" for i in range(len(cfg.assets_cfg))] + asset_prim_paths = [f"{prefix_path}/{base_glob.replace('*', str(i))}" for i in range(len(cfg.assets_cfg))] if cfg.random_choice: logger.warning( diff --git a/source/isaaclab/isaaclab/sim/utils/__init__.pyi b/source/isaaclab/isaaclab/sim/utils/__init__.pyi index 44a8b0ecf9e0..b37b8b6e035f 100644 --- a/source/isaaclab/isaaclab/sim/utils/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/utils/__init__.pyi @@ -35,6 +35,8 @@ __all__ = [ "find_first_matching_prim", "find_matching_prims", "matches_path_expr_prefix", + "path_expr_to_glob", + "split_path_expr", "resolve_matching_prims_from_source", "find_matching_prim_paths", "find_global_fixed_joint_prim", @@ -105,6 +107,8 @@ from .queries import ( find_first_matching_prim, find_matching_prims, matches_path_expr_prefix, + path_expr_to_glob, + split_path_expr, resolve_matching_prims_from_source, find_matching_prim_paths, find_global_fixed_joint_prim, diff --git a/source/isaaclab/isaaclab/sim/utils/prims.py b/source/isaaclab/isaaclab/sim/utils/prims.py index 417daa1bb086..a7845f65debb 100644 --- a/source/isaaclab/isaaclab/sim/utils/prims.py +++ b/source/isaaclab/isaaclab/sim/utils/prims.py @@ -20,7 +20,13 @@ from isaaclab.utils.string import to_camel_case from isaaclab.utils.version import has_kit -from .queries import find_matching_prim_paths, has_deformable_body_api, has_deformable_curve_api +from .queries import ( + find_matching_prim_paths, + has_deformable_body_api, + has_deformable_curve_api, + path_expr_to_glob, + split_path_expr, +) from .semantics import add_labels from .stage import get_current_stage, resolve_paths from .transforms import convert_world_pose_to_local, standardize_xform_ops @@ -698,7 +704,10 @@ def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") # resolve: {SPAWN_NS}/AssetName # note: this assumes that the spawn namespace already exists in the stage - root_path, asset_path = prim_path.rsplit("/", 1) + # split on separators only: a segment wildcard is written as a character class whose + # text contains a '/' that is not a separator. + *root_segments, asset_path = split_path_expr(prim_path) + root_path = "/".join(root_segments) # check if input is a regex expression # note: a valid prim path can only contain alphanumeric characters, underscores, and forward slashes is_regex_expression = re.match(r"^[a-zA-Z0-9/_]+$", root_path) is None @@ -755,7 +764,9 @@ def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs): _schemas.activate_contact_sensors(prim_spawn_path) # clone asset using cloner API if len(source_prim_paths) > 1: - sanitized_asset = asset_path.replace(".*", "0") + # the leaf may carry an index slot as a segment wildcard; normalizing to glob + # collapses its spellings to the single '*' that the index replaces. + sanitized_asset = path_expr_to_glob(asset_path).replace("*", "0") rl = stage.GetRootLayer() with Sdf.ChangeBlock(): for src_parent in source_prim_paths[1:]: diff --git a/source/isaaclab/isaaclab/sim/utils/queries.py b/source/isaaclab/isaaclab/sim/utils/queries.py index c2f401dbe611..f09223ff7250 100644 --- a/source/isaaclab/isaaclab/sim/utils/queries.py +++ b/source/isaaclab/isaaclab/sim/utils/queries.py @@ -9,7 +9,7 @@ import logging import re -from collections.abc import Callable +from collections.abc import Callable, Iterator from typing import TYPE_CHECKING from isaaclab import cloner @@ -23,6 +23,29 @@ # import logger logger = logging.getLogger(__name__) +_CHARACTER_CLASS = re.compile(r"\[\^?[^]]*\]") +"""Matches a regex character class, whose text may hold a ``/`` that is not a path separator.""" + +_SEGMENT_WILDCARD = re.compile(r"\[\^/\][*+]|\.\*") +"""Matches the ways an expression spells "anything within one path segment".""" + + +def path_expr_to_glob(path_expr: str) -> str: + """Convert a prim path expression to the glob syntax the physics engines accept. + + Physics views take a glob, where ``*`` spans one path segment. The spellings supported by + Isaac Lab's adapters -- ``.*`` and the segment-safe ``[^/]*`` / ``[^/]+`` -- map onto it. + Other regular-expression constructs pass through unchanged; this function does not attempt + to translate arbitrary Python regular expressions into globs. + + Args: + path_expr: The prim path expression to convert. + + Returns: + The equivalent glob. + """ + return _SEGMENT_WILDCARD.sub("*", path_expr) + 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. @@ -300,6 +323,9 @@ def get_all_matching_child_prims( 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. + The candidate set is identical to :func:`find_matching_prims`: all authored prims exposed by + the stage, including inactive and undefined prims as well as instance proxies. + 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. @@ -310,47 +336,64 @@ def find_first_matching_prim(prim_path_regex: str, stage: Usd.Stage | None = Non 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() + stage = get_current_stage() if stage is None else stage + return next(_iter_matching_prims_in_subtree(prim_path_regex, stage.GetPseudoRoot()), None) - # 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 split_path_expr(path_expr: str) -> list[str]: + """Split a path expression on its separators, ignoring any inside a character class. -def _normalize_legacy_wildcard_pattern(prim_path_regex: str) -> str: - """Convert legacy '*' wildcard usage to '.*' and warn users.""" - fixed_regex = re.sub(r"(? bool: """Return whether ``prim_path`` matches ``path_expr`` up to ``prim_path`` depth.""" - prefix_expr = "/".join(path_expr.split("/")[: prim_path.count("/") + 1]) - return re.match(f"^{_normalize_legacy_wildcard_pattern(prefix_expr)}$", prim_path) is not None + prefix_expr = "/".join(split_path_expr(path_expr)[: prim_path.count("/") + 1]) + return re.fullmatch(prefix_expr, prim_path) is not None + + +def _iter_matching_prims_in_subtree(prim_path_regex: str, root_prim: Usd.Prim) -> Iterator[Usd.Prim]: + """Yield full-path regex matches from an explicitly supplied subtree.""" + from pxr import Usd # noqa: PLC0415 + + if not prim_path_regex.startswith("/"): + raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") + pattern = re.compile(prim_path_regex) + if not root_prim.IsValid(): + return + predicate = Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate) + for prim in Usd.PrimRange(root_prim, predicate): + if pattern.fullmatch(prim.GetPath().pathString) is not None: + yield prim 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. + The expression is a plain Python regular expression matched against the *whole* prim path. + Standard regex semantics apply: ``.`` matches any character including ``/``, so + ``/World/Robot/.*`` selects every descendant at any depth, while ``[^/]+`` confines a + wildcard to a single path segment. Every prim on the stage is tested; the expression does not + imply a traversal root or depth limit. The traversal includes inactive and undefined prims + as well as instance proxies. + 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. @@ -361,39 +404,15 @@ def find_matching_prims(prim_path_regex: str, stage: Usd.Stage | None = 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() - - # 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 + stage = get_current_stage() if stage is None else stage + return list(_iter_matching_prims_in_subtree(prim_path_regex, stage.GetPseudoRoot())) def resolve_matching_prims_from_source( path_expr: str, predicate: Callable[[Usd.Prim], bool] | None = None, expected_num_matches: int | None = None, - env_regex_ns: str = "/World/envs/env_.*", + env_regex_ns: str = "/World/envs/env_[^/]+", raise_if_no_matches: bool = True, traverse_instance_prims: bool = True, ) -> list[tuple[Usd.Prim, str]]: @@ -420,10 +439,12 @@ def resolve_matching_prims_from_source( plan = SimulationContext.instance().get_clone_plan() resolved = cloner.query.path_to_source(plan, path_expr) if plan is not None else None if resolved is not None: - source_path, dest_glob, asset_suffix = resolved - walk_root = source_path + asset_suffix + source_path, dest_expr, asset_suffix = resolved + source_expr = source_path + asset_suffix + source_prim = get_current_stage().GetPrimAtPath(source_path) results = [ - (prim, dest_glob + prim.GetPath().pathString[len(source_path) :]) for prim in find_matching_prims(walk_root) + (prim, dest_expr + prim.GetPath().pathString[len(source_path) :]) + for prim in _iter_matching_prims_in_subtree(source_expr, source_prim) ] else: # No clone plan, or ``path_expr`` is not owned by any plan row. Resolve from the stage @@ -431,8 +452,8 @@ def resolve_matching_prims_from_source( # search from, (2) collect the bodies of interest within just that instance and map each # back to the multi-instance pattern. Phase 1 stops at the first match and phase 2 walks # under a concrete instance prefix, so only a single instance subtree is traversed. - segments = path_expr.strip("/").split("/") - ns_segments = env_regex_ns.strip("/").split("/") + segments = split_path_expr(path_expr.strip("/")) + ns_segments = split_path_expr(env_regex_ns.strip("/")) # Instance ("env") boundary. Assume the standard namespace ``env_regex_ns`` and put the # boundary at its depth when ``path_expr`` sits under it -- literal ns segments must # match, wildcard ns segments (e.g. ``env_.*``) accept any segment. Otherwise fall back @@ -493,13 +514,7 @@ def find_matching_prim_paths(prim_path_regex: str, stage: Usd.Stage | None = Non 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 + return [prim.GetPath().pathString for prim in find_matching_prims(prim_path_regex, stage)] def find_global_fixed_joint_prim( diff --git a/source/isaaclab/isaaclab/sim/views/usd_frame_view.py b/source/isaaclab/isaaclab/sim/views/usd_frame_view.py index 22707ceb8a73..e561713cdeed 100644 --- a/source/isaaclab/isaaclab/sim/views/usd_frame_view.py +++ b/source/isaaclab/isaaclab/sim/views/usd_frame_view.py @@ -71,7 +71,7 @@ def __init__( Args: prim_path: USD prim path pattern to match prims. Supports wildcards (``*``) and - regex patterns (e.g., ``"/World/Env_.*/Robot"``). See + regex patterns (e.g., ``"/World/Env_[^/]*/Robot"``). See :func:`isaaclab.sim.utils.find_matching_prims` for pattern syntax. device: Device to place arrays on. Can be ``"cpu"`` or CUDA devices like ``"cuda:0"``. Defaults to ``"cpu"``. diff --git a/source/isaaclab/test/assets/check_external_force.py b/source/isaaclab/test/assets/check_external_force.py index da251f1ff2b5..25c9782ea50c 100644 --- a/source/isaaclab/test/assets/check_external_force.py +++ b/source/isaaclab/test/assets/check_external_force.py @@ -69,7 +69,7 @@ def main(): robot_cfg.spawn.func("/World/Anymal_c/Robot_1", robot_cfg.spawn, translation=(0.0, -0.5, 0.65)) robot_cfg.spawn.func("/World/Anymal_c/Robot_2", robot_cfg.spawn, translation=(0.0, 0.5, 0.65)) # create handles for the robots - robot = Articulation(robot_cfg.replace(prim_path="/World/Anymal_c/Robot.*")) + robot = Articulation(robot_cfg.replace(prim_path="/World/Anymal_c/Robot[^/]*")) # Play the simulator sim.reset() diff --git a/source/isaaclab/test/assets/check_ridgeback_franka.py b/source/isaaclab/test/assets/check_ridgeback_franka.py index ca5a837824c0..5c46e9476e04 100644 --- a/source/isaaclab/test/assets/check_ridgeback_franka.py +++ b/source/isaaclab/test/assets/check_ridgeback_franka.py @@ -64,7 +64,7 @@ def add_robots() -> Articulation: robot_cfg.spawn.func("/World/Robot_1", robot_cfg.spawn, translation=(0.0, -1.0, 0.0)) robot_cfg.spawn.func("/World/Robot_2", robot_cfg.spawn, translation=(0.0, 1.0, 0.0)) # -- Create interface - robot = Articulation(cfg=robot_cfg.replace(prim_path="/World/Robot.*")) + robot = Articulation(cfg=robot_cfg.replace(prim_path="/World/Robot[^/]*")) return robot diff --git a/source/isaaclab/test/assets/test_articulation_ordering.py b/source/isaaclab/test/assets/test_articulation_ordering.py index d49e4c092740..8b30273f431f 100644 --- a/source/isaaclab/test/assets/test_articulation_ordering.py +++ b/source/isaaclab/test/assets/test_articulation_ordering.py @@ -517,11 +517,11 @@ class _Stage: monkeypatch.setitem(sys.modules, "isaaclab.sim.utils.queries", queries_mod) class _Articulation: - cfg = types.SimpleNamespace(prim_path="/World/envs/env_.*/Robot", articulation_root_prim_path=None) + cfg = types.SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot", articulation_root_prim_path=None) reason = ordering_resolvers._describe_newton_usd_builder_unavailability(_Articulation()) - assert reason == "source asset prim matching '/World/envs/env_.*/Robot' was not found" + assert reason == "source asset prim matching '/World/envs/env_[^/]+/Robot' was not found" def _install_source_asset_resolver(monkeypatch: pytest.MonkeyPatch, resolve_matching_prims_from_source) -> None: @@ -541,7 +541,7 @@ def _install_source_asset_resolver(monkeypatch: pytest.MonkeyPatch, resolve_matc _ROBOT_SCHEMA_PRIM_PATH = "/World/envs/env_0/Robot" -_ROBOT_SCHEMA_SOURCE_EXPR = "/World/envs/env_.*/Robot" +_ROBOT_SCHEMA_SOURCE_EXPR = "/World/envs/env_[^/]+/Robot" def _author_robot_schema_relationship(prim: Usd.Prim, relationship_name: str, target_paths: list[str]) -> None: @@ -785,10 +785,10 @@ def test_mjwarp_ordering_helper_builds_newton_view_from_usd_source(monkeypatch: root_prim = stage.DefinePrim("/World/envs/env_0/Robot/base", "Xform") def _resolve_matching_prims_from_source(path_expr, predicate=None, expected_num_matches=None): - assert path_expr == "/World/envs/env_.*/Robot" + assert path_expr == "/World/envs/env_[^/]+/Robot" if predicate is None: - return [(robot_prim, "/World/envs/env_.*/Robot")] - return [(root_prim, "/World/envs/env_.*/Robot/base")] + return [(robot_prim, "/World/envs/env_[^/]+/Robot")] + return [(root_prim, "/World/envs/env_[^/]+/Robot/base")] _install_newton_usd_builder_mocks( monkeypatch, @@ -801,7 +801,7 @@ def _resolve_matching_prims_from_source(path_expr, predicate=None, expected_num_ class _Articulation: __backend_name__ = "physx" _ordering_convention_name_cache: dict = {} - cfg = types.SimpleNamespace(prim_path="/World/envs/env_.*/Robot", articulation_root_prim_path=None) + cfg = types.SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot", articulation_root_prim_path=None) @property def backend_joint_names(self) -> list[str]: @@ -831,10 +831,10 @@ def test_physx_ordering_helper_builds_bfs_newton_view_from_usd_source(monkeypatc root_prim = stage.DefinePrim("/World/envs/env_0/Robot/base", "Xform") def _resolve_matching_prims_from_source(path_expr, predicate=None, expected_num_matches=None): - assert path_expr == "/World/envs/env_.*/Robot" + assert path_expr == "/World/envs/env_[^/]+/Robot" if predicate is None: - return [(robot_prim, "/World/envs/env_.*/Robot")] - return [(root_prim, "/World/envs/env_.*/Robot/base")] + return [(robot_prim, "/World/envs/env_[^/]+/Robot")] + return [(root_prim, "/World/envs/env_[^/]+/Robot/base")] _install_newton_usd_builder_mocks( monkeypatch, @@ -850,7 +850,7 @@ def _resolve_matching_prims_from_source(path_expr, predicate=None, expected_num_ class _Articulation: __backend_name__ = "newton" _ordering_convention_name_cache: dict = {} - cfg = types.SimpleNamespace(prim_path="/World/envs/env_.*/Robot", articulation_root_prim_path=None) + cfg = types.SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot", articulation_root_prim_path=None) @property def backend_joint_names(self) -> list[str]: diff --git a/source/isaaclab/test/cloner/test_clone_plan_algebra.py b/source/isaaclab/test/cloner/test_clone_plan_algebra.py index 6c0817536939..6865b2e0fa4e 100644 --- a/source/isaaclab/test/cloner/test_clone_plan_algebra.py +++ b/source/isaaclab/test/cloner/test_clone_plan_algebra.py @@ -50,6 +50,16 @@ def test_path_rebase(): cloner.path.rebase("/World/envs/env_0/Robot/base", "/World/envs/env_0", "/World/envs/env_5") == "/World/envs/env_5/Robot/base" ) + + +def test_expand_env_regex_ns_preserves_regex_quantifiers(): + """Macro expansion changes only the named macro, not braces owned by the regex.""" + path_expr = r"{ENV_REGEX_NS}/Robot/link_[0-9]{2}" + + assert cloner.expand_env_regex_ns(path_expr) == r"/World/envs/env_[^/]+/Robot/link_[0-9]{2}" + assert cloner.expand_env_regex_ns(path_expr, "/World/scenes/scene_{}") == ( + r"/World/scenes/scene_[^/]+/Robot/link_[0-9]{2}" + ) # boundary-safe: str.replace would corrupt this, rebase leaves it unchanged assert ( cloner.path.rebase("/World/envs/env_0X/Robot", "/World/envs/env_0", "/World/envs/env_5") @@ -101,7 +111,7 @@ def test_path_match_captures_the_clone_slot(): """match keeps the instance the template's slot captured, which relativize discards.""" tmpl = "/World/envs/env_{}/Robot" assert cloner.path.match("/World/envs/env_3/Robot/base", tmpl) == ("3", "/base") - assert cloner.path.match("/World/envs/env_.*/Robot", tmpl) == (".*", "") + assert cloner.path.match("/World/envs/env_[^/]+/Robot", tmpl) == ("[^/]+", "") assert cloner.path.match("/World/envs/env_3/RobotArm", tmpl) is None @@ -230,13 +240,13 @@ def test_path_to_source_nested_templates_pick_most_specific(): resolved = cloner.query.path_to_source(plan, "/World/envs/env_0/Robot/ee_link/palm_link/Camera") assert resolved == ( "/World/envs/env_0/Robot/ee_link/palm_link/Camera", - "/World/envs/env_*/Robot/ee_link/palm_link/Camera", + "/World/envs/env_[^/]+/Robot/ee_link/palm_link/Camera", "", ) # A path that only the ancestor template owns still resolves against it with its suffix. resolved = cloner.query.path_to_source(plan, "/World/envs/env_0/Robot/base") - assert resolved == ("/World/envs/env_0/Robot", "/World/envs/env_*/Robot", "/base") + assert resolved == ("/World/envs/env_0/Robot", "/World/envs/env_[^/]+/Robot", "/base") def test_path_to_source_ambiguous_templates_raise(): @@ -263,12 +273,12 @@ def test_path_to_source_merges_same_template_rows(): ) # Without an env id, the first populated row represents the asset. - resolved = cloner.query.path_to_source(plan, "/World/envs/env_.*/Object/Body/Camera") - assert resolved == ("/World/envs/env_0/Object", "/World/envs/env_*/Object", "/Body/Camera") + resolved = cloner.query.path_to_source(plan, "/World/envs/env_[^/]+/Object/Body/Camera") + assert resolved == ("/World/envs/env_0/Object", "/World/envs/env_[^/]+/Object", "/Body/Camera") # With an env id, the variant that actually populates that env is reported. - resolved = cloner.query.path_to_source(plan, "/World/envs/env_.*/Object/Body/Camera", env_id=3) - assert resolved == ("/World/envs/env_1/Object", "/World/envs/env_*/Object", "/Body/Camera") + resolved = cloner.query.path_to_source(plan, "/World/envs/env_[^/]+/Object/Body/Camera", env_id=3) + assert resolved == ("/World/envs/env_1/Object", "/World/envs/env_[^/]+/Object", "/Body/Camera") def test_path_to_source_partial_coverage_returns(): @@ -280,11 +290,11 @@ def test_path_to_source_partial_coverage_returns(): [[True, False, True, False], [False, True, False, False]], ) - resolved = cloner.query.path_to_source(plan, "/World/envs/env_.*/Object/Body/Camera") - assert resolved == ("/World/envs/env_0/Object", "/World/envs/env_*/Object", "/Body/Camera") + resolved = cloner.query.path_to_source(plan, "/World/envs/env_[^/]+/Object/Body/Camera") + assert resolved == ("/World/envs/env_0/Object", "/World/envs/env_[^/]+/Object", "/Body/Camera") # No row populates env 3, so resolving for that env reports nothing. - assert cloner.query.path_to_source(plan, "/World/envs/env_.*/Object/Body/Camera", env_id=3) is None + assert cloner.query.path_to_source(plan, "/World/envs/env_[^/]+/Object/Body/Camera", env_id=3) is None def test_path_to_source_inactive_rows_return_none(): @@ -295,7 +305,7 @@ def test_path_to_source_inactive_rows_return_none(): [[False, False, False, False]], ) - assert cloner.query.path_to_source(plan, "/World/envs/env_.*/Object/Body") is None + assert cloner.query.path_to_source(plan, "/World/envs/env_[^/]+/Object/Body") is None def test_iter_sources_yields_nearest_owner(): @@ -306,7 +316,7 @@ def test_iter_sources_yields_nearest_owner(): [[True, True, False, False], [False, False, True, True]], ) - matches = list(cloner.query.iter_sources(plan, "/World/envs/env_.*/Object/Body/Camera")) + matches = list(cloner.query.iter_sources(plan, "/World/envs/env_[^/]+/Object/Body/Camera")) assert matches == [ ( @@ -332,7 +342,7 @@ def test_iter_sources_skips_rows_without_envs(): [[False, False, True, True]], ) - assert list(cloner.query.iter_sources(plan, "/World/envs/env_.*/Object/Body/Camera")) == [ + assert list(cloner.query.iter_sources(plan, "/World/envs/env_[^/]+/Object/Body/Camera")) == [ ( "/World/envs/env_2/Object", "/World/envs/env_{}/Object", @@ -346,7 +356,7 @@ def test_iter_sources_distinct_env_root(): """The destination template need not sit under the default env root.""" plan = PLANS["distinct_env_root"] - assert list(cloner.query.iter_sources(plan, "/World/scenes/.*/Robot/base")) == [ + assert list(cloner.query.iter_sources(plan, "/World/scenes/[^/]+/Robot/base")) == [ ("/World/source/Robot", "/World/scenes/{}/Robot", "/World/source/Robot/base", (0, 1)) ] @@ -361,7 +371,7 @@ def test_iter_sources_ranks_variants_independently_of_env_id_width(): """ plan = _wide_env_id_plan() - matches = list(cloner.query.iter_sources(plan, "/World/envs/env_.*/Object/Body")) + matches = list(cloner.query.iter_sources(plan, "/World/envs/env_[^/]+/Object/Body")) assert [match[0] for match in matches] == ["/World/envs/env_0/Object", "/World/envs/env_10/Object"] assert [match[3] for match in matches] == [tuple(range(10)), (10, 11)] @@ -451,7 +461,7 @@ def test_query_resolve_distinguishes_concrete_paths_from_wildcards(): assert source + suffix == "/World/envs/env_2/Object/base" # Wildcard: one-to-many, so it reports a representative variant... - wildcard = "/World/envs/env_.*/Object/base" + wildcard = "/World/envs/env_[^/]+/Object/base" source, _glob, suffix = cloner.query.path_to_source(plan, wildcard) assert source + suffix == "/World/envs/env_0/Object/base" @@ -478,7 +488,7 @@ def test_query_translates_env_ids_through_the_plan(): assert cloner.query.path_to_clone(plan, path, 5) == "/World/envs/env_5/Robot/base" # Column indices are not environments: env 1 is not targeted by this plan. assert cloner.query.path_to_clone(plan, path, 1) is None - assert next(iter(cloner.query.iter_sources(plan, "/World/envs/env_.*/Robot")))[3] == (2, 5) + assert next(iter(cloner.query.iter_sources(plan, "/World/envs/env_[^/]+/Robot")))[3] == (2, 5) source, _glob, suffix = cloner.query.path_to_source(plan, "/World/envs/env_5/Robot/base") assert source + suffix == path @@ -489,7 +499,7 @@ def test_query_rejects_env_ids_outside_the_plan(env_id): """Out-of-range and negative ids resolve to nothing instead of wrapping the mask.""" plan = _robot_plan() assert cloner.query.path_to_clone(plan, "/World/envs/env_0/Robot/base", env_id) is None - assert cloner.query.path_to_source(plan, "/World/envs/env_.*/Robot", env_id=env_id) is None + assert cloner.query.path_to_source(plan, "/World/envs/env_[^/]+/Robot", env_id=env_id) is None def test_query_agrees_across_duplicate_source_rows(): diff --git a/source/isaaclab/test/cloner/test_replicate_session.py b/source/isaaclab/test/cloner/test_replicate_session.py index 02458b8f6af5..fb8be8e8af1a 100644 --- a/source/isaaclab/test/cloner/test_replicate_session.py +++ b/source/isaaclab/test/cloner/test_replicate_session.py @@ -52,7 +52,7 @@ def replicate(self): monkeypatch.setattr(SimulationContext, "instance", lambda: published) cfg = SimpleNamespace( - prim_path="/World/envs/env_.*/Robot", + prim_path="/World/envs/env_[^/]+/Robot", cloning_contexts=(FakeUsdContext,) if explicit_request else (), spawn=object(), ) diff --git a/source/isaaclab/test/controllers/test_differential_ik.py b/source/isaaclab/test/controllers/test_differential_ik.py index ae02894aabad..67f6b6732ffa 100644 --- a/source/isaaclab/test/controllers/test_differential_ik.py +++ b/source/isaaclab/test/controllers/test_differential_ik.py @@ -83,7 +83,7 @@ def test_franka_ik_pose_abs(sim): sim_context, num_envs, ee_pose_b_des_set = sim # Create robot instance - robot_cfg = FRANKA_PANDA_HIGH_PD_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot_cfg = FRANKA_PANDA_HIGH_PD_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") robot = Articulation(cfg=robot_cfg) # Create IK controller @@ -101,7 +101,7 @@ def test_ur10_ik_pose_abs(sim): sim_context, num_envs, ee_pose_b_des_set = sim # Create robot instance - robot_cfg = UR10_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot_cfg = UR10_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") robot_cfg.spawn.rigid_props.disable_gravity = True robot = Articulation(cfg=robot_cfg) diff --git a/source/isaaclab/test/controllers/test_operational_space.py b/source/isaaclab/test/controllers/test_operational_space.py index 1925c6673a0d..95a965c76aef 100644 --- a/source/isaaclab/test/controllers/test_operational_space.py +++ b/source/isaaclab/test/controllers/test_operational_space.py @@ -94,7 +94,7 @@ def sim(): # clone the env xform cloner.usd_replicate(stage, [env_fmt.format(0)], [env_fmt], env_ids, positions=env_origins) - robot_cfg = FRANKA_PANDA_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot_cfg = FRANKA_PANDA_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") robot_cfg.actuators["panda_shoulder"].stiffness = 0.0 robot_cfg.actuators["panda_shoulder"].damping = 0.0 robot_cfg.actuators["panda_forearm"].stiffness = 0.0 @@ -569,25 +569,25 @@ def test_franka_wrench_abs_open_loop(sim): activate_contact_sensors=True, ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle1", + "/World/envs/env_[^/]+/obstacle1", obstacle_spawn_cfg, translation=(0.2, 0.0, 0.93), orientation=(0.0, -0.1736, 0.0, 0.9848), ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle2", + "/World/envs/env_[^/]+/obstacle2", obstacle_spawn_cfg, translation=(0.2, 0.35, 0.7), orientation=(0.707, 0.0, 0.0, 0.707), ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle3", + "/World/envs/env_[^/]+/obstacle3", obstacle_spawn_cfg, translation=(0.55, 0.0, 0.7), orientation=(0.0, 0.707, 0.0, 0.707), ) contact_forces_cfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/obstacle.*", + prim_path="{ENV_REGEX_NS}/obstacle[^/]*", update_period=0.0, history_length=50, debug_vis=False, @@ -650,25 +650,25 @@ def test_franka_wrench_abs_closed_loop(sim): activate_contact_sensors=True, ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle1", + "/World/envs/env_[^/]+/obstacle1", obstacle_spawn_cfg, translation=(0.2, 0.0, 0.93), orientation=(0.0, -0.1736, 0.0, 0.9848), ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle2", + "/World/envs/env_[^/]+/obstacle2", obstacle_spawn_cfg, translation=(0.2, 0.35, 0.7), orientation=(0.707, 0.0, 0.0, 0.707), ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle3", + "/World/envs/env_[^/]+/obstacle3", obstacle_spawn_cfg, translation=(0.55, 0.0, 0.7), orientation=(0.0, 0.707, 0.0, 0.707), ) contact_forces_cfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/obstacle.*", + prim_path="{ENV_REGEX_NS}/obstacle[^/]*", update_period=0.0, history_length=2, debug_vis=False, @@ -739,13 +739,13 @@ def test_franka_hybrid_decoupled_motion(sim): activate_contact_sensors=True, ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle1", + "/World/envs/env_[^/]+/obstacle1", obstacle_spawn_cfg, translation=(target_hybrid_set_b[0, 0] + 0.05, 0.0, 0.7), orientation=(0.0, 0.707, 0.0, 0.707), ) contact_forces_cfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/obstacle.*", + prim_path="{ENV_REGEX_NS}/obstacle[^/]*", update_period=0.0, history_length=2, debug_vis=False, @@ -816,13 +816,13 @@ def test_franka_hybrid_variable_kp_impedance(sim): activate_contact_sensors=True, ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle1", + "/World/envs/env_[^/]+/obstacle1", obstacle_spawn_cfg, translation=(target_hybrid_set_b[0, 0] + 0.05, 0.0, 0.7), orientation=(0.0, 0.707, 0.0, 0.707), ) contact_forces_cfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/obstacle.*", + prim_path="{ENV_REGEX_NS}/obstacle[^/]*", update_period=0.0, history_length=2, debug_vis=False, @@ -996,13 +996,13 @@ def test_franka_taskframe_hybrid(sim): activate_contact_sensors=True, ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle1", + "/World/envs/env_[^/]+/obstacle1", obstacle_spawn_cfg, translation=(target_hybrid_set_tilted[0, 0] + 0.085, 0.0, 0.3), orientation=(0.0, -0.3826834324, 0.0, 0.9238795325), ) contact_forces_cfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/obstacle.*", + prim_path="{ENV_REGEX_NS}/obstacle[^/]*", update_period=0.0, history_length=2, debug_vis=False, @@ -1228,13 +1228,13 @@ def test_franka_taskframe_hybrid_with_nullspace_centering(sim): activate_contact_sensors=True, ) obstacle_spawn_cfg.func( - "/World/envs/env_.*/obstacle1", + "/World/envs/env_[^/]+/obstacle1", obstacle_spawn_cfg, translation=(target_hybrid_set_tilted[0, 0] + 0.085, 0.0, 0.3), orientation=(0.0, -0.3826834324, 0.0, 0.9238795325), ) contact_forces_cfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/obstacle.*", + prim_path="{ENV_REGEX_NS}/obstacle[^/]*", update_period=0.0, history_length=2, debug_vis=False, diff --git a/source/isaaclab/test/envs/test_scale_randomization.py b/source/isaaclab/test/envs/test_scale_randomization.py index 997b6f985f6a..d88029a8965e 100644 --- a/source/isaaclab/test/envs/test_scale_randomization.py +++ b/source/isaaclab/test/envs/test_scale_randomization.py @@ -152,7 +152,7 @@ class MySceneCfg(InteractiveSceneCfg): # add cube for scale randomization cube1: RigidObjectCfg = RigidObjectCfg( - prim_path="{ENV_REGEX_NS}/cube1", + prim_path="/World/envs/env_[^/]+/cube1", spawn=sim_utils.CuboidCfg( size=(0.2, 0.2, 0.2), rigid_props=sim_utils.RigidBodyPropertiesCfg(max_depenetration_velocity=1.0, disable_gravity=True), @@ -165,7 +165,7 @@ class MySceneCfg(InteractiveSceneCfg): # add cube for static scale values cube2: RigidObjectCfg = RigidObjectCfg( - prim_path="{ENV_REGEX_NS}/cube2", + prim_path="/World/envs/env_[^/]+/cube2", spawn=sim_utils.CuboidCfg( size=(0.2, 0.2, 0.2), rigid_props=sim_utils.RigidBodyPropertiesCfg(max_depenetration_velocity=1.0, disable_gravity=True), @@ -299,12 +299,12 @@ def test_scale_randomization(device): target_position -= env.scene.env_origins # test to make sure all assets in the scene are created - all_prim_paths = sim_utils.find_matching_prim_paths("/World/envs/env_.*/cube.*/.*") + all_prim_paths = sim_utils.find_matching_prim_paths("/World/envs/env_[^/]+/cube[^/]*/[^/]*") assert len(all_prim_paths) == (env.num_envs * 2) # test to make sure randomized values are truly random applied_scaling_randomization = set() - prim_paths = sim_utils.find_matching_prim_paths("/World/envs/env_.*/cube1") + prim_paths = sim_utils.find_matching_prim_paths("/World/envs/env_[^/]+/cube1") # get the stage stage = sim_utils.get_current_stage() @@ -320,7 +320,7 @@ def test_scale_randomization(device): applied_scaling_randomization.add(scale_spec.default) # test to make sure that fixed values are assigned correctly - prim_paths = sim_utils.find_matching_prim_paths("/World/envs/env_.*/cube2") + prim_paths = sim_utils.find_matching_prim_paths("/World/envs/env_[^/]+/cube2") for i in range(3): prim_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_paths[i]) scale_spec = prim_spec.GetAttributeAtPath(prim_paths[i] + ".xformOp:scale") diff --git a/source/isaaclab/test/performance/test_robot_load_performance.py b/source/isaaclab/test/performance/test_robot_load_performance.py index ee2ac17158c8..aed63a07fa50 100644 --- a/source/isaaclab/test/performance/test_robot_load_performance.py +++ b/source/isaaclab/test/performance/test_robot_load_performance.py @@ -73,7 +73,7 @@ def test_robot_load_performance(test_config, device): ) with Timer(f"{test_config['name']} load time for device {device}") as timer: - robot = Articulation(test_config["robot_cfg"].replace(prim_path="/World/Robots_.*/Robot")) # noqa: F841 + robot = Articulation(test_config["robot_cfg"].replace(prim_path="/World/Robots_[^/]*/Robot")) # noqa: F841 sim.reset() elapsed_time = timer.time_elapsed assert elapsed_time <= test_config["expected_load_time"] diff --git a/source/isaaclab/test/scene/test_interactive_scene.py b/source/isaaclab/test/scene/test_interactive_scene.py index 798ad822f63d..c02a3edd97fe 100644 --- a/source/isaaclab/test/scene/test_interactive_scene.py +++ b/source/isaaclab/test/scene/test_interactive_scene.py @@ -36,7 +36,7 @@ class MySceneCfg(InteractiveSceneCfg): # articulation robot = ArticulationCfg( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", spawn=sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/IsaacSim/SimpleArticulation/revolute_articulation.usd", ), @@ -46,7 +46,7 @@ class MySceneCfg(InteractiveSceneCfg): ) # rigid object rigid_obj = RigidObjectCfg( - prim_path="/World/envs/env_.*/RigidObj", + prim_path="{ENV_REGEX_NS}/RigidObj", spawn=sim_utils.CuboidCfg( size=(0.5, 0.5, 0.5), rigid_props=sim_utils.RigidBodyPropertiesCfg( @@ -277,11 +277,11 @@ def test_cfg_cloning_contexts_override_backend_default(monkeypatch: pytest.Monke InteractiveScene(scene_cfg) queued_by_path = {cfg.prim_path: cfg for cfg in REPLICATION_QUEUE} # the override rides the queued cfg; resolution happens at replicate() - assert queued_by_path["/World/envs/env_.*/RigidObj"].cloning_contexts == ( + assert queued_by_path["/World/envs/env_[^/]+/RigidObj"].cloning_contexts == ( "isaaclab.cloner:UsdReplicateContext", ) # untouched asset resolves to the backend default stack at replicate() - assert queued_by_path["/World/envs/env_.*/Robot"].cloning_contexts is None + assert queued_by_path["/World/envs/env_[^/]+/Robot"].cloning_contexts is None finally: REPLICATION_QUEUE.clear() @@ -304,13 +304,12 @@ def test_collect_asset_cfgs_resolves_env_regex_macros(): objects=RigidObjectCollectionCfg(rigid_objects={"cube": cube_cfg, "shape": shape_cfg}), ) scene.cloner_cfg = CloneCfg() - scene._env_regex_ns = scene.cloner_cfg.clone_regex - scene._env_ns = scene._env_regex_ns.rsplit("/", 1)[0] + scene._env_fmt = scene.cloner_cfg.clone_template cfgs = scene._collect_asset_cfgs() prim_paths = sorted(c.prim_path for c in cfgs) - assert prim_paths == ["/World/envs/env_.*/Cube", "/World/envs/env_.*/Shape"] + assert prim_paths == ["/World/envs/env_[^/]+/Cube", "/World/envs/env_[^/]+/Shape"] def test_collect_asset_cfgs_orders_sensors_last(): @@ -322,7 +321,7 @@ def test_collect_asset_cfgs_orders_sensors_last(): body = SimpleNamespace(prim_path="{ENV_REGEX_NS}/Robot") scene.cfg = SimpleNamespace(num_envs=1, sensor=sensor, body=body) scene.cloner_cfg = CloneCfg() - scene._env_ns = scene.cloner_cfg.clone_regex.rsplit("/", 1)[0] + scene._env_fmt = scene.cloner_cfg.clone_template cfgs = scene._collect_asset_cfgs() diff --git a/source/isaaclab/test/sensors/check_multi_mesh_ray_caster.py b/source/isaaclab/test/sensors/check_multi_mesh_ray_caster.py index 3f6f0667cffa..68da4d7d0702 100644 --- a/source/isaaclab/test/sensors/check_multi_mesh_ray_caster.py +++ b/source/isaaclab/test/sensors/check_multi_mesh_ray_caster.py @@ -156,11 +156,11 @@ def main(): ] if args_cli.num_objects != 0: mesh_targets.append( - MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="/World/envs/env_.*/object_.*", track_mesh_transforms=True) + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/object_[^/]*", track_mesh_transforms=True) ) # Create a ray-caster sensor ray_caster_cfg = MultiMeshRayCasterCfg( - prim_path="/World/envs/env_.*/ball", + prim_path="{ENV_REGEX_NS}/ball", mesh_prim_paths=mesh_targets, pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=(1.6, 1.0)), ray_alignment="yaw", @@ -169,7 +169,7 @@ def main(): ray_caster = MultiMeshRayCaster(cfg=ray_caster_cfg) # Create a view over all the balls balls_cfg = RigidObjectCfg( - prim_path="/World/envs/env_.*/ball", + prim_path="{ENV_REGEX_NS}/ball", spawn=None, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 5.0)), ) diff --git a/source/isaaclab/test/sensors/check_ray_caster.py b/source/isaaclab/test/sensors/check_ray_caster.py index 01d37ba3e932..bfde5c1d0dcb 100644 --- a/source/isaaclab/test/sensors/check_ray_caster.py +++ b/source/isaaclab/test/sensors/check_ray_caster.py @@ -120,7 +120,7 @@ def main(): # Create a ray-caster sensor ray_caster_cfg = RayCasterCfg( - prim_path="/World/envs/env_.*/ball", + prim_path="{ENV_REGEX_NS}/ball", mesh_prim_paths=["/World/ground"], pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=(1.6, 1.0)), ray_alignment="yaw", @@ -129,7 +129,7 @@ def main(): ray_caster = RayCaster(cfg=ray_caster_cfg) # Create a view over all the balls balls_cfg = RigidObjectCfg( - prim_path="/World/envs/env_.*/ball", + prim_path="{ENV_REGEX_NS}/ball", spawn=None, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 5.0)), ) diff --git a/source/isaaclab/test/sensors/generate_synthetic_gaussian_asset.py b/source/isaaclab/test/sensors/generate_synthetic_gaussian_asset.py index 9c92a9cae7fd..69a4baa0efb5 100644 --- a/source/isaaclab/test/sensors/generate_synthetic_gaussian_asset.py +++ b/source/isaaclab/test/sensors/generate_synthetic_gaussian_asset.py @@ -597,7 +597,7 @@ def assert_tiled_views_match( """Camera prim name authored inside the synthesised asset USD.""" SYNTHETIC_GAUSSIAN_CAMERA_REGEX = ( - f"/World/envs/env_.*/{SYNTHETIC_GAUSSIAN_SCENE_REL_PATH}/Cameras/{SYNTHETIC_GAUSSIAN_CAMERA_NAME}" + f"/World/envs/env_[^/]+/{SYNTHETIC_GAUSSIAN_SCENE_REL_PATH}/Cameras/{SYNTHETIC_GAUSSIAN_CAMERA_NAME}" ) """Regex camera prim path that resolves to one camera per env (single or tiled).""" diff --git a/source/isaaclab/test/sensors/test_camera.py b/source/isaaclab/test/sensors/test_camera.py index 8e3584d54356..cfdb449b3e93 100644 --- a/source/isaaclab/test/sensors/test_camera.py +++ b/source/isaaclab/test/sensors/test_camera.py @@ -852,7 +852,7 @@ def test_camera_multi_regex_init(setup_camera_device, device): sim_utils.create_prim(f"/World/Origin_{i}", "Xform") camera_cfg = copy.deepcopy(camera_cfg) - camera_cfg.prim_path = "/World/Origin_.*/CameraSensor" + camera_cfg.prim_path = "/World/Origin_[^/]*/CameraSensor" camera = Camera(camera_cfg) sim.reset() @@ -907,7 +907,7 @@ def test_camera_all_annotators(setup_camera_device, device): camera_cfg = copy.deepcopy(camera_cfg) camera_cfg.data_types = all_annotator_types - camera_cfg.prim_path = "/World/Origin_.*/CameraSensor" + camera_cfg.prim_path = "/World/Origin_[^/]*/CameraSensor" camera = Camera(camera_cfg) sim.reset() @@ -970,7 +970,7 @@ def test_camera_segmentation_non_colorize(setup_camera_device, device): camera_cfg = copy.deepcopy(camera_cfg) camera_cfg.data_types = ["semantic_segmentation", "instance_segmentation", "instance_id_segmentation_fast"] - camera_cfg.prim_path = "/World/Origin_.*/CameraSensor" + camera_cfg.prim_path = "/World/Origin_[^/]*/CameraSensor" camera_cfg.renderer_cfg.colorize_semantic_segmentation = False camera_cfg.renderer_cfg.colorize_instance_segmentation = False camera_cfg.renderer_cfg.colorize_instance_id_segmentation = False @@ -1000,7 +1000,7 @@ def test_camera_normals_unit_length(setup_camera_device, device): camera_cfg = copy.deepcopy(camera_cfg) camera_cfg.data_types = ["normals"] - camera_cfg.prim_path = "/World/Origin_.*/CameraSensor" + camera_cfg.prim_path = "/World/Origin_[^/]*/CameraSensor" camera = Camera(camera_cfg) sim.reset() diff --git a/source/isaaclab/test/sensors/test_camera_opencv_distortion_ovrtx.py b/source/isaaclab/test/sensors/test_camera_opencv_distortion_ovrtx.py index 0108b8cb429c..9e611c4e72f7 100644 --- a/source/isaaclab/test/sensors/test_camera_opencv_distortion_ovrtx.py +++ b/source/isaaclab/test/sensors/test_camera_opencv_distortion_ovrtx.py @@ -132,7 +132,7 @@ def _render_grid(distortion: OpenCvDistortionCfg, device: str) -> tuple[np.ndarr ) camera = Camera( CameraCfg( - prim_path="/World/envs/env_.*/Camera", + prim_path="{ENV_REGEX_NS}/Camera", update_period=0.0, height=HEIGHT, width=WIDTH, diff --git a/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py b/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py index be12fdef5e14..5b7ee1906f1e 100644 --- a/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py +++ b/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py @@ -549,16 +549,16 @@ def test_depth_output_equal_to_usd_camera_heterogeneous_scene(setup_simulation): mesh_prim_paths = [ "/World/defaultGroundPlane", MultiMeshRayCasterCameraCfg.RaycastTargetCfg( - prim_expr="/World/envs/env_.*/Object", + prim_expr="{ENV_REGEX_NS}/Object", track_mesh_transforms=False, ), MultiMeshRayCasterCameraCfg.RaycastTargetCfg( - prim_expr="/World/envs/env_.*/Robot/.+", + prim_expr="{ENV_REGEX_NS}/Robot/[^/]+", track_mesh_transforms=True, ), ] camera_cfg_warp = MultiMeshRayCasterCameraCfg( - prim_path="/World/envs/env_.*/RayCasterCamera", + prim_path="{ENV_REGEX_NS}/RayCasterCamera", mesh_prim_paths=mesh_prim_paths, update_period=0, debug_vis=False, @@ -573,7 +573,7 @@ def test_depth_output_equal_to_usd_camera_heterogeneous_scene(setup_simulation): camera_cfg_usd = CameraCfg( height=height, width=width, - prim_path="/World/envs/env_.*/UsdCamera", + prim_path="{ENV_REGEX_NS}/UsdCamera", update_period=0, data_types=["distance_to_image_plane"], spawn=PinholeCameraCfg( diff --git a/source/isaaclab/test/sensors/test_multi_tiled_camera.py b/source/isaaclab/test/sensors/test_multi_tiled_camera.py index 833c51034717..087a20f3abc8 100644 --- a/source/isaaclab/test/sensors/test_multi_tiled_camera.py +++ b/source/isaaclab/test/sensors/test_multi_tiled_camera.py @@ -83,7 +83,7 @@ def test_multi_tiled_camera_init(setup_camera): # Create camera camera_cfg = copy.deepcopy(camera_cfg) - camera_cfg.prim_path = f"/World/Origin_{i}.*/CameraSensor" + camera_cfg.prim_path = f"/World/Origin_{i}[^/]*/CameraSensor" camera = TiledCamera(camera_cfg) tiled_cameras.append(camera) @@ -174,7 +174,7 @@ def test_all_annotators_multi_tiled_camera(setup_camera): # Create camera camera_cfg = copy.deepcopy(camera_cfg) camera_cfg.data_types = all_annotator_types - camera_cfg.prim_path = f"/World/Origin_{i}.*/CameraSensor" + camera_cfg.prim_path = f"/World/Origin_{i}[^/]*/CameraSensor" camera = TiledCamera(camera_cfg) tiled_cameras.append(camera) @@ -270,7 +270,7 @@ def test_different_resolution_multi_tiled_camera(setup_camera): # Create camera camera_cfg = copy.deepcopy(camera_cfg) - camera_cfg.prim_path = f"/World/Origin_{i}.*/CameraSensor" + camera_cfg.prim_path = f"/World/Origin_{i}[^/]*/CameraSensor" camera_cfg.height, camera_cfg.width = resolutions[i] camera = TiledCamera(camera_cfg) tiled_cameras.append(camera) @@ -336,7 +336,7 @@ def test_frame_offset_multi_tiled_camera(setup_camera): # Create camera camera_cfg = copy.deepcopy(camera_cfg) - camera_cfg.prim_path = f"/World/Origin_{i}.*/CameraSensor" + camera_cfg.prim_path = f"/World/Origin_{i}[^/]*/CameraSensor" camera = TiledCamera(camera_cfg) tiled_cameras.append(camera) @@ -404,7 +404,7 @@ def test_frame_different_poses_multi_tiled_camera(setup_camera): # Create camera camera_cfg = copy.deepcopy(camera_cfg) - camera_cfg.prim_path = f"/World/Origin_{i}.*/CameraSensor" + camera_cfg.prim_path = f"/World/Origin_{i}[^/]*/CameraSensor" camera_cfg.offset = TiledCameraCfg.OffsetCfg(pos=positions[i], rot=rotations[i], convention="ros") camera = TiledCamera(camera_cfg) tiled_cameras.append(camera) diff --git a/source/isaaclab/test/sensors/test_ray_caster_integration.py b/source/isaaclab/test/sensors/test_ray_caster_integration.py index 6f38b95ccfd3..a3fa80b906da 100644 --- a/source/isaaclab/test/sensors/test_ray_caster_integration.py +++ b/source/isaaclab/test/sensors/test_ray_caster_integration.py @@ -368,10 +368,10 @@ def _create_object_body(path: str) -> None: sim_utils.update_stage() cfg = MultiMeshRayCasterCfg( - prim_path="/World/envs/env_.*/Sensor", + prim_path="{ENV_REGEX_NS}/Sensor", mesh_prim_paths=[ MultiMeshRayCasterCfg.RaycastTargetCfg( - prim_expr="/World/envs/env_.*/Object/part_.*", + prim_expr="{ENV_REGEX_NS}/Object/part_[^/]*", track_mesh_transforms=True, ), ], diff --git a/source/isaaclab/test/sensors/test_sensor_base.py b/source/isaaclab/test/sensors/test_sensor_base.py index 072d17c37d47..84e94bb08213 100644 --- a/source/isaaclab/test/sensors/test_sensor_base.py +++ b/source/isaaclab/test/sensors/test_sensor_base.py @@ -83,7 +83,7 @@ def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | None class DummySensorCfg(SensorBaseCfg): class_type = DummySensor - prim_path = "/World/envs/env_.*/Cube/dummy_sensor" + prim_path = "{ENV_REGEX_NS}/Cube/dummy_sensor" def _populate_scene(): @@ -343,11 +343,11 @@ def test_rigid_body_ancestor_expr_trims_only_terminal_suffix(create_dummy_sensor UsdPhysics.RigidBodyAPI.Apply(sim_utils.get_current_stage().GetPrimAtPath(parent_path)) sim_utils.update_stage() - sensor_cfg.prim_path = "/World/envs/env_.*/Robot/link/link" + sensor_cfg.prim_path = "{ENV_REGEX_NS}/Robot/link/link" sensor = DummySensor(cfg=sensor_cfg) rigid_parent_expr, fixed_pos_b, fixed_quat_b = sensor._resolve_rigid_body_ancestor_expr() - assert rigid_parent_expr == "/World/envs/env_.*/Robot/link" + assert rigid_parent_expr == "/World/envs/env_[^/]+/Robot/link" assert fixed_pos_b is not None assert fixed_quat_b is not None diff --git a/source/isaaclab/test/sim/check_meshes.py b/source/isaaclab/test/sim/check_meshes.py index ca12466141c7..1c7b4691fcd7 100644 --- a/source/isaaclab/test/sim/check_meshes.py +++ b/source/isaaclab/test/sim/check_meshes.py @@ -136,7 +136,7 @@ def design_scene(): # randomize the color obj_cfg.visual_material.diffuse_color = (random.random(), random.random(), random.random()) # spawn the object - obj_cfg.func(f"/World/Origin.*/Object{idx:02d}", obj_cfg, translation=origin) + obj_cfg.func(f"/World/Origin[^/]+/Object{idx:02d}", obj_cfg, translation=origin) def main(): diff --git a/source/isaaclab/test/sim/test_cloner.py b/source/isaaclab/test/sim/test_cloner.py index 7bf436e70234..bacdf151189d 100644 --- a/source/isaaclab/test/sim/test_cloner.py +++ b/source/isaaclab/test/sim/test_cloner.py @@ -36,6 +36,7 @@ usd_replicate, ) from isaaclab.sim import build_simulation_context +from isaaclab.sim.utils import queries pytestmark = [pytest.mark.integration, pytest.mark.isaacsim_ci] @@ -251,10 +252,10 @@ def capturing_copy_spec(src_layer, src_path, dst_layer, dst_path): [ ( ["/World/rig_0_alpha", "/World/rig_0_beta", "/World/rig_0_gamma"], - "/World/rig_0_.*/Sensor", + "/World/rig_0_[^/]*/Sensor", ["/World/rig_0_alpha/Sensor", "/World/rig_0_beta/Sensor", "/World/rig_0_gamma/Sensor"], "/World/rig_00/Sensor", - "/World/rig_0_.*", + "/World/rig_0_[^/]*", ), ( [ @@ -263,7 +264,7 @@ def capturing_copy_spec(src_layer, src_path, dst_layer, dst_path): "/World/group_b/slot_0", "/World/group_b/slot_1", ], - "/World/group_.*/slot_.*/Sensor", + "/World/group_[^/]*/slot_[^/]*/Sensor", [ "/World/group_a/slot_0/Sensor", "/World/group_a/slot_1/Sensor", @@ -271,7 +272,7 @@ def capturing_copy_spec(src_layer, src_path, dst_layer, dst_path): "/World/group_b/slot_1/Sensor", ], "/World/group_0/slot_0/Sensor", - "/World/group_.*/slot_.*", + "/World/group_[^/]*/slot_[^/]*", ), ( ["/World/template/Object"], @@ -313,8 +314,8 @@ def test_clone_decorator_wildcard_patterns( def test_queue_replication_only_appends(sim): """queue_replication must only append the cfg-directed contexts — no other side effects.""" - cfg_a = SimpleNamespace(prim_path="/World/envs/env_.*/Robot") - cfg_b = SimpleNamespace(prim_path="/World/envs/env_.*/Object") + cfg_a = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot") + cfg_b = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Object") queue_replication(cfg_a) queue_replication(cfg_b) @@ -325,7 +326,7 @@ def test_queue_replication_only_appends(sim): def test_make_clone_plan_homogeneous_returns_env_root_plan(sim): """Homogeneous (single-variant) cfgs produce one source row at the env root.""" cube = SimpleNamespace( - prim_path="/World/envs/env_.*/Robot", + prim_path="/World/envs/env_[^/]+/Robot", spawn=sim_utils.CuboidCfg(size=(0.1, 0.1, 0.1)), ) @@ -346,10 +347,55 @@ def test_make_clone_plan_homogeneous_returns_env_root_plan(sim): assert cube.spawn.spawn_path == "/World/envs/env_0/Robot" +def test_resolve_matching_prims_from_source_searches_only_plan_source(sim, monkeypatch): + """Clone-aware regex discovery traverses its plan source, never cloned destinations.""" + stage = sim_utils.get_current_stage() + for path in ( + "/World/envs/env_0/Robot/foo", + "/World/envs/env_0/Robot/foo/bar", + "/World/envs/env_1/Robot/clone_only", + ): + stage.DefinePrim(path, "Xform") + plan = ClonePlan( + sources=("/World/envs/env_0/Robot",), + destinations=("/World/envs/env_{}/Robot",), + clone_mask=torch.ones((1, 2), dtype=torch.bool, device=sim.cfg.device), + env_ids=torch.arange(2, dtype=torch.long, device=sim.cfg.device), + positions=torch.zeros((2, 3), device=sim.cfg.device), + ) + sim.set_clone_plan(plan) + + traversed_roots = [] + source_matcher = queries._iter_matching_prims_in_subtree + + def record_source_root(path_expr, root_prim): + traversed_roots.append(root_prim.GetPath().pathString) + return source_matcher(path_expr, root_prim) + + monkeypatch.setattr(queries, "_iter_matching_prims_in_subtree", record_source_root) + monkeypatch.setattr( + queries, + "find_matching_prims", + lambda *args, **kwargs: pytest.fail("clone-aware resolution called the unscoped stage matcher"), + ) + + matches = queries.resolve_matching_prims_from_source(r"/World/envs/env_[^/]+/Robot/[^A]+") + + assert traversed_roots == ["/World/envs/env_0/Robot"] + assert [prim.GetPath().pathString for prim, _ in matches] == [ + "/World/envs/env_0/Robot/foo", + "/World/envs/env_0/Robot/foo/bar", + ] + assert [path_expr for _, path_expr in matches] == [ + "/World/envs/env_[^/]+/Robot/foo", + "/World/envs/env_[^/]+/Robot/foo/bar", + ] + + def test_make_clone_plan_heterogeneous_mutates_spawn_paths(sim): """Multi-variant spawners get per-variant spawn_paths and contribute multiple plan rows.""" multi_cfg = SimpleNamespace( - prim_path="/World/envs/env_.*/Object", + prim_path="/World/envs/env_[^/]+/Object", spawn=sim_utils.MultiAssetSpawnerCfg( assets_cfg=[ sim_utils.ConeCfg(radius=0.1, height=0.2), @@ -358,7 +404,7 @@ def test_make_clone_plan_heterogeneous_mutates_spawn_paths(sim): ), ) plain_cfg = SimpleNamespace( - prim_path="/World/envs/env_.*/Robot", + prim_path="/World/envs/env_[^/]+/Robot", spawn=sim_utils.CuboidCfg(size=(0.1, 0.1, 0.1)), ) @@ -403,8 +449,8 @@ def test_make_clone_plan_skips_global_cfgs(sim): def test_clone_plan_from_env_0_populates_cfg_rows(sim): """clone_plan_from_env_0 auto-maps queued env-scoped cfgs to row 0 and excludes global ones.""" - env_cfg_a = SimpleNamespace(prim_path="/World/envs/env_.*/Robot") - env_cfg_b = SimpleNamespace(prim_path="/World/envs/env_.*/Object") + env_cfg_a = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot") + env_cfg_b = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Object") global_cfg = SimpleNamespace(prim_path="/World/global/Light") for cfg in (env_cfg_a, env_cfg_b, global_cfg): @@ -444,7 +490,9 @@ def replicate(self): stage = sim_utils.get_current_stage() stage.DefinePrim("/World/envs/env_0/Robot", "Xform") - cfg = SimpleNamespace(prim_path="/World/envs/env_.*/Robot", cloning_contexts=(UsdReplicateContext, FakePhysicsCtx)) + cfg = SimpleNamespace( + prim_path="/World/envs/env_[^/]+/Robot", cloning_contexts=(UsdReplicateContext, FakePhysicsCtx) + ) REPLICATION_QUEUE.append(cfg) plan = ClonePlan( @@ -480,8 +528,8 @@ def queue_mapping(self, sources, destinations, env_ids, mask, *, positions=None) def replicate(self): self.replicate_calls += 1 - cfg_a = SimpleNamespace(prim_path="/World/envs/env_.*/Robot") - cfg_b = SimpleNamespace(prim_path="/World/envs/env_.*/Object") + cfg_a = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot") + cfg_b = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Object") cfg_a.cloning_contexts = (FakeCtx,) cfg_b.cloning_contexts = (FakeCtx,) REPLICATION_QUEUE.append(cfg_a) @@ -535,7 +583,7 @@ def queue_mapping(self, sources, destinations, env_ids, mask, *, positions=None) def replicate(self): self.replicate_calls += 1 - cfgs = [SimpleNamespace(prim_path=f"/World/envs/env_.*/asset_{i}") for i in range(5)] + cfgs = [SimpleNamespace(prim_path=f"/World/envs/env_[^/]+/asset_{i}") for i in range(5)] for cfg in cfgs: cfg.cloning_contexts = (FakeCtx,) REPLICATION_QUEUE.append(cfg) @@ -589,7 +637,7 @@ def queue_mapping(self, *args, **kwargs): def replicate(self): call_order.append("high") - cfg = SimpleNamespace(prim_path="/World/envs/env_.*/Robot") + cfg = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot") cfg.cloning_contexts = (HighPriority, LowPriority) REPLICATION_QUEUE.append(cfg) @@ -645,7 +693,7 @@ def queue_mapping(self, *args, **kwargs): def replicate(self): raise RuntimeError("backend boom") - cfg = SimpleNamespace(prim_path="/World/envs/env_.*/Robot") + cfg = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot") cfg.cloning_contexts = (ExplodingCtx,) REPLICATION_QUEUE.append(cfg) @@ -668,7 +716,7 @@ def test_replicate_session_clears_queue_when_asset_init_fails(sim): """ReplicateSession.__exit__ drops queued cfgs if the asset constructor body raises.""" from isaaclab.cloner import ReplicateSession - leaked_cfg = SimpleNamespace(prim_path="/World/envs/env_.*/Robot") + leaked_cfg = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot") sentinel = MagicMock() sentinel_cls = MagicMock(return_value=sentinel) diff --git a/source/isaaclab/test/sim/test_simulation_stage_in_memory.py b/source/isaaclab/test/sim/test_simulation_stage_in_memory.py index f91947fc32b9..081496376092 100644 --- a/source/isaaclab/test/sim/test_simulation_stage_in_memory.py +++ b/source/isaaclab/test/sim/test_simulation_stage_in_memory.py @@ -105,7 +105,7 @@ def test_stage_in_memory_with_shapes(sim): mass_props=sim_utils.MassPropertiesCfg(mass=1.0), collision_props=sim_utils.CollisionPropertiesCfg(), ) - prim_path_regex = "/World/Cone/asset_.*" + prim_path_regex = "/World/Cone/asset_[^/]*" cfg.func(prim_path_regex, cfg) # verify prims exist in stage @@ -160,7 +160,7 @@ def test_stage_in_memory_with_usds(sim): ), activate_contact_sensors=True, ) - prim_path_regex = "/World/Robot/asset_.*" + prim_path_regex = "/World/Robot/asset_[^/]*" cfg.func(prim_path_regex, cfg) # verify prims exist in stage diff --git a/source/isaaclab/test/sim/test_spawn_shapes.py b/source/isaaclab/test/sim/test_spawn_shapes.py index be59ea011d01..fb501e15a771 100644 --- a/source/isaaclab/test/sim/test_spawn_shapes.py +++ b/source/isaaclab/test/sim/test_spawn_shapes.py @@ -288,5 +288,7 @@ def test_spawn_cone_clone_with_all_props_global_material(sim): assert prim.IsValid() assert str(prim.GetPath()) == "/World/env_0/Cone" # find matching material prims - prims = sim_utils.find_matching_prim_paths("/Looks/visualMaterial.*") + # the global material is one shared prim at exactly this path -- a trailing ``.*`` would also + # select its Shader child, since ``.*`` spans separators like it does in any regex. + prims = sim_utils.find_matching_prim_paths("/Looks/visualMaterial") assert len(prims) == 1 diff --git a/source/isaaclab/test/sim/test_spawn_wrappers.py b/source/isaaclab/test/sim/test_spawn_wrappers.py index a0be9336a56f..9523633f2a11 100644 --- a/source/isaaclab/test/sim/test_spawn_wrappers.py +++ b/source/isaaclab/test/sim/test_spawn_wrappers.py @@ -70,7 +70,7 @@ def test_spawn_multiple_shapes_with_regex_prefix(sim): prim = cfg.func("/World/env_.*/Cone/asset_.*", cfg) assert str(prim.GetPath()) == "/World/env_0/Cone/asset_0" - prim_paths = sim_utils.find_matching_prim_paths("/World/env_.*/Cone/asset_.*") + prim_paths = sim_utils.find_matching_prim_paths("/World/env_[^/]+/Cone/asset_[^/]*") assert len(prim_paths) == num_assets * num_envs for env_idx in range(num_envs): @@ -111,7 +111,7 @@ def test_spawn_multiple_shapes_with_global_settings(sim): assert prim.IsValid() assert str(prim.GetPath()) == "/World/template/Cone/asset_0" - prim_paths = sim_utils.find_matching_prim_paths("/World/template/Cone/asset_.*") + prim_paths = sim_utils.find_matching_prim_paths("/World/template/Cone/asset_[^/]*") assert len(prim_paths) == 3 for prim_path in prim_paths: @@ -154,7 +154,7 @@ def test_spawn_multiple_shapes_with_individual_settings(sim): assert prim.IsValid() assert str(prim.GetPath()) == "/World/template/Cone/asset_0" - prim_paths = sim_utils.find_matching_prim_paths("/World/template/Cone/asset_.*") + prim_paths = sim_utils.find_matching_prim_paths("/World/template/Cone/asset_[^/]*") assert len(prim_paths) == 3 for prim_path in prim_paths: @@ -229,5 +229,5 @@ def test_spawn_multiple_files_with_global_settings(sim): assert prim.IsValid() assert str(prim.GetPath()) == "/World/template/Robot/asset_0" - prim_paths = sim_utils.find_matching_prim_paths("/World/template/Robot/asset_.*") + prim_paths = sim_utils.find_matching_prim_paths("/World/template/Robot/asset_[^/]*") assert len(prim_paths) == 2 diff --git a/source/isaaclab/test/sim/test_utils_queries.py b/source/isaaclab/test/sim/test_utils_queries.py index 973e7e718565..046007bc54bc 100644 --- a/source/isaaclab/test/sim/test_utils_queries.py +++ b/source/isaaclab/test/sim/test_utils_queries.py @@ -13,11 +13,16 @@ """Rest everything follows.""" +import ast +import inspect +import textwrap + import pytest from pxr import UsdPhysics import isaaclab.sim as sim_utils +from isaaclab.sim.utils import queries from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR pytestmark = pytest.mark.integration @@ -90,11 +95,76 @@ def test_get_first_matching_ancestor_prim(): def test_matches_path_expr_prefix(): - path_expr = "/World/envs/env_.*/Robot" + path_expr = "/World/envs/env_[^/]+/Robot" assert sim_utils.matches_path_expr_prefix(path_expr, "/World/envs/env_0") assert sim_utils.matches_path_expr_prefix(path_expr, "/World/envs/env_0/Robot") assert not sim_utils.matches_path_expr_prefix(path_expr, "/World/envs/env_0/Object") assert not sim_utils.matches_path_expr_prefix(path_expr, "/World/envs/env_0/Robot/base") + assert not sim_utils.matches_path_expr_prefix( + "/World/envs/env_[^/]+/Robot/cart|pole", "/World/envs/env_0/Robot/cartXX" + ) + + +def test_path_expression_helpers_preserve_supported_regex_text(): + """Path adapters touch only the syntax they explicitly support.""" + path_expr = r"/World/envs/env_[^/]+/Robot/link_[0-9]{2}" + + assert sim_utils.split_path_expr(path_expr) == ["", "World", "envs", "env_[^/]+", "Robot", "link_[0-9]{2}"] + assert sim_utils.path_expr_to_glob(path_expr) == r"/World/envs/env_*/Robot/link_[0-9]{2}" + assert sim_utils.path_expr_to_glob(r"/World/Robot/[^/]{2}") == r"/World/Robot/[^/]{2}" + + +def test_find_matching_prims_uses_unbounded_full_path_regex(): + """Regex tokens retain their Python semantics across prim path separators.""" + sim_utils.create_prim("/World/Robot/foo") + sim_utils.create_prim("/World/Robot/foo/bar") + sim_utils.create_prim("/World/Robot/Arm") + sim_utils.create_prim("/World/A/foo") + sim_utils.create_prim("/World/B/foo") + + matches = sim_utils.find_matching_prims(r"/World/Robot/[^A]+") + + assert [prim.GetPath().pathString for prim in matches] == ["/World/Robot/foo", "/World/Robot/foo/bar"] + matches = sim_utils.find_matching_prims(r"/World/[^/]+/foo") + assert [prim.GetPath().pathString for prim in matches] == ["/World/Robot/foo", "/World/A/foo", "/World/B/foo"] + + +def test_find_matching_prims_has_no_inferred_traversal_bounds(): + """The query must not narrow or prune USD traversal from the user's regex.""" + sources = (sim_utils.find_matching_prims, queries._iter_matching_prims_in_subtree) + tree = ast.parse("\n".join(textwrap.dedent(inspect.getsource(function)) for function in sources)) + called_methods = { + node.func.attr for node in ast.walk(tree) if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + } + called_functions = { + node.func.id for node in ast.walk(tree) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + } + + assert "GetPrimAtPath" not in called_methods + assert "PruneChildren" not in called_methods + assert "_bound_search" not in called_functions + + +def test_find_matching_prims_fullmatches_top_level_alternation(): + """Anchoring applies to the complete expression rather than individual alternatives.""" + sim_utils.create_prim("/World/Robot/foo/bar") + sim_utils.create_prim("/World/Robot/foo/bar/baz") + sim_utils.create_prim("/World/Floor") + + matches = sim_utils.find_matching_prims(r"/World/Robot/foo/bar|/World/Floor") + + assert [prim.GetPath().pathString for prim in matches] == ["/World/Robot/foo/bar", "/World/Floor"] + + +def test_find_matching_prims_includes_inactive_and_undefined_prims(): + """An unscoped query exposes authored prims instead of silently filtering stage state.""" + stage = sim_utils.get_current_stage() + stage.DefinePrim("/World/Inactive", "Xform").SetActive(False) + stage.OverridePrim("/World/Undefined") + + matches = sim_utils.find_matching_prims(r"/World/(Inactive|Undefined)") + + assert [prim.GetPath().pathString for prim in matches] == ["/World/Inactive", "/World/Undefined"] def test_get_all_matching_child_prims(): diff --git a/source/isaaclab/test/sim/test_views_xform_prim.py b/source/isaaclab/test/sim/test_views_xform_prim.py index 9217ca537d05..0121e779f82e 100644 --- a/source/isaaclab/test/sim/test_views_xform_prim.py +++ b/source/isaaclab/test/sim/test_views_xform_prim.py @@ -89,7 +89,7 @@ def factory(num_envs: int, device: str) -> ViewBundle: sim_utils.create_prim(f"/World/Parent_{i}", "Xform", translation=PARENT_POS, stage=stage) sim_utils.create_prim(f"/World/Parent_{i}/Child", "Xform", translation=CHILD_OFFSET, stage=stage) - view = FrameView("/World/Parent_.*/Child", device=device) + view = FrameView("/World/Parent_[^/]*/Child", device=device) return ViewBundle( view=view, get_parent_pos=_get_parent_positions, @@ -116,7 +116,7 @@ def test_visibility_toggle(device): for i in range(num_prims): sim_utils.create_prim(f"/World/Object_{i}", "Xform", stage=stage) - view = FrameView("/World/Object_.*", device=device) + view = FrameView("/World/Object_[^/]*", device=device) assert torch.all(view.get_visibility()) @@ -145,7 +145,7 @@ def test_visibility_parent_inheritance(device): sim_utils.create_prim(f"/World/Parent/Child_{i}", "Xform", stage=stage) parent_view = FrameView("/World/Parent", device=device) - children_view = FrameView("/World/Parent/Child_.*", device=device) + children_view = FrameView("/World/Parent/Child_[^/]*", device=device) parent_view.set_visibility(torch.tensor([False], dtype=torch.bool, device=device)) assert not torch.any(children_view.get_visibility()) @@ -172,7 +172,7 @@ def test_prim_ordering_follows_creation_order(device): sim_utils.create_prim(f"/World/Env_{i}/Object_0", "Xform", stage=stage) sim_utils.create_prim(f"/World/Env_{i}/Object_A", "Xform", stage=stage) - view = FrameView("/World/Env_.*/Object_.*", device=device) + view = FrameView("/World/Env_[^/]*/Object_[^/]*", device=device) expected = [] for i in range(num_envs): expected += [f"/World/Env_{i}/Object_1", f"/World/Env_{i}/Object_0", f"/World/Env_{i}/Object_A"] @@ -227,8 +227,8 @@ def test_nested_hierarchy_world_poses(device): sim_utils.create_prim(f"/World/Frame_{i}", "Xform", translation=frame_positions[i], stage=stage) sim_utils.create_prim(f"/World/Frame_{i}/Target", "Xform", translation=target_positions[i], stage=stage) - frames_view = FrameView("/World/Frame_.*", device=device) - targets_view = FrameView("/World/Frame_.*/Target", device=device) + frames_view = FrameView("/World/Frame_[^/]*", device=device) + targets_view = FrameView("/World/Frame_[^/]*/Target", device=device) with frames_view.xform_local_space_writer() as w: w.set_poses(positions=torch.tensor(frame_positions, device=device)) @@ -260,7 +260,7 @@ def _make_scaled_parent_child_view(device, parent_scale, child_scale=None): sim_utils.create_prim("/World/Parent_0", "Xform", translation=PARENT_POS, scale=parent_scale, stage=stage) child_kwargs = {} if child_scale is None else {"scale": child_scale} sim_utils.create_prim("/World/Parent_0/Child", "Xform", translation=CHILD_OFFSET, stage=stage, **child_kwargs) - return FrameView("/World/Parent_.*/Child", device=device) + return FrameView("/World/Parent_[^/]*/Child", device=device) @pytest.mark.parametrize("device", ["cpu", "cuda"]) @@ -321,7 +321,7 @@ def test_compare_get_world_poses_with_isaacsim(): quat = (0.0, 0.0, 0.0, 1.0) if i % 2 == 0 else (0.0, 0.0, 0.7071068, 0.7071068) sim_utils.create_prim(f"/World/Env_{i}/Object", "Xform", translation=pos, orientation=quat, stage=stage) - pattern = "/World/Env_.*/Object" + pattern = "/World/Env_[^/]*/Object" isaacsim_paths = [f"/World/Env_{i}/Object" for i in range(num_prims)] isaaclab_view = FrameView(pattern, device="cpu") @@ -365,7 +365,7 @@ def test_with_franka_robots(device): sim_utils.create_prim("/World/Franka_1", "Xform", usd_path=franka_usd_path, stage=stage) sim_utils.create_prim("/World/Franka_2", "Xform", usd_path=franka_usd_path, stage=stage) - view = FrameView("/World/Franka_.*", device=device) + view = FrameView("/World/Franka_[^/]*", device=device) assert view.count == 2 positions = view.get_world_poses()[0].torch diff --git a/source/isaaclab/test/terrains/check_terrain_importer.py b/source/isaaclab/test/terrains/check_terrain_importer.py index 2aea7fd68b4e..4d04bab0a4d6 100644 --- a/source/isaaclab/test/terrains/check_terrain_importer.py +++ b/source/isaaclab/test/terrains/check_terrain_importer.py @@ -154,7 +154,7 @@ def main(): ) # Set ball positions over terrain origins using FrameView (before simulation starts) - xform_view = sim_utils.FrameView("/World/envs/env_.*/ball") + xform_view = sim_utils.FrameView("{ENV_REGEX_NS}/ball") # cache initial state of the balls ball_initial_positions = terrain_importer.env_origins.clone() ball_initial_positions[:, 2] += 5.0 diff --git a/source/isaaclab/test/terrains/test_terrain_importer.py b/source/isaaclab/test/terrains/test_terrain_importer.py index 553f01f79233..6b352b989672 100644 --- a/source/isaaclab/test/terrains/test_terrain_importer.py +++ b/source/isaaclab/test/terrains/test_terrain_importer.py @@ -312,7 +312,7 @@ def _populate_scene(sim: SimulationContext, num_balls: int = 2048, geom_sphere: # Set ball positions over terrain origins # Create a view over all the balls using Isaac Lab's FrameView - ball_view = sim_utils.FrameView("/World/envs/env_.*/ball") + ball_view = sim_utils.FrameView("/World/envs/env_[^/]+/ball") # cache initial state of the balls ball_initial_positions = terrain_importer.env_origins.clone() ball_initial_positions[:, 2] += 5.0 diff --git a/source/isaaclab/test/utils/test_wrench_composer_integration.py b/source/isaaclab/test/utils/test_wrench_composer_integration.py index 49bf1782fbb5..5f5e0ccb1ee6 100644 --- a/source/isaaclab/test/utils/test_wrench_composer_integration.py +++ b/source/isaaclab/test/utils/test_wrench_composer_integration.py @@ -45,7 +45,7 @@ def generate_cubes_scene( ) cube_object_cfg = RigidObjectCfg( - prim_path="/World/Table_.*/Object", + prim_path="/World/Table_[^/]*/Object", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height)), ) diff --git a/source/isaaclab/test/utils/test_wrench_composer_vs_physx.py b/source/isaaclab/test/utils/test_wrench_composer_vs_physx.py index 122f80021e10..f186993d3b8a 100644 --- a/source/isaaclab/test/utils/test_wrench_composer_vs_physx.py +++ b/source/isaaclab/test/utils/test_wrench_composer_vs_physx.py @@ -73,14 +73,14 @@ def generate_dual_cube_scene( ) cube_composer_cfg = RigidObjectCfg( - prim_path="/World/Composer_.*/Object", + prim_path="/World/Composer_[^/]*/Object", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height), rot=initial_rot), ) cube_composer = RigidObject(cfg=cube_composer_cfg) cube_raw_cfg = RigidObjectCfg( - prim_path="/World/Raw_.*/Object", + prim_path="/World/Raw_[^/]*/Object", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, y_offset, height), rot=initial_rot), ) diff --git a/source/isaaclab/test/visualizers/test_visualizer.py b/source/isaaclab/test/visualizers/test_visualizer.py index b5164c93aab7..b78d802aae94 100644 --- a/source/isaaclab/test/visualizers/test_visualizer.py +++ b/source/isaaclab/test/visualizers/test_visualizer.py @@ -155,7 +155,7 @@ def test_prim_world_positions_prefers_scene_articulation_state(): ] ) articulation = SimpleNamespace( - cfg=SimpleNamespace(prim_path="/World/envs/env_.*/Robot"), + cfg=SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot"), body_names=["base", "foot"], data=SimpleNamespace( root_pos_w=SimpleNamespace(torch=torch.zeros((2, 3))), diff --git a/source/isaaclab_assets/changelog.d/octi-prim-path-real-regex-matcher.rst b/source/isaaclab_assets/changelog.d/octi-prim-path-real-regex-matcher.rst new file mode 100644 index 000000000000..98b0493cabd1 --- /dev/null +++ b/source/isaaclab_assets/changelog.d/octi-prim-path-real-regex-matcher.rst @@ -0,0 +1,6 @@ +Changed +^^^^^^^ + +* Changed prim path expressions to spell a single path segment ``[^/]`` rather than ``.``, so each + pattern selects what it selected before now that ``.`` matches ``/`` in + :func:`~isaaclab.sim.utils.find_matching_prims`. diff --git a/source/isaaclab_assets/isaaclab_assets/robots/unitree.py b/source/isaaclab_assets/isaaclab_assets/robots/unitree.py index 8e4f692ca6df..cb72fcfe46ab 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/unitree.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/unitree.py @@ -534,7 +534,7 @@ armature=0.001, ), }, - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", ) """Configuration for the Unitree G1 Humanoid robot for locomanipulation tasks. @@ -632,7 +632,7 @@ enabled_self_collisions=False, solver_position_iteration_count=4, solver_velocity_iteration_count=0 ), ), - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", init_state=ArticulationCfg.InitialStateCfg( pos=(0.0, 0.0, 0.75), joint_pos={ diff --git a/source/isaaclab_contrib/changelog.d/octi-prim-path-real-regex-matcher.rst b/source/isaaclab_contrib/changelog.d/octi-prim-path-real-regex-matcher.rst new file mode 100644 index 000000000000..98b0493cabd1 --- /dev/null +++ b/source/isaaclab_contrib/changelog.d/octi-prim-path-real-regex-matcher.rst @@ -0,0 +1,6 @@ +Changed +^^^^^^^ + +* Changed prim path expressions to spell a single path segment ``[^/]`` rather than ``.``, so each + pattern selects what it selected before now that ``.`` matches ``/`` in + :func:`~isaaclab.sim.utils.find_matching_prims`. diff --git a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py index d760321862cc..838fbb657025 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py +++ b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py @@ -70,7 +70,7 @@ class Multirotor(Articulation): # Create multirotor configuration multirotor_cfg = MultirotorCfg( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", spawn=sim_utils.UsdFileCfg(usd_path="path/to/quadcopter.usd"), actuators={"thrusters": thruster_cfg}, allocation_matrix=[ # 6x4 matrix for quadcopter (6 DOF, 4 thrusters) diff --git a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_cfg.py index 3d3037ddb9bc..63aeb1670e63 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_cfg.py +++ b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_cfg.py @@ -47,7 +47,7 @@ class MultirotorCfg(ArticulationCfg): # Quadcopter configuration quadcopter_cfg = MultirotorCfg( - prim_path="/World/envs/env_.*/Quadcopter", + prim_path="{ENV_REGEX_NS}/Quadcopter", spawn=sim_utils.UsdFileCfg( usd_path="path/to/quadcopter.usd", ), diff --git a/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py b/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py index 424316028ad2..c71dec35be40 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py +++ b/source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py @@ -203,8 +203,9 @@ def setup_registered_deformable_fabric_sync(manager_cls: type[SimulationManager] synced_any = False for entry in manager_cls._deformable_registry: for inst_idx, offset in enumerate(entry.particle_offsets): - resolved_vis = re.sub(r"(?<=[Ee]nv_)\.\*", str(inst_idx), entry.vis_mesh_prim_path) - resolved_vis = re.sub(r"\.\*", str(inst_idx), resolved_vis) + resolved_vis = re.sub(r"(?<=[Ee]nv_)(?:\[\^/\][*+]|\.\*)", str(inst_idx), entry.vis_mesh_prim_path) + # any wildcard left over stands for the instance too, in whichever way it is spelled + resolved_vis = re.sub(r"\[\^/\][*+]|\.\*", str(inst_idx), resolved_vis) vis_prim = stage.GetPrimAtPath(resolved_vis) if not vis_prim or not vis_prim.IsValid(): diff --git a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor.py b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor.py index 29d96759bae5..395df63b27f0 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor.py +++ b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor.py @@ -322,9 +322,9 @@ def _create_physx_views(self) -> None: # Resolve the elastomer's destination expression (multi-env glob form for PhysX views). # The sensor's cfg.prim_path lives under the elastomer; the parent expression is the # elastomer body itself (matching :attr:`SensorBase._parent_prims`). - elastomer_expr = self.cfg.prim_path.rsplit("/", 1)[0] + elastomer_expr = "/".join(sim_utils.split_path_expr(self.cfg.prim_path)[:-1]) elastomer_dest_expr = sim_utils.resolve_matching_prims_from_source(elastomer_expr)[0][1] - elastomer_pattern = elastomer_dest_expr.replace(".*", "*") + elastomer_pattern = sim_utils.path_expr_to_glob(elastomer_dest_expr) self._elastomer_body_view = self._physics_sim_view.create_rigid_body_view([elastomer_pattern]) # Get elastomer COM for velocity correction self._elastomer_com_b = ( @@ -424,7 +424,7 @@ def _generate_tactile_points(self, num_divs: list, margin: float, visualize: boo # Resolve the elastomer's source-side env prim and use it as the walk root. # The sensor's cfg.prim_path lives under the elastomer; the parent expression is the # elastomer body itself (matching :attr:`SensorBase._parent_prims`). - elastomer_expr = self.cfg.prim_path.rsplit("/", 1)[0] + elastomer_expr = "/".join(sim_utils.split_path_expr(self.cfg.prim_path)[:-1]) elastomer_prim_path = sim_utils.resolve_matching_prims_from_source(elastomer_expr)[0][0].GetPath().pathString def is_visual_mesh(prim) -> bool: diff --git a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_cfg.py index 8f5b0d187e1d..c43307750109 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_cfg.py +++ b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_cfg.py @@ -154,7 +154,7 @@ class VisuoTactileSensorCfg(SensorBaseCfg): The expression can contain the environment namespace regex ``{ENV_REGEX_NS}`` which will be replaced with the environment namespace. - Example: ``{ENV_REGEX_NS}/ContactObject`` will be replaced with ``/World/envs/env_.*/ContactObject``. + Example: ``{ENV_REGEX_NS}/ContactObject`` will be replaced with ``/World/envs/env_[^/]+/ContactObject``. .. attention:: For force field computation to work properly, the contact object must have an SDF collision mesh. diff --git a/source/isaaclab_contrib/test/assets/test_multirotor.py b/source/isaaclab_contrib/test/assets/test_multirotor.py index 84044231b16c..0d737aafcd47 100644 --- a/source/isaaclab_contrib/test/assets/test_multirotor.py +++ b/source/isaaclab_contrib/test/assets/test_multirotor.py @@ -284,7 +284,7 @@ def generate_multirotor( # or simulator not available) fall back to the simulator-free stub so # tests can still run and validate behavior without IsaacSim. try: - multirotor = Multirotor(multirotor_cfg.replace(prim_path="/World/Env_.*/Robot")) + multirotor = Multirotor(multirotor_cfg.replace(prim_path="/World/Env_[^/]*/Robot")) return multirotor, translations except Exception: # Determine a reasonable number of thrusters for the stub from the diff --git a/source/isaaclab_contrib/test/coupling/test_coupler.py b/source/isaaclab_contrib/test/coupling/test_coupler.py index 85a58475779e..5bf2a3fe6dc7 100644 --- a/source/isaaclab_contrib/test/coupling/test_coupler.py +++ b/source/isaaclab_contrib/test/coupling/test_coupler.py @@ -197,7 +197,7 @@ def test_config_validation_requires_concrete_nested_factory(): def test_string_selector_resolves_full_body_labels_and_descendants(): assert NewtonCouplerManager._resolve_entities_to_body_ids( _FakeModel(), - ["/World/envs/env_.*/Robot"], + ["/World/envs/env_[^/]+/Robot"], "entry 'rigid'", ) == [0, 1] @@ -215,7 +215,7 @@ def test_raw_body_label_selector_reports_no_matches(): with pytest.raises(ValueError, match="matched no Newton bodies"): NewtonCouplerManager._resolve_entities_to_body_ids( _FakeModel(), - ["/World/envs/env_.*/Missing"], + ["/World/envs/env_[^/]+/Missing"], "entry 'missing'", ) @@ -234,7 +234,7 @@ def test_proxy_resolution_writes_body_ids_into_config_in_place(): """Resolution replaces the proxy's selectors with body ids and is idempotent.""" model = _FakeModel() proxy = CouplerProxyMappingCfg( - source="rigid", destination="soft", bodies=[r"/World/envs/env_.*/Robot"], particles=[1, 1] + source="rigid", destination="soft", bodies=[r"/World/envs/env_[^/]+/Robot"], particles=[1, 1] ) resolved = NewtonCouplerManager._resolve_proxy(model, proxy) @@ -253,12 +253,12 @@ def test_three_named_entries_partition_bodies_joints_shapes_and_particles(): CouplerEntryCfg( name="rigid", solver_cfg=XPBDSolverCfg(), - bodies=[r"/World/envs/env_.*/Robot"], + bodies=[r"/World/envs/env_[^/]+/Robot"], ), CouplerEntryCfg( name="object", solver_cfg=XPBDSolverCfg(), - bodies=["/World/envs/env_.*/Object"], + bodies=["/World/envs/env_[^/]+/Object"], all_particles=True, ), CouplerEntryCfg( @@ -280,8 +280,8 @@ def test_three_named_entries_partition_bodies_joints_shapes_and_particles(): assert resolved[2].bodies == [] assert resolved[2].joints == [] assert resolved[2].shapes == [3] - assert entries[0].bodies == [r"/World/envs/env_.*/Robot"] - assert entries[1].bodies == ["/World/envs/env_.*/Object"] + assert entries[0].bodies == [r"/World/envs/env_[^/]+/Robot"] + assert entries[1].bodies == ["/World/envs/env_[^/]+/Object"] def test_cross_entry_joint_is_left_unowned_for_admm_attachment(): @@ -292,12 +292,12 @@ def test_cross_entry_joint_is_left_unowned_for_admm_attachment(): CouplerEntryCfg( name="robot", solver_cfg=XPBDSolverCfg(), - bodies=[r"/World/envs/env_.*/Robot"], + bodies=[r"/World/envs/env_[^/]+/Robot"], ), CouplerEntryCfg( name="object", solver_cfg=XPBDSolverCfg(), - bodies=[r"/World/envs/env_.*/Object"], + bodies=[r"/World/envs/env_[^/]+/Object"], all_particles=True, include_static_shapes=True, ), @@ -352,7 +352,7 @@ def test_shape_label_patterns_and_static_shape_selection_are_additive(): CouplerEntryCfg( name="special", solver_cfg=XPBDSolverCfg(), - bodies=[r"/World/envs/env_.*/Robot/base"], + bodies=[r"/World/envs/env_[^/]+/Robot/base"], include_body_shapes=False, include_static_shapes=True, shape_label_patterns=[r".*/Object/object_collision"], @@ -369,7 +369,7 @@ def test_proxy_resolution_keeps_only_collidable_selected_bodies(): ) proxy = NewtonCouplerManager._resolve_proxy( model, - CouplerProxyMappingCfg(source="rigid", destination="soft", bodies=[r"/World/envs/env_.*/Robot"]), + CouplerProxyMappingCfg(source="rigid", destination="soft", bodies=[r"/World/envs/env_[^/]+/Robot"]), ) assert proxy.bodies == [0] @@ -401,7 +401,7 @@ def custom_pipeline(model_view): CouplerProxyMappingCfg( source="rigid", destination="soft", - bodies=["/World/envs/env_.*/Robot/base"], + bodies=["/World/envs/env_[^/]+/Robot/base"], mode="staggered", mass_scale=0.25, collide_interval=4, diff --git a/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py b/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py index 393458d15335..473d4589c7fa 100644 --- a/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py +++ b/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py @@ -80,12 +80,12 @@ def generate_robot_and_two_cubes( cfg = sim_utils.GroundPlaneCfg() cfg.func("/World/defaultGroundPlane", cfg) - robot_cfg = FRANKA_PANDA_CFG.replace(prim_path="/World/env_.*/Robot") + robot_cfg = FRANKA_PANDA_CFG.replace(prim_path="/World/env_[^/]+/Robot") robot = Articulation(robot_cfg) colliding_cube = DeformableObject( cfg=DeformableObjectCfg( - prim_path="/World/env_.*/cube_collide", + prim_path="/World/env_[^/]+/cube_collide", spawn=sim_utils.MeshCuboidCfg( size=(0.05, 0.05, 0.05), deformable_props=NewtonDeformableBodyPropertiesCfg(), @@ -103,7 +103,7 @@ def generate_robot_and_two_cubes( free_cube = DeformableObject( cfg=DeformableObjectCfg( - prim_path="/World/env_.*/cube_free", + prim_path="/World/env_[^/]+/cube_free", spawn=sim_utils.MeshCuboidCfg( size=(0.05, 0.05, 0.05), deformable_props=NewtonDeformableBodyPropertiesCfg(), @@ -131,7 +131,7 @@ def generate_lateral_rigid_and_deformable_cubes( rigid_cube = RigidObject( cfg=RigidObjectCfg( - prim_path="/World/env_.*/rigid_cube", + prim_path="/World/env_[^/]+/rigid_cube", spawn=sim_utils.CuboidCfg( size=(0.2, 0.2, 0.2), rigid_props=sim_utils.RigidBodyPropertiesCfg(), @@ -145,7 +145,7 @@ def generate_lateral_rigid_and_deformable_cubes( deformable_cube = DeformableObject( cfg=DeformableObjectCfg( - prim_path="/World/env_.*/deformable_cube", + prim_path="/World/env_[^/]+/deformable_cube", spawn=sim_utils.MeshCuboidCfg( size=(0.08, 0.08, 0.08), deformable_props=NewtonDeformableBodyPropertiesCfg(), diff --git a/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py b/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py index db6dddc3b3a4..db0b40492ffd 100644 --- a/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py +++ b/source/isaaclab_contrib/test/deformable/test_deformable_builder_hooks.py @@ -60,9 +60,9 @@ def GetPrimAtPath(self, path: str) -> _FakePrim: def _make_surface_entry() -> DeformableRegistryEntry: half_sqrt = math.sqrt(0.5) return DeformableRegistryEntry( - prim_path="/World/envs/env_.*/cloth", - sim_mesh_prim_path="/World/envs/env_.*/cloth/mesh", - vis_mesh_prim_path="/World/envs/env_.*/cloth/mesh", + prim_path="{ENV_REGEX_NS}/cloth", + sim_mesh_prim_path="{ENV_REGEX_NS}/cloth/mesh", + vis_mesh_prim_path="{ENV_REGEX_NS}/cloth/mesh", vertices=[ wp.vec3(0.0, 0.0, 0.0), wp.vec3(1.0, 0.0, 0.0), diff --git a/source/isaaclab_contrib/test/deformable/test_deformable_object.py b/source/isaaclab_contrib/test/deformable/test_deformable_object.py index 1954723959a3..1676fd654cec 100644 --- a/source/isaaclab_contrib/test/deformable/test_deformable_object.py +++ b/source/isaaclab_contrib/test/deformable/test_deformable_object.py @@ -69,7 +69,7 @@ def generate_cubes_scene( sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=origin) cube_object_cfg = DeformableObjectCfg( - prim_path="/World/env_.*/Cube", + prim_path="/World/env_[^/]+/Cube", spawn=sim_utils.MeshCuboidCfg( size=(0.1, 0.1, 0.1), deformable_props=NewtonDeformableBodyPropertiesCfg(), @@ -109,7 +109,7 @@ def generate_cloth_scene( sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=origin) cloth_object_cfg = DeformableObjectCfg( - prim_path="/World/env_.*/Cloth", + prim_path="/World/env_[^/]+/Cloth", spawn=sim_utils.MeshRectangleCfg( size=(0.2, 0.2), resolution=(3, 3), @@ -130,7 +130,7 @@ def generate_cuboid_and_cylinder_scene(height: float = 1.0) -> tuple[DeformableO sim_utils.create_prim("/World/env_0", "Xform", translation=(0.0, 0.0, 0.0)) cuboid_cfg = DeformableObjectCfg( - prim_path="/World/env_.*/Cuboid", + prim_path="/World/env_[^/]+/Cuboid", spawn=sim_utils.MeshCuboidCfg( size=(0.16, 0.08, 0.12), deformable_props=NewtonDeformableBodyPropertiesCfg(), @@ -144,7 +144,7 @@ def generate_cuboid_and_cylinder_scene(height: float = 1.0) -> tuple[DeformableO init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height)), ) cylinder_cfg = DeformableObjectCfg( - prim_path="/World/env_.*/Cylinder", + prim_path="/World/env_[^/]+/Cylinder", spawn=sim_utils.MeshCylinderCfg( radius=0.06, height=0.14, diff --git a/source/isaaclab_mimic/changelog.d/octi-prim-path-real-regex-matcher.rst b/source/isaaclab_mimic/changelog.d/octi-prim-path-real-regex-matcher.rst new file mode 100644 index 000000000000..98b0493cabd1 --- /dev/null +++ b/source/isaaclab_mimic/changelog.d/octi-prim-path-real-regex-matcher.rst @@ -0,0 +1,6 @@ +Changed +^^^^^^^ + +* Changed prim path expressions to spell a single path segment ``[^/]`` rather than ``.``, so each + pattern selects what it selected before now that ``.`` matches ``/`` in + :func:`~isaaclab.sim.utils.find_matching_prims`. diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/g1_locomanipulation_sdg_env.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/g1_locomanipulation_sdg_env.py index a1419bc5488a..21851dd38213 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/g1_locomanipulation_sdg_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/g1_locomanipulation_sdg_env.py @@ -40,7 +40,7 @@ @configclass class G1LocomanipulationSDGSceneCfg(LocomanipulationG1SceneCfg): packing_table_2 = AssetBaseCfg( - prim_path="/World/envs/env_.*/PackingTable2", + prim_path="{ENV_REGEX_NS}/PackingTable2", init_state=AssetBaseCfg.InitialStateCfg( pos=[-2, -3.55, -0.3], # rot=[0, 0, 0, 1]), @@ -54,7 +54,7 @@ class G1LocomanipulationSDGSceneCfg(LocomanipulationG1SceneCfg): def add_robot_pov_cam(self, height, width): robot_pov_cam = CameraCfg( - prim_path="/World/envs/env_.*/Robot/torso_link/d435_link/camera", + prim_path="{ENV_REGEX_NS}/Robot/torso_link/d435_link/camera", update_period=0.0, height=height, width=width, @@ -66,7 +66,7 @@ def add_robot_pov_cam(self, height, width): def add_background_asset(self, background_usd_path: str): background = AssetBaseCfg( - prim_path="/World/envs/env_.*/Background", + prim_path="{ENV_REGEX_NS}/Background", init_state=AssetBaseCfg.InitialStateCfg( pos=[0, 0, 0], rot=[0.0, 0.0, 0.0, 1.0], @@ -81,7 +81,7 @@ def add_background_asset(self, background_usd_path: str): def add_forklifts(self, num_forklifts: int): for i in range(num_forklifts): forklift = AssetBaseCfg( - prim_path=f"/World/envs/env_.*/Forklift{i}", + prim_path=f"/World/envs/env_[^/]+/Forklift{i}", init_state=AssetBaseCfg.InitialStateCfg(pos=[0.0, 0.0, 0.0], rot=[0.0, 0.0, 0.0, 1.0]), spawn=UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Forklift/forklift.usd", @@ -93,7 +93,7 @@ def add_forklifts(self, num_forklifts: int): def add_boxes(self, num_boxes: int): for i in range(num_boxes): box = AssetBaseCfg( - prim_path=f"/World/envs/env_.*/Box{i}", + prim_path=f"/World/envs/env_[^/]+/Box{i}", init_state=AssetBaseCfg.InitialStateCfg(pos=[0.0, 0.0, 0.0], rot=[0.0, 0.0, 0.0, 1.0]), spawn=UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Simple_Warehouse/Props/SM_CardBoxB_01_681.usd", diff --git a/source/isaaclab_newton/changelog.d/octi-prim-path-real-regex-matcher.rst b/source/isaaclab_newton/changelog.d/octi-prim-path-real-regex-matcher.rst new file mode 100644 index 000000000000..4d2f9a23dd4d --- /dev/null +++ b/source/isaaclab_newton/changelog.d/octi-prim-path-real-regex-matcher.rst @@ -0,0 +1,13 @@ +Changed +^^^^^^^ + +* Changed prim path expressions to spell a single path segment ``[^/]`` rather than ``.``, so each + pattern selects what it selected before now that ``.`` matches ``/`` in + :func:`~isaaclab.sim.utils.find_matching_prims`. + +Fixed +^^^^^ + +* Fixed :class:`~isaaclab.sensors.MultiMeshRayCaster` raising ``KeyError`` on a tracked ray-cast + target under Newton, because the environment slot was spelled one way when the target was + registered and another when it was looked up. diff --git a/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py b/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py index f9b1284495e7..0a55cfab2c68 100644 --- a/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py +++ b/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py @@ -26,7 +26,7 @@ from isaaclab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator from isaaclab.assets.articulation import ordering_kernels from isaaclab.assets.articulation.base_articulation import BaseArticulation -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source _HAS_NEWTON_ACTUATORS = importlib.util.find_spec("isaaclab_newton.actuators") is not None @@ -93,7 +93,7 @@ def has_articulation_root_api(prim) -> bool: def _configure_builder_joint_target_modes(builder, cfg: ArticulationCfg) -> None: """Resolve configured actuator gains into Newton builder target modes before finalization.""" - root_prim_path_regex = _resolve_articulation_root_prim_path_expr(cfg).replace(".*", "*").replace("*", ".*") + root_prim_path_regex = path_expr_to_glob(_resolve_articulation_root_prim_path_expr(cfg)).replace("*", ".*") articulation_ids, _ = resolve_matching_names( root_prim_path_regex, builder.articulation_label, raise_when_no_match=False ) @@ -3658,7 +3658,7 @@ def _initialize_impl(self): # -- articulation self._root_view = ArticulationView( SimulationManager.get_model(), - root_prim_path_expr.replace(".*", "*"), + path_expr_to_glob(root_prim_path_expr), verbose=False, exclude_joint_types=[JointType.FREE, JointType.FIXED], ) diff --git a/source/isaaclab_newton/isaaclab_newton/assets/cable_object/cable_object.py b/source/isaaclab_newton/isaaclab_newton/assets/cable_object/cable_object.py index 05234eb61947..7713ce7cadd0 100644 --- a/source/isaaclab_newton/isaaclab_newton/assets/cable_object/cable_object.py +++ b/source/isaaclab_newton/isaaclab_newton/assets/cable_object/cable_object.py @@ -18,7 +18,7 @@ from isaaclab.assets.cable_object.base_cable_object import BaseCableObject from isaaclab.cloner import queue_replication from isaaclab.physics import PhysicsEvent -from isaaclab.sim.utils.queries import has_deformable_curve_api, resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import has_deformable_curve_api, path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.warp import ProxyArray from isaaclab_newton.physics import NewtonManager as SimulationManager @@ -196,7 +196,7 @@ def is_cable_curve(prim) -> bool: articulation_path_expr = f"{curve_path_expr}_articulation" self._root_view = ArticulationView( model, - articulation_path_expr.replace(".*", "*"), + path_expr_to_glob(articulation_path_expr), verbose=False, ) topology_error = "CableObject requires one standalone, unwelded cable articulation per simulation world." diff --git a/source/isaaclab_newton/isaaclab_newton/assets/rigid_object/rigid_object.py b/source/isaaclab_newton/isaaclab_newton/assets/rigid_object/rigid_object.py index d789ec05cec4..7a6808c9315b 100644 --- a/source/isaaclab_newton/isaaclab_newton/assets/rigid_object/rigid_object.py +++ b/source/isaaclab_newton/isaaclab_newton/assets/rigid_object/rigid_object.py @@ -20,7 +20,7 @@ import isaaclab.utils.string as string_utils from isaaclab.assets.rigid_object.base_rigid_object import BaseRigidObject from isaaclab.physics import PhysicsEvent -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.warp import ProxyArray from isaaclab.utils.wrench_composer import WrenchComposer @@ -1057,7 +1057,7 @@ def has_rigid_body_api(prim) -> bool: # -- object view self._root_view = ArticulationView( SimulationManager.get_model(), - root_prim_path_expr.replace(".*", "*"), + path_expr_to_glob(root_prim_path_expr), verbose=False, ) diff --git a/source/isaaclab_newton/isaaclab_newton/assets/rigid_object_collection/rigid_object_collection.py b/source/isaaclab_newton/isaaclab_newton/assets/rigid_object_collection/rigid_object_collection.py index e450e2d04747..fcc40f0f8fb6 100644 --- a/source/isaaclab_newton/isaaclab_newton/assets/rigid_object_collection/rigid_object_collection.py +++ b/source/isaaclab_newton/isaaclab_newton/assets/rigid_object_collection/rigid_object_collection.py @@ -1200,7 +1200,7 @@ def has_rigid_body_api(prim) -> bool: for name, obj_cfg in self.cfg.rigid_objects.items(): _, root_expr = sim_utils.resolve_matching_prims_from_source(obj_cfg.prim_path, **resolve_kwargs)[0] - root_prim_path_exprs.append(root_expr.replace(".*", "*")) + root_prim_path_exprs.append(sim_utils.path_expr_to_glob(root_expr)) self._body_names_list.append(name) # Build a single pattern that matches ALL body types by wildcarding the differing path segment. diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 799de90ff70c..4b4d44d6f768 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -88,7 +88,7 @@ def _paused_gc(): ) from isaaclab.sim import SimulationContext from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors -from isaaclab.sim.utils.queries import has_deformable_curve_api +from isaaclab.sim.utils.queries import has_deformable_curve_api, path_expr_to_glob from isaaclab.sim.utils.stage import get_current_stage from isaaclab.utils import checked_apply from isaaclab.utils.string import resolve_matching_names @@ -3269,8 +3269,8 @@ def _to_fnmatch(expr: str | list[str] | None) -> str | list[str] | None: if expr is None: return None if isinstance(expr, str): - return expr.replace(".*", "*") - return [p.replace(".*", "*") for p in expr] + return path_expr_to_glob(expr) + return [path_expr_to_glob(p) for p in expr] def _normalize_for_labels(expr: str | list[str] | None, labels: list[str]) -> str | list[str] | None: """Strip leading path components from *expr* when labels are bare names. diff --git a/source/isaaclab_newton/isaaclab_newton/sensors/frame_transformer/frame_transformer.py b/source/isaaclab_newton/isaaclab_newton/sensors/frame_transformer/frame_transformer.py index 2037e595438f..d8a88bbe0102 100644 --- a/source/isaaclab_newton/isaaclab_newton/sensors/frame_transformer/frame_transformer.py +++ b/source/isaaclab_newton/isaaclab_newton/sensors/frame_transformer/frame_transformer.py @@ -11,6 +11,7 @@ import warp as wp from isaaclab.sensors.frame_transformer.base_frame_transformer import BaseFrameTransformer +from isaaclab.sim.utils.queries import split_path_expr from isaaclab_newton.physics import NewtonManager @@ -62,7 +63,7 @@ def __init__(self, cfg: FrameTransformerCfg): self._stride: int = 0 self._sensor_index: int | None = None - self._source_frame_body_name: str = cfg.prim_path.rsplit("/", 1)[-1] + self._source_frame_body_name: str = split_path_expr(cfg.prim_path)[-1] # Register world-origin reference site self._world_origin_label = NewtonManager.cl_register_site(None, wp.transform()) @@ -81,12 +82,12 @@ def __init__(self, cfg: FrameTransformerCfg): label = NewtonManager.cl_register_site(target_frame.prim_path, target_offset) self._target_labels.append(label) - body_name = target_frame.prim_path.rsplit("/", 1)[-1] + body_name = split_path_expr(target_frame.prim_path)[-1] self._target_frame_body_names.append(target_frame.name or body_name) self._num_targets += 1 # Set target frame names for base class find_bodies() and data container - self._target_frame_names = [t.name or t.prim_path.rsplit("/", 1)[-1] for t in cfg.target_frames] + self._target_frame_names = [t.name or split_path_expr(t.prim_path)[-1] for t in cfg.target_frames] self._data._target_frame_names = self._target_frame_names logger.info( diff --git a/source/isaaclab_newton/isaaclab_newton/sensors/joint_wrench/joint_wrench_sensor.py b/source/isaaclab_newton/isaaclab_newton/sensors/joint_wrench/joint_wrench_sensor.py index 9d7fcd0e9e08..6ad958495e3c 100644 --- a/source/isaaclab_newton/isaaclab_newton/sensors/joint_wrench/joint_wrench_sensor.py +++ b/source/isaaclab_newton/isaaclab_newton/sensors/joint_wrench/joint_wrench_sensor.py @@ -16,7 +16,7 @@ from pxr import UsdPhysics from isaaclab.sensors.joint_wrench import BaseJointWrenchSensor -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab_newton.physics import NewtonManager @@ -135,7 +135,7 @@ def has_articulation_root_api(prim) -> bool: _, root_prim_path_expr = resolve_matching_prims_from_source(self.cfg.prim_path, **resolve_kwargs)[0] self._root_view = ArticulationView( model, - root_prim_path_expr.replace(".*", "*"), + path_expr_to_glob(root_prim_path_expr), verbose=False, exclude_joint_types=[JointType.FREE, JointType.FIXED], ) diff --git a/source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/legacy_ray_caster.py b/source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/legacy_ray_caster.py index 857508f5c96b..dcb297b50516 100644 --- a/source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/legacy_ray_caster.py +++ b/source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/legacy_ray_caster.py @@ -16,7 +16,6 @@ from pxr import UsdPhysics import isaaclab.sim as sim_utils -from isaaclab import cloner from isaaclab.sensors.ray_caster.base_multi_mesh_ray_caster import BaseMultiMeshRayCaster from isaaclab.sensors.ray_caster.base_multi_mesh_ray_caster_camera import BaseMultiMeshRayCasterCamera from isaaclab.sensors.ray_caster.base_ray_caster import BaseRayCaster @@ -47,32 +46,11 @@ def __init__(self: Any, cfg): def _resolve_target_owner_exprs(self, prim_expr: str) -> list[str]: """Resolve mesh target expressions to owning rigid-body expressions.""" - plan = sim_utils.SimulationContext.instance().get_clone_plan() - resolved = cloner.query.path_to_source(plan, prim_expr) if plan is not None else None - if resolved is not None: - source_path, dest_glob, asset_suffix = resolved - walk_root = source_path + asset_suffix - source_prims = sim_utils.find_matching_prims(walk_root) - if not source_prims: - raise RuntimeError(f"No ClonePlan source prims matched '{walk_root}'.") - owner_exprs: list[str] = [] - for source_prim in source_prims: - body = sim_utils.get_first_matching_ancestor_prim(source_prim.GetPath(), predicate=_has_rigid_body_api) - if body is None: - raise RuntimeError( - f"Cannot track non-physics ray-cast target '{prim_expr}' with Newton. " - "Set track_mesh_transforms=False for static targets, or apply RigidBodyAPI " - "to dynamic targets." - ) - owner_prim_path = str(body.GetPath()) - owner_exprs.append(dest_glob + owner_prim_path[len(source_path) :]) - return list(dict.fromkeys(owner_exprs)) - - prims = sim_utils.find_matching_prims(prim_expr) - if not prims: + matches = sim_utils.resolve_matching_prims_from_source(prim_expr, raise_if_no_matches=False) + if not matches: return [_newton_body_pattern(prim_expr)] owner_exprs = [] - for prim in prims: + for prim, dest_expr in matches: body = sim_utils.get_first_matching_ancestor_prim(prim.GetPath(), predicate=_has_rigid_body_api) if body is None: raise RuntimeError( @@ -80,7 +58,9 @@ def _resolve_target_owner_exprs(self, prim_expr: str) -> list[str]: "Set track_mesh_transforms=False for static targets, or apply RigidBodyAPI " "to dynamic targets." ) - owner_exprs.append(_newton_body_pattern(str(body.GetPath()))) + prim_path = prim.GetPath().pathString + owner_suffix = prim_path[len(body.GetPath().pathString) :] + owner_exprs.append(_newton_body_pattern(dest_expr.removesuffix(owner_suffix))) return list(dict.fromkeys(owner_exprs)) def _register_target_sites_for_exprs(self, owner_exprs: list[str]) -> list[str]: diff --git a/source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/newton_raycast_sensor.py b/source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/newton_raycast_sensor.py index c6448d6b62f2..717dd9c99895 100644 --- a/source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/newton_raycast_sensor.py +++ b/source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/newton_raycast_sensor.py @@ -14,6 +14,7 @@ import isaaclab.sim as sim_utils from isaaclab import cloner +from isaaclab.cloner.cloner_cfg import DEFAULT_ENV_TEMPLATE from isaaclab.sensors.ray_caster.base_ray_caster import BaseRayCaster from isaaclab.sensors.ray_caster.kernels import ALIGNMENT_BASE, update_ray_caster_kernel from isaaclab.utils.warp import ProxyArray @@ -63,10 +64,16 @@ def _gather_pose_by_index_kernel( quat_dst[index] = quat_src[source_index] +# the clone slot, spelled the way every other destination expression spells it, so patterns +# built here compare equal to ones built from the destination template. +_ENV_SLOT_NS = DEFAULT_ENV_TEMPLATE.format("[^/]+") +_CONCRETE_ENV_NS = re.compile(rf"^{re.escape(DEFAULT_ENV_TEMPLATE.format(''))}\d+/") + + def _newton_body_pattern(body_path: str) -> str: """Convert a concrete environment index to a prototype body pattern.""" - body_path = body_path.replace("{}", ".*") - return re.sub(r"^(/World/envs/)env_\d+/", r"\1env_.*/", body_path) + body_path = body_path.replace("{}", "[^/]+") + return _CONCRETE_ENV_NS.sub(_ENV_SLOT_NS + "/", body_path) def _identity_offsets(count: int, device: str) -> tuple[wp.array, wp.array]: @@ -95,8 +102,8 @@ def _register_sites_for_expr(self, prim_expr: str) -> list[str]: plan = sim_utils.SimulationContext.instance().get_clone_plan() if plan is not None: for destination_template in plan.destinations: - destination_prefix, _ = cloner.path.split(destination_template) - if prim_expr.startswith(destination_prefix) and "/" not in prim_expr[len(destination_prefix) :]: + matched = cloner.path.match(prim_expr, destination_template) + if matched is not None and not matched.suffix: return [NewtonManager.cl_register_site(None, wp.transform(), per_world=True)] try: @@ -105,8 +112,9 @@ def _register_sites_for_expr(self, prim_expr: str) -> list[str]: # Preserve support for sensor paths registered before their USD prim # exists. Known camera/raycaster child names attach to their parent. body_expr = prim_expr - if prim_expr.rsplit("/", 1)[-1].lower() in ("camera", "raycaster"): - body_expr = prim_expr.rsplit("/", 1)[0] + *parent_segments, leaf_segment = sim_utils.split_path_expr(prim_expr) + if leaf_segment.lower() in ("camera", "raycaster"): + body_expr = "/".join(parent_segments) fixed_pos = None fixed_quat = None diff --git a/source/isaaclab_newton/isaaclab_newton/sim/views/newton_site_frame_view.py b/source/isaaclab_newton/isaaclab_newton/sim/views/newton_site_frame_view.py index 089fc6d4717e..89e5d528889c 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/views/newton_site_frame_view.py +++ b/source/isaaclab_newton/isaaclab_newton/sim/views/newton_site_frame_view.py @@ -8,6 +8,7 @@ from __future__ import annotations import logging +import re import warp as wp @@ -271,14 +272,15 @@ def _resolve_site_specs( matches = tuple(cloner.query.iter_sources(plan, path_expr)) if plan is not None else () if matches: for source_root, destination_template, source_path, env_ids in matches: - source_prim = None - if not any(token in source_path for token in "*[]()+?|\\"): - source_prim = stage.GetPrimAtPath(source_path) - if source_prim is None or not source_prim.IsValid(): - source_prim = sim_utils.find_first_matching_prim(source_path, stage) - if source_prim is None or not source_prim.IsValid(): + source_pattern = re.compile(source_path) + source_prims = sim_utils.get_all_matching_child_prims( + source_root, + lambda prim: source_pattern.fullmatch(prim.GetPath().pathString) is not None, + stage=stage, + ) + if not source_prims: raise RuntimeError(f"FrameView '{path_expr}' could not resolve source prim '{source_path}'.") - specs.append( + specs.extend( self._resolve_source_prim( source_prim, validate_xform_ops, @@ -288,14 +290,16 @@ def _resolve_site_specs( use_clone_body_pattern, stage, ) + for source_prim in source_prims ) continue - prim = sim_utils.find_first_matching_prim(path_expr, stage) - if prim is None or not prim.IsValid(): + prims = sim_utils.find_matching_prims(path_expr, stage) + if not prims: raise RuntimeError(f"FrameView '{path_expr}' could not resolve a source prim.") - specs.append( + specs.extend( self._resolve_source_prim(prim, validate_xform_ops, None, None, None, use_clone_body_pattern, stage) + for prim in prims ) return specs diff --git a/source/isaaclab_newton/test/assets/test_articulation.py b/source/isaaclab_newton/test/assets/test_articulation.py index dc705e3fbd5a..f03ff42fb5b6 100644 --- a/source/isaaclab_newton/test/assets/test_articulation.py +++ b/source/isaaclab_newton/test/assets/test_articulation.py @@ -415,7 +415,7 @@ def generate_articulation( # Create Top-level Xforms, one for each articulation for i in range(num_articulations): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) - articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_.*/Robot")) + articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_[^/]*/Robot")) # Fix reversed joints for known-broken USD assets (body0/body1 swapped) usd_path = getattr(articulation_cfg.spawn, "usd_path", "") @@ -455,7 +455,7 @@ def _setup_franka_at_home_pose(sim, *, zero_actuator_pd: bool = False, disable_g Returns: Tuple of ``(robot, ee_frame_idx, ee_jacobi_idx, arm_joint_ids)``. """ - cfg = FRANKA_PANDA_HIGH_PD_CFG.copy().replace(prim_path="/World/Env_.*/Robot") + cfg = FRANKA_PANDA_HIGH_PD_CFG.copy().replace(prim_path="/World/Env_[^/]*/Robot") if zero_actuator_pd: cfg.actuators["panda_shoulder"].stiffness = 0.0 cfg.actuators["panda_shoulder"].damping = 0.0 @@ -682,7 +682,7 @@ def test_actuator_cfg_sets_newton_target_mode_before_solver_init( ): """Resolve configured modes before finalization constructs MuJoCo actuators.""" articulation_cfg = ArticulationCfg( - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", actuators={"joint": actuator_cfg}, ) articulation = Articulation(articulation_cfg) @@ -704,7 +704,7 @@ def test_actuator_cfg_matches_explicit_descendant_articulation_root(sim, device, """Match target modes against an explicitly configured descendant articulation root.""" articulation = Articulation( ArticulationCfg( - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", articulation_root_prim_path="/base", actuators={"joint": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=10.0, damping=0.0)}, ) @@ -721,7 +721,7 @@ def test_actuator_cfg_matches_clone_plan_root_glob(sim, device, articulation_typ """Match builder labels when clone-plan root resolution returns a glob.""" articulation = Articulation( ArticulationCfg( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", actuators={"joint": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=10.0, damping=0.0)}, ) ) @@ -742,7 +742,7 @@ def test_actuator_cfg_leaves_excluded_joint_types_imported(sim, device, articula """Leave target modes for free and fixed joints unchanged.""" articulation = Articulation( ArticulationCfg( - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", actuators={"joint": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=10.0, damping=0.0)}, ) ) @@ -757,7 +757,7 @@ def test_actuator_cfg_leaves_excluded_joint_types_imported(sim, device, articula def test_actuator_cfg_keeps_imported_newton_target_mode_for_none_gain(sim, device, articulation_type): """Retain the imported stiffness when an implicit actuator config leaves it unset.""" articulation_cfg = ArticulationCfg( - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", actuators={"joint": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=None, damping=0.0)}, ) articulation = Articulation(articulation_cfg) @@ -771,7 +771,7 @@ def test_actuator_cfg_keeps_imported_newton_target_mode_for_none_gain(sim, devic def test_actuator_cfg_leaves_unconfigured_newton_target_modes_imported(sim, device, articulation_type): """Leave target modes for DOFs outside an actuator group unchanged.""" subset_cfg = ArticulationCfg( - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", actuators={ "shoulder": ImplicitActuatorCfg(joint_names_expr=["left_shoulder"], stiffness=10.0, damping=0.0), }, @@ -802,7 +802,7 @@ def test_actuator_cfg_aligns_partial_dictionary_gains_by_joint_name( """Resolve sparse stiffness and damping dictionaries independently by joint name.""" articulation = Articulation( ArticulationCfg( - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", actuators={"joint": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=stiffness, damping=damping)}, ) ) @@ -4053,8 +4053,8 @@ def test_heterogeneous_scene_per_view_shapes(sim, device, add_ground_plane, arti # per-articulation shape gate without that pre-existing quirk. num_per_type = 1 - franka_cfg = FRANKA_PANDA_CFG.replace(prim_path="/World/Env_franka_.*/Robot") - anymal_cfg = ANYMAL_C_CFG.replace(prim_path="/World/Env_anymal_.*/Robot") + franka_cfg = FRANKA_PANDA_CFG.replace(prim_path="/World/Env_franka_[^/]*/Robot") + anymal_cfg = ANYMAL_C_CFG.replace(prim_path="/World/Env_anymal_[^/]*/Robot") for i in range(num_per_type): sim_utils.create_prim(f"/World/Env_franka_{i}", "Xform", translation=(2.5 * i, 0.0, 0.0)) diff --git a/source/isaaclab_newton/test/assets/test_mpm_object.py b/source/isaaclab_newton/test/assets/test_mpm_object.py index d2fd6d6dd82f..fd6e423a67e5 100644 --- a/source/isaaclab_newton/test/assets/test_mpm_object.py +++ b/source/isaaclab_newton/test/assets/test_mpm_object.py @@ -37,7 +37,7 @@ def test_mpm_particle_material_emits_custom_attributes(): def test_mpm_object_cfg_resolves_asset_class(): cfg = MPMObjectCfg( - prim_path="/World/envs/env_.*/Sand", + prim_path="{ENV_REGEX_NS}/Sand", spawn=MPMGridCfg(lower=(0.0, 0.0, 0.0), upper=(0.1, 0.1, 0.1), voxel_size=0.1), ) @@ -49,7 +49,7 @@ def test_mpm_grid_emission_records_constant_offsets_per_env(): NewtonMPMManager._register_builder_attributes(builder) cfg = MPMObjectCfg( - prim_path="/World/envs/env_.*/Sand", + prim_path="{ENV_REGEX_NS}/Sand", spawn=MPMGridCfg( lower=(0.0, 0.0, 0.0), upper=(0.1, 0.1, 0.1), @@ -74,7 +74,7 @@ def test_mpm_points_emission_records_constant_offsets_per_env(): NewtonMPMManager._register_builder_attributes(builder) cfg = MPMObjectCfg( - prim_path="/World/envs/env_.*/Fluid", + prim_path="{ENV_REGEX_NS}/Fluid", spawn=MPMPointsCfg( positions=((0.0, 0.0, 0.0), (0.0, 0.0, 0.1), (0.0, 0.1, 0.0)), velocities=((0.0, 0.0, 0.0), (0.0, 0.0, 0.1), (0.0, 0.1, 0.0)), diff --git a/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py b/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py index 78d71bf3106e..ad3da92a9081 100644 --- a/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py +++ b/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py @@ -162,7 +162,7 @@ def _run_simulation( sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) art_cfg = ANYMAL_C_CFG.replace( actuators=actuators, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", joint_ordering=joint_ordering, ) articulation = Articulation(art_cfg) @@ -487,10 +487,10 @@ def _run_anymal_and_cartpole(use_newton_actuators: bool, *, num_steps: int = NUM for i in range(NUM_ENVS): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 6.0, 0, 0)) - anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_.*/Anymal") + anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_[^/]*/Anymal") cartpole_cfg = CARTPOLE_CFG.replace( actuators=CARTPOLE_EXPLICIT_ACTUATORS, - prim_path="/World/Env_.*/Cartpole", + prim_path="/World/Env_[^/]*/Cartpole", ) # Stand the cartpole well clear of the anymal. cartpole_cfg.init_state = cartpole_cfg.init_state.replace(pos=(0.0, 3.0, 2.0)) @@ -665,7 +665,7 @@ def test_single_articulation(self): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) art_cfg = ANYMAL_C_CFG.replace( actuators=IDEAL_PD_ACTUATORS, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", ) anymal = Articulation(art_cfg) sim.reset() @@ -713,10 +713,10 @@ def test_two_articulations(self): for i in range(NUM_ENVS): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 6.0, 0, 0)) - anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_.*/Anymal") + anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_[^/]*/Anymal") cartpole_cfg = CARTPOLE_CFG.replace( actuators=CARTPOLE_EXPLICIT_ACTUATORS, - prim_path="/World/Env_.*/Cartpole", + prim_path="/World/Env_[^/]*/Cartpole", ) cartpole_cfg.init_state = cartpole_cfg.init_state.replace(pos=(0.0, 3.0, 2.0)) anymal = Articulation(anymal_cfg) @@ -795,7 +795,7 @@ def test_snapshot_matches_config_for_all_envs(self): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) art_cfg = ANYMAL_C_CFG.replace( actuators=IDEAL_PD_ACTUATORS, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", ) anymal = Articulation(art_cfg) sim.reset() @@ -948,7 +948,7 @@ def _build_and_warm(self, *, use_newton_actuators: bool): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) art_cfg = ANYMAL_C_CFG.replace( actuators=DELAYED_PD_ACTUATORS, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", ) articulation = Articulation(art_cfg) sim.reset() @@ -1183,7 +1183,7 @@ def _run_authoring_introspection(actuator_cfgs: dict) -> dict: art_cfg = ANYMAL_C_CFG.replace( actuators=actuator_cfgs, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", ) articulation = Articulation(art_cfg) sim.reset() diff --git a/source/isaaclab_newton/test/assets/test_rigid_object.py b/source/isaaclab_newton/test/assets/test_rigid_object.py index f3e20889bc94..fc5497e32ce4 100644 --- a/source/isaaclab_newton/test/assets/test_rigid_object.py +++ b/source/isaaclab_newton/test/assets/test_rigid_object.py @@ -111,7 +111,7 @@ def generate_cubes_scene( # Create rigid object cube_object_cfg = RigidObjectCfg( - prim_path="/World/Env_.*/Object", + prim_path="/World/Env_[^/]*/Object", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height)), ) @@ -849,7 +849,7 @@ def test_rigid_body_set_mass(num_cubes, device): sim_utils.create_prim(f"/World/Env_{index}", "Xform", translation=(float(index), 0.0, 1.0)) cube_object = RigidObject( RigidObjectCfg( - prim_path="/World/Env_.*/Object", + prim_path="/World/Env_[^/]*/Object", spawn=sim_utils.CuboidCfg( size=(0.2, 0.2, 0.2), rigid_props=sim_utils.RigidBodyPropertiesCfg(disable_gravity=True), diff --git a/source/isaaclab_newton/test/assets/test_rigid_object_collection.py b/source/isaaclab_newton/test/assets/test_rigid_object_collection.py index 67869729b2a9..be2c2f30c95e 100644 --- a/source/isaaclab_newton/test/assets/test_rigid_object_collection.py +++ b/source/isaaclab_newton/test/assets/test_rigid_object_collection.py @@ -108,14 +108,14 @@ def generate_cubes_scene( cube_config_dict = {} for i in range(num_cubes): cube_object_cfg = RigidObjectCfg( - prim_path=f"/World/Env_.*/Object_{i}", + prim_path=f"/World/Env_[^/]*/Object_{i}", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 3 * i, height)), ) cube_config_dict[f"cube_{i}"] = cube_object_cfg if spawn_unrelated_sibling: spawn_cfg.func( - "/World/Env_.*/UnrelatedObject", + "/World/Env_[^/]*/UnrelatedObject", spawn_cfg, translation=(0.0, -3.0, height), ) @@ -197,7 +197,7 @@ def test_set_body_inertial_properties_updates_inverses(device): RigidObjectCollectionCfg( rigid_objects={ f"cube_{body_index}": RigidObjectCfg( - prim_path=f"/World/Env_.*/Object_{body_index}", + prim_path=f"/World/Env_[^/]*/Object_{body_index}", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, float(body_index), 0.0)), ) diff --git a/source/isaaclab_newton/test/physics/test_newton_solver_reset.py b/source/isaaclab_newton/test/physics/test_newton_solver_reset.py index 2546c57cdf37..01b221ad6ace 100644 --- a/source/isaaclab_newton/test/physics/test_newton_solver_reset.py +++ b/source/isaaclab_newton/test/physics/test_newton_solver_reset.py @@ -32,7 +32,7 @@ def _generate_single_joint_articulations(num_articulations: int, device: str) -> for i in range(num_articulations): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 2.5, 0.0, 0.0)) articulation_cfg = ArticulationCfg( - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", spawn=sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/IsaacSim/SimpleArticulation/revolute_articulation.usd", joint_drive_props=sim_utils.JointDrivePropertiesCfg(max_force=80.0, max_joint_velocity=5.0), diff --git a/source/isaaclab_newton/test/sensors/test_contact_sensor.py b/source/isaaclab_newton/test/sensors/test_contact_sensor.py index 11794569cda6..e9ad48e17b6e 100644 --- a/source/isaaclab_newton/test/sensors/test_contact_sensor.py +++ b/source/isaaclab_newton/test/sensors/test_contact_sensor.py @@ -977,7 +977,7 @@ def test_sensor_metadata(device: str): sim._app_control_on_stop_handle = None scene_cfg = _make_two_box_scene_cfg(num_envs) scene_cfg.contact_sensor_a = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Box.*", + prim_path="{ENV_REGEX_NS}/Box[^/]*", update_period=0.0, history_length=1, ) @@ -1019,8 +1019,8 @@ def test_sensor_metadata(device: str): sim._app_control_on_stop_handle = None scene_cfg = _make_two_box_scene_cfg(num_envs) scene_cfg.contact_sensor_a = NewtonContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Box.*", - sensor_shape_prim_expr=["{ENV_REGEX_NS}/Box.*"], + prim_path="{ENV_REGEX_NS}/Box[^/]*", + sensor_shape_prim_expr=["{ENV_REGEX_NS}/Box[^/]*"], update_period=0.0, history_length=1, ) diff --git a/source/isaaclab_newton/test/sensors/test_frame_transformer.py b/source/isaaclab_newton/test/sensors/test_frame_transformer.py index ff019a62a053..f75005fb0767 100644 --- a/source/isaaclab_newton/test/sensors/test_frame_transformer.py +++ b/source/isaaclab_newton/test/sensors/test_frame_transformer.py @@ -535,7 +535,7 @@ def test_frame_transformer_all_bodies(sim): prim_path="{ENV_REGEX_NS}/Robot/base", target_frames=[ FrameTransformerCfg.FrameCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", ), ], ) @@ -627,7 +627,7 @@ def test_sensor_print(sim): prim_path="{ENV_REGEX_NS}/Robot/base", target_frames=[ FrameTransformerCfg.FrameCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", ), ], ) diff --git a/source/isaaclab_newton/test/sim/test_mpm_spawners.py b/source/isaaclab_newton/test/sim/test_mpm_spawners.py index 2ddc9c7f6bbe..a9a244652a3e 100644 --- a/source/isaaclab_newton/test/sim/test_mpm_spawners.py +++ b/source/isaaclab_newton/test/sim/test_mpm_spawners.py @@ -149,7 +149,7 @@ def test_mpm_config_imports_do_not_load_pxr(): from isaaclab_newton.sim.spawners.mpm import MPMGridCfg, MPMParticleMaterialCfg, MPMPointsCfg MPMObjectCfg( - prim_path="/World/envs/env_.*/Sand", + prim_path="{ENV_REGEX_NS}/Sand", spawn=MPMGridCfg( lower=(0.0, 0.0, 0.0), upper=(0.1, 0.1, 0.1), diff --git a/source/isaaclab_newton/test/sim/test_views_xform_prim_newton.py b/source/isaaclab_newton/test/sim/test_views_xform_prim_newton.py index 8ac61278d52f..83cd56d2ac14 100644 --- a/source/isaaclab_newton/test/sim/test_views_xform_prim_newton.py +++ b/source/isaaclab_newton/test/sim/test_views_xform_prim_newton.py @@ -86,7 +86,7 @@ def factory(num_envs: int, device: str) -> ViewBundle: sim._app_control_on_stop_handle = None InteractiveScene(_SceneCfg(num_envs=num_envs, env_spacing=2.0)) sim_utils.create_prim("/World/envs/env_0/Cube/CameraMount", translation=CHILD_OFFSET) - view = FrameView("/World/envs/env_.*/Cube/CameraMount", device=device) + view = FrameView("/World/envs/env_[^/]+/Cube/CameraMount", device=device) sim.reset() return ViewBundle( @@ -114,7 +114,7 @@ def test_reject_body_path(device): sim.reset() with pytest.raises(ValueError, match="physics body"): - FrameView("/World/envs/env_.*/Cube", device=device) + FrameView("/World/envs/env_[^/]+/Cube", device=device) ctx.__exit__(None, None, None) @@ -150,7 +150,7 @@ def test_clone_plan_view_uses_source_child_without_destination_usd(device): assert not stage.GetPrimAtPath("/World/envs/env_1/Cube").IsValid() sim_utils.create_prim("/World/envs/env_0/Cube/CameraMount", translation=CHILD_OFFSET) - view = FrameView("/World/envs/env_.*/Cube/CameraMount", device=device) + view = FrameView("/World/envs/env_[^/]+/Cube/CameraMount", device=device) sim.reset() assert view.count == num_envs @@ -172,7 +172,7 @@ def test_view_can_resolve_from_body_labels_after_reset(device): sim_utils.create_prim("/World/envs/env_0/Cube/CameraMount", translation=CHILD_OFFSET) sim.reset() - view = FrameView("/World/envs/env_.*/Cube/CameraMount", device=device) + view = FrameView("/World/envs/env_[^/]+/Cube/CameraMount", device=device) pos = view.get_world_poses()[0].torch expected = _get_body_positions(num_envs, device) + torch.tensor(CHILD_OFFSET, device=device) diff --git a/source/isaaclab_ov/changelog.d/octi-prim-path-real-regex-matcher.rst b/source/isaaclab_ov/changelog.d/octi-prim-path-real-regex-matcher.rst new file mode 100644 index 000000000000..09484865c765 --- /dev/null +++ b/source/isaaclab_ov/changelog.d/octi-prim-path-real-regex-matcher.rst @@ -0,0 +1,15 @@ +Changed +^^^^^^^ + +* Changed prim path expressions to spell a single path segment ``[^/]`` rather than ``.``, so each + pattern selects what it selected before now that ``.`` matches ``/`` in + :func:`~isaaclab.sim.utils.find_matching_prims`. + +Fixed +^^^^^ + +* Fixed the OVRTX deformable render bindings leaving the environment slot unresolved, so they + bound against a path expression instead of the concrete per-environment mesh prims. +* Fixed physics views receiving a regular expression where the engine expects a glob. The + conversion rewrote only ``.*`` and left a segment-safe wildcard untouched, so the view matched + no bodies; it now goes through :func:`~isaaclab.sim.utils.path_expr_to_glob`. diff --git a/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation.py b/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation.py index f2514e016179..b5290807e6ba 100644 --- a/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation.py +++ b/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation.py @@ -4114,7 +4114,7 @@ def has_articulation_root_api(prim) -> bool: # IsaacLab paths may use ``.*`` regex or ``{ENV_REGEX_NS}`` placeholder; ovphysx # ``create_tensor_binding`` expects fnmatch globs. pattern = re.sub(r"\{ENV_REGEX_NS\}", "*", root_prim_path_expr) - pattern = re.sub(r"\.\*", "*", pattern) + pattern = sim_utils.path_expr_to_glob(pattern) self._binding_pattern = pattern # eagerly create every binding the data container reads at init, so diff --git a/source/isaaclab_ov/isaaclab_ov/assets/deformable_object/deformable_object.py b/source/isaaclab_ov/isaaclab_ov/assets/deformable_object/deformable_object.py index ce9f2f6d3dc8..a46b2dfc63b9 100644 --- a/source/isaaclab_ov/isaaclab_ov/assets/deformable_object/deformable_object.py +++ b/source/isaaclab_ov/isaaclab_ov/assets/deformable_object/deformable_object.py @@ -414,7 +414,7 @@ def has_deformable_body_api(prim) -> bool: ) root_pattern = re.sub(r"\{ENV_REGEX_NS\}", "*", root_path_expr) - root_pattern = re.sub(r"\.\*", "*", root_pattern) + root_pattern = sim_utils.path_expr_to_glob(root_pattern) try: self._root_physx_view = OvPhysxDeformableBodyView( physx_instance, @@ -440,7 +440,7 @@ def has_deformable_body_api(prim) -> bool: else material_path ) material_pattern = re.sub(r"\{ENV_REGEX_NS\}", "*", material_path_expr) - material_pattern = re.sub(r"\.\*", "*", material_pattern) + material_pattern = sim_utils.path_expr_to_glob(material_pattern) material_tensor_types = [ TT.DEFORMABLE_MATERIAL_DYNAMIC_FRICTION, TT.DEFORMABLE_MATERIAL_YOUNGS_MODULUS, diff --git a/source/isaaclab_ov/isaaclab_ov/assets/rigid_object/rigid_object.py b/source/isaaclab_ov/isaaclab_ov/assets/rigid_object/rigid_object.py index 36b472be4423..01ea2c439f6b 100644 --- a/source/isaaclab_ov/isaaclab_ov/assets/rigid_object/rigid_object.py +++ b/source/isaaclab_ov/isaaclab_ov/assets/rigid_object/rigid_object.py @@ -19,7 +19,7 @@ from isaaclab.assets.rigid_object.base_rigid_object import BaseRigidObject from isaaclab.assets.rigid_object.rigid_object_cfg import RigidObjectCfg -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.string import resolve_matching_names from isaaclab.utils.warp import ProxyArray from isaaclab.utils.wrench_composer import WrenchComposer @@ -950,7 +950,7 @@ def has_rigid_body_api(prim) -> bool: # IsaacLab paths may use ``.*`` regex or ``{ENV_REGEX_NS}`` placeholder; ovphysx # ``create_tensor_binding`` expects fnmatch globs. pattern = re.sub(r"\{ENV_REGEX_NS\}", "*", root_prim_path_expr) - pattern = re.sub(r"\.\*", "*", pattern) + pattern = path_expr_to_glob(pattern) self._binding_pattern = pattern # Eagerly create every binding the data container reads at init, so failures diff --git a/source/isaaclab_ov/isaaclab_ov/assets/rigid_object_collection/rigid_object_collection.py b/source/isaaclab_ov/isaaclab_ov/assets/rigid_object_collection/rigid_object_collection.py index 47144a77d22a..bd273d8360ed 100644 --- a/source/isaaclab_ov/isaaclab_ov/assets/rigid_object_collection/rigid_object_collection.py +++ b/source/isaaclab_ov/isaaclab_ov/assets/rigid_object_collection/rigid_object_collection.py @@ -1105,7 +1105,7 @@ def has_rigid_body_api(prim) -> bool: # IsaacLab paths may use ``.*`` regex or ``{ENV_REGEX_NS}`` placeholder; ovphysx # ``create_tensor_binding`` expects fnmatch globs. pattern = re.sub(r"\{ENV_REGEX_NS\}", "*", root_prim_path_expr) - pattern = re.sub(r"\.\*", "*", pattern) + pattern = sim_utils.path_expr_to_glob(pattern) self._prim_paths.append(pattern) self._body_names_list.append(name) diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py index a1da0a05c4a7..46f6a1e90f35 100644 --- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py @@ -754,7 +754,9 @@ def _setup_deformable_bindings_legacy(self, num_envs: int): self._deformable_particle_offsets.append(particle_offset) self._deformable_particle_counts.append(entry.particles_per_body) - vis_mesh_prim_paths.append(re.sub(r"(?<=[Ee]nv_)\.\*", str(idx), entry.vis_mesh_prim_path)) + vis_mesh_prim_paths.append( + re.sub(r"(?<=[Ee]nv_)(?:\[\^/\][*+]|\.\*)", str(idx), entry.vis_mesh_prim_path) + ) prim_count = len(vis_mesh_prim_paths) if prim_count == 0: @@ -1903,7 +1905,9 @@ def _setup_deformable_bindings_ovstage(self, num_envs: int) -> None: self._deformable_particle_offsets.append(particle_offset) self._deformable_particle_counts.append(entry.particles_per_body) - vis_mesh_prim_paths.append(re.sub(r"(?<=[Ee]nv_)\.\*", str(idx), entry.vis_mesh_prim_path)) + vis_mesh_prim_paths.append( + re.sub(r"(?<=[Ee]nv_)(?:\[\^/\][*+]|\.\*)", str(idx), entry.vis_mesh_prim_path) + ) prim_count = len(vis_mesh_prim_paths) if prim_count == 0: diff --git a/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py b/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py index 1f1b124c9a1f..757fe06a1681 100644 --- a/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py +++ b/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py @@ -17,7 +17,7 @@ import warp as wp from isaaclab.sensors.contact_sensor import BaseContactSensor -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source, split_path_expr from isaaclab.utils.warp import ProxyArray import isaaclab_ov.tensor_types as TT @@ -194,7 +194,8 @@ def _initialize_impl(self) -> None: # library is loaded by ``omni.physx``. The unfiltered API matches what # the underlying USD apiSchemas listOp actually carries (verified against # :class:`pxr.Sdf.PrimSpec.GetInfo("apiSchemas")`). - parent_expr, leaf_pattern = self.cfg.prim_path.rsplit("/", 1) + *parent_segments, leaf_pattern = split_path_expr(self.cfg.prim_path) + parent_expr = "/".join(parent_segments) name_pattern = re.compile(leaf_pattern) def has_contact_report(prim) -> bool: @@ -218,11 +219,11 @@ def has_contact_report(prim) -> bool: # hierarchies (child links authored under their parent link prim), where the bodies do # not share a parent. IsaacLab path forms map to ovphysx fnmatch globs the same way # Articulation does. - sensor_patterns = [re.sub(r"\.\*", "*", re.sub(r"\{ENV_REGEX_NS\}", "*", expr)) for _, expr in body_matches] + sensor_patterns = [path_expr_to_glob(re.sub(r"\{ENV_REGEX_NS\}", "*", expr)) for _, expr in body_matches] # Build filter patterns (flat: len = n_sensors * filters_per_sensor). filter_globs = [ - re.sub(r"\.\*", "*", re.sub(r"\{ENV_REGEX_NS\}", "*", expr)) for expr in self.cfg.filter_prim_paths_expr + path_expr_to_glob(re.sub(r"\{ENV_REGEX_NS\}", "*", expr)) for expr in self.cfg.filter_prim_paths_expr ] filters_per_sensor = len(filter_globs) if filters_per_sensor > 0: diff --git a/source/isaaclab_ov/isaaclab_ov/sensors/frame_transformer/frame_transformer.py b/source/isaaclab_ov/isaaclab_ov/sensors/frame_transformer/frame_transformer.py index 869965cbfe6e..33ac123d9008 100644 --- a/source/isaaclab_ov/isaaclab_ov/sensors/frame_transformer/frame_transformer.py +++ b/source/isaaclab_ov/isaaclab_ov/sensors/frame_transformer/frame_transformer.py @@ -18,7 +18,7 @@ from isaaclab.markers import VisualizationMarkers from isaaclab.sensors.frame_transformer import BaseFrameTransformer -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.math import is_identity_pose, normalize, quat_from_angle_axis import isaaclab_ov.tensor_types as TT @@ -611,7 +611,7 @@ def _env_wildcardify(prim_path: str) -> str: Args: prim_path: An env-0 prim path (e.g. ``"/World/envs/env_0/Robot/LF_FOOT"``) or an - IsaacLab regex form (e.g. ``"/World/envs/env_.*/Robot/LF_FOOT"`` or + IsaacLab regex form (e.g. ``"{ENV_REGEX_NS}/Robot/LF_FOOT"`` or ``"{ENV_REGEX_NS}/Robot/LF_FOOT"``). Returns: @@ -622,7 +622,7 @@ def _env_wildcardify(prim_path: str) -> str: substitutions. """ pattern = re.sub(r"\{ENV_REGEX_NS\}", "*", prim_path) - pattern = re.sub(r"\.\*", "*", pattern) + pattern = path_expr_to_glob(pattern) pattern = re.sub(r"/envs/env_\d+(/|$)", r"/envs/env_*\1", pattern) return pattern @@ -636,5 +636,7 @@ def _get_relative_body_path(prim_path: str) -> str: Returns: The prim path with that segment collapsed to ``/envs/``, so prim paths from any env compare equal. """ - pattern = re.compile(r"/envs/env_[^/]+/") + # the env slot may be a concrete id or a segment wildcard; try the wildcard spellings + # first so a bare ``[^/]+`` alternative cannot consume half of a character class. + pattern = re.compile(r"/envs/env_(?:\[\^/\][*+]|\.\*|[^/]+)/") return pattern.sub("/envs/", prim_path) diff --git a/source/isaaclab_ov/isaaclab_ov/sensors/imu/imu.py b/source/isaaclab_ov/isaaclab_ov/sensors/imu/imu.py index 789515251c30..040b2c6e3b3c 100644 --- a/source/isaaclab_ov/isaaclab_ov/sensors/imu/imu.py +++ b/source/isaaclab_ov/isaaclab_ov/sensors/imu/imu.py @@ -13,6 +13,7 @@ import isaaclab.utils.math as math_utils from isaaclab.sensors.imu import BaseImu +from isaaclab.sim.utils.queries import path_expr_to_glob import isaaclab_ov.tensor_types as TT from isaaclab_ov.physics import OvPhysxManager as SimulationManager @@ -133,7 +134,7 @@ def _initialize_impl(self): self._rigid_parent_expr, fixed_pos_b, fixed_quat_b = self._resolve_rigid_body_ancestor_expr() # Translate the regex-style path expression to an ovphysx fnmatch glob. - pattern = self._rigid_parent_expr.replace(".*", "*") + pattern = path_expr_to_glob(self._rigid_parent_expr) self._root_view = OvPhysxView(physx_instance, pattern=pattern, device=self._device) self._pose_binding = self._root_view.binding_for(TT.RIGID_BODY_POSE) diff --git a/source/isaaclab_ov/isaaclab_ov/sensors/joint_wrench/joint_wrench_sensor.py b/source/isaaclab_ov/isaaclab_ov/sensors/joint_wrench/joint_wrench_sensor.py index f14319e8dbd3..d1d5825cf483 100644 --- a/source/isaaclab_ov/isaaclab_ov/sensors/joint_wrench/joint_wrench_sensor.py +++ b/source/isaaclab_ov/isaaclab_ov/sensors/joint_wrench/joint_wrench_sensor.py @@ -18,7 +18,7 @@ from pxr import Usd, UsdPhysics from isaaclab.sensors.joint_wrench import BaseJointWrenchSensor -from isaaclab.sim.utils.queries import find_first_matching_prim, get_all_matching_child_prims +from isaaclab.sim.utils.queries import find_first_matching_prim, get_all_matching_child_prims, path_expr_to_glob import isaaclab_ov.tensor_types as TT from isaaclab_ov.physics import OvPhysxManager @@ -134,7 +134,7 @@ def _initialize_impl(self) -> None: # Resolve the articulation root and translate to an fnmatch glob. root_prim_path_expr = self._resolve_articulation_root_prim_path() pattern = re.sub(r"\{ENV_REGEX_NS\}", "*", root_prim_path_expr) - pattern = re.sub(r"\.\*", "*", pattern) + pattern = path_expr_to_glob(pattern) self._root_view = OvPhysxView(physx_instance, pattern=pattern, device=self._device) self._wrench_binding = self._root_view.binding_for(TT.LINK_INCOMING_JOINT_FORCE) diff --git a/source/isaaclab_ov/isaaclab_ov/sensors/pva/pva.py b/source/isaaclab_ov/isaaclab_ov/sensors/pva/pva.py index 8610f7d44b91..81e5aec99455 100644 --- a/source/isaaclab_ov/isaaclab_ov/sensors/pva/pva.py +++ b/source/isaaclab_ov/isaaclab_ov/sensors/pva/pva.py @@ -16,6 +16,7 @@ import isaaclab.utils.math as math_utils from isaaclab.markers import VisualizationMarkers from isaaclab.sensors.pva import BasePva +from isaaclab.sim.utils.queries import path_expr_to_glob import isaaclab_ov.tensor_types as TT from isaaclab_ov.physics import OvPhysxManager as SimulationManager @@ -142,7 +143,7 @@ def _initialize_impl(self): self._rigid_parent_expr, fixed_pos_b, fixed_quat_b = self._resolve_rigid_body_ancestor_expr() # Translate the regex-style path expression to an ovphysx fnmatch glob. - pattern = self._rigid_parent_expr.replace(".*", "*") + pattern = path_expr_to_glob(self._rigid_parent_expr) self._root_view = OvPhysxView(physx_instance, pattern=pattern, device=self._device) self._num_bodies = self._root_view.binding_for(TT.RIGID_BODY_POSE).count diff --git a/source/isaaclab_ov/isaaclab_ov/sensors/ray_caster/ray_caster.py b/source/isaaclab_ov/isaaclab_ov/sensors/ray_caster/ray_caster.py index d0a1f2e80db0..f6aa8a8223dd 100644 --- a/source/isaaclab_ov/isaaclab_ov/sensors/ray_caster/ray_caster.py +++ b/source/isaaclab_ov/isaaclab_ov/sensors/ray_caster/ray_caster.py @@ -47,7 +47,7 @@ def _ovphysx_body_glob(body_expr: str) -> str: fnmatch glob, so ``{}`` template placeholders and ``.*`` regex segments both map to ``*``. """ - return body_expr.replace("{}", "*").replace(".*", "*") + return sim_utils.path_expr_to_glob(body_expr.replace("{}", "*")) class _OvPhysxRayCasterMixin: @@ -189,30 +189,10 @@ def _create_tracked_target_view(self: Any, target_prim_paths: str | list[str]): if isinstance(target_prim_paths, str): target_prim_paths = [target_prim_paths] - - body_paths: list[str] = [] - for target_prim_path in target_prim_paths: - prims = sim_utils.find_matching_prims(target_prim_path) - if len(prims) == 0: - # ClonePlan-backed targets may not have USD destination prims; - # in that case BaseMultiMeshRayCaster forwards the - # destination owner-body expression directly. - body_paths.append(target_prim_path) - continue - for prim in prims: - body = _find_physics_ancestor(prim) - if body is None: - raise RuntimeError( - f"Cannot track non-physics ray-cast target {target_prim_path!r} " - "with OVPhysX. Set track_mesh_transforms=False for static targets, " - "or apply RigidBodyAPI to dynamic targets." - ) - body_paths.append(body.GetPath().pathString) - - if len(body_paths) == 0: + if not target_prim_paths: raise RuntimeError(f"No tracked target bodies resolved from: {target_prim_paths}") - patterns = sorted({_ovphysx_body_glob(path) for path in body_paths}) + patterns = sorted({_ovphysx_body_glob(path) for path in target_prim_paths}) if len(patterns) > 1: raise NotImplementedError( f"OvPhysxRayCaster v1 supports a single body-type pattern for dynamic targets; " diff --git a/source/isaaclab_ov/isaaclab_ov/sim/views/ovphysx_frame_view.py b/source/isaaclab_ov/isaaclab_ov/sim/views/ovphysx_frame_view.py index 52c453fab1db..6e40955125b8 100644 --- a/source/isaaclab_ov/isaaclab_ov/sim/views/ovphysx_frame_view.py +++ b/source/isaaclab_ov/isaaclab_ov/sim/views/ovphysx_frame_view.py @@ -313,7 +313,22 @@ def __init__(self, prim_path: str, device: str = "cpu", stage: Usd.Stage | None stage = sim_utils.get_current_stage() if stage is None else stage self._stage = stage - self._prims: list[Usd.Prim] = sim_utils.find_matching_prims(prim_path, stage=stage) + sim = sim_utils.SimulationContext.instance() + plan = sim.get_clone_plan() if sim is not None else None + source_matches = tuple(cloner.query.iter_sources(plan, prim_path)) if plan is not None else () + self._source_records = [] + self._prims: list[Usd.Prim] = [] + for source_root, destination_template, source_path, env_ids in source_matches: + source_pattern = re.compile(source_path) + source_prims = sim_utils.get_all_matching_child_prims( + source_root, + lambda prim: source_pattern.fullmatch(prim.GetPath().pathString) is not None, + stage=stage, + ) + self._prims.extend(source_prims) + self._source_records.extend((source_root, destination_template, prim, env_ids) for prim in source_prims) + if not source_matches: + self._prims = sim_utils.find_matching_prims(prim_path, stage=stage) if not self._prims: raise ValueError(f"OvPhysxFrameView: pattern {prim_path!r} matched zero prims.") @@ -351,17 +366,9 @@ def _on_physics_ready(self, _event) -> None: def _initialize_impl(self, physx: Any) -> None: """Resolve prims to rigid-body ancestors and create a RIGID_BODY_POSE tensor binding. - Site discovery handles two scene-construction modes: - - * **``clone_usd=True``** (Newton-style cloning): every env has its own - USD prims; ``find_matching_prims`` returns one prim per env, and the - binding row count matches. - * **``clone_usd=False``** (OVPhysX default): only ``env_0`` has authored - USD prims; ``env_1..N`` are physics-layer clones (no USD twin). The - RIGID_BODY_POSE binding still exposes one row per env. In that case - the binding is the source of truth for the site count, and per-env - site paths are synthesized from the env_0 template prim's path with - ``env_0`` replaced by the row's env_id. + With a ClonePlan, site discovery reads only its authored source prims, whether or not + destination USD prims exist. The RIGID_BODY_POSE binding is the source of truth for the + site count, and per-env site paths are synthesized from the source prim paths. """ from isaaclab_ov import tensor_types as TT # noqa: PLC0415 from isaaclab_ov.sim.views.ovphysx_view import OvPhysxView # noqa: PLC0415 @@ -432,8 +439,7 @@ def _initialize_impl(self, physx: Any) -> None: binding_paths = [] world_sites = self._expand_world_sites_from_clone_plan(xform_cache) if not binding_paths else [] - # 5. Detect clone_usd=False expansion: binding row count > number of matched USD prims. - # Replace per-prim arrays with one entry per binding row, all derived from the env_0 template. + # 5. Expand source prim data to one entry per binding row. if binding_paths and len(binding_paths) > len(self._prims): template_ancestor = per_prim_ancestor[0] template_site_local = per_prim_site_local[0] @@ -511,20 +517,11 @@ def _expand_world_sites_from_clone_plan( self, xform_cache: UsdGeom.XformCache ) -> list[tuple[int, Usd.Prim, list[float], list[float], str]]: """Return row-ordered source prims and projected poses for source-only world sites.""" - sim = sim_utils.SimulationContext.instance() - plan = sim.get_clone_plan() if sim is not None else None - matches = tuple(cloner.query.iter_sources(plan, self._prim_path)) if plan is not None else () - if sum(len(env_ids) for _, _, _, env_ids in matches) <= len(self._prims): + if sum(len(env_ids) for _, _, _, env_ids in self._source_records) <= len(self._prims): return [] records: list[tuple[int, Usd.Prim, list[float], list[float], str]] = [] - for source_root, destination_template, source_path, env_ids in matches: - source_prim = self._stage.GetPrimAtPath(source_path) - if not source_prim.IsValid(): - source_prim = sim_utils.find_first_matching_prim(source_path, self._stage) - if source_prim is None or not source_prim.IsValid(): - raise RuntimeError(f"OvPhysxFrameView could not resolve source prim {source_path!r}.") - + for source_root, destination_template, source_prim, env_ids in self._source_records: source_prim_path = source_prim.GetPath().pathString suffix = cloner.path.relative_to(source_prim_path, source_root) if suffix is None: diff --git a/source/isaaclab_ov/test/assets/test_articulation.py b/source/isaaclab_ov/test/assets/test_articulation.py index 0c05580084ef..bb9a29c6c6bf 100644 --- a/source/isaaclab_ov/test/assets/test_articulation.py +++ b/source/isaaclab_ov/test/assets/test_articulation.py @@ -390,7 +390,7 @@ def generate_articulation( # Create Top-level Xforms, one for each articulation for i in range(num_articulations): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) - articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_.*/Robot")) + articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_[^/]*/Robot")) return articulation, translations diff --git a/source/isaaclab_ov/test/assets/test_deformable_object.py b/source/isaaclab_ov/test/assets/test_deformable_object.py index 54208d477b38..0bc028a1ee37 100644 --- a/source/isaaclab_ov/test/assets/test_deformable_object.py +++ b/source/isaaclab_ov/test/assets/test_deformable_object.py @@ -115,7 +115,7 @@ def _generate_deformable_scene( for index in range(num_objects): sim_utils.create_prim(f"/World/Table_{index}", "Xform", translation=(index * 1.0, 0.0, height)) cfg = DeformableObjectCfg( - prim_path="/World/Table_.*/Object", + prim_path="/World/Table_[^/]*/Object", spawn=spawn, init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height), rot=initial_rot), ) diff --git a/source/isaaclab_ov/test/assets/test_rigid_object.py b/source/isaaclab_ov/test/assets/test_rigid_object.py index 8f6634ddb430..db6a5b75a0c3 100644 --- a/source/isaaclab_ov/test/assets/test_rigid_object.py +++ b/source/isaaclab_ov/test/assets/test_rigid_object.py @@ -157,7 +157,7 @@ def generate_cubes_scene( # Create rigid object. OVPhysX matches prim paths via fnmatch globs (not regex), # so use ``Table_*`` rather than the PhysX ``Table_.*`` form. cube_object_cfg = RigidObjectCfg( - prim_path="/World/Table_*/Object", + prim_path="/World/Table_[^/]+/Object", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height)), ) diff --git a/source/isaaclab_ov/test/assets/test_rigid_object_collection.py b/source/isaaclab_ov/test/assets/test_rigid_object_collection.py index 28982bf2d878..c258a8b2b61a 100644 --- a/source/isaaclab_ov/test/assets/test_rigid_object_collection.py +++ b/source/isaaclab_ov/test/assets/test_rigid_object_collection.py @@ -148,7 +148,7 @@ def generate_cubes_scene( cube_config_dict = {} for i in range(num_cubes): cube_object_cfg = RigidObjectCfg( - prim_path=f"/World/Table_*/Object_{i}", + prim_path=f"/World/Table_[^/]+/Object_{i}", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 3 * i, height)), ) diff --git a/source/isaaclab_ov/test/sensors/check_imu.py b/source/isaaclab_ov/test/sensors/check_imu.py index 0fa2264595a4..8fdcc37d4fc3 100644 --- a/source/isaaclab_ov/test/sensors/check_imu.py +++ b/source/isaaclab_ov/test/sensors/check_imu.py @@ -48,12 +48,12 @@ def main() -> None: balls = RigidObject( RigidObjectCfg( - prim_path="/World/env_*/ball", + prim_path="/World/env_[^/]+/ball", spawn=None, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 1.0)), ) ) - imu = Imu(ImuCfg(prim_path="/World/env_*/ball")) + imu = Imu(ImuCfg(prim_path="/World/env_[^/]+/ball")) sim.reset() dt = sim.get_physics_dt() diff --git a/source/isaaclab_ov/test/sensors/check_pva.py b/source/isaaclab_ov/test/sensors/check_pva.py index 2ec92d3d7428..0e751abf6807 100644 --- a/source/isaaclab_ov/test/sensors/check_pva.py +++ b/source/isaaclab_ov/test/sensors/check_pva.py @@ -49,12 +49,12 @@ def main() -> None: balls = RigidObject( RigidObjectCfg( - prim_path="/World/env_*/ball", + prim_path="/World/env_[^/]+/ball", spawn=None, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 1.0)), ) ) - pva = Pva(PvaCfg(prim_path="/World/env_*/ball")) + pva = Pva(PvaCfg(prim_path="/World/env_[^/]+/ball")) sim.reset() dt = sim.get_physics_dt() diff --git a/source/isaaclab_ov/test/sensors/test_contact_sensor.py b/source/isaaclab_ov/test/sensors/test_contact_sensor.py index 5d51d26d4eb1..39041b566257 100644 --- a/source/isaaclab_ov/test/sensors/test_contact_sensor.py +++ b/source/isaaclab_ov/test/sensors/test_contact_sensor.py @@ -488,7 +488,7 @@ def test_multi_body_per_sensor_indexing(device, num_envs): scene_cfg.shape_2.init_state.pos = (0.0, 1.5, 3.0) # Single ContactSensor that matches BOTH cubes via a regex glob. scene_cfg.contact_sensor = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Cube_.*", + prim_path="{ENV_REGEX_NS}/Cube_[^/]*", track_pose=False, debug_vis=False, update_period=0.0, @@ -593,7 +593,7 @@ def test_nested_rigid_body_hierarchy(device, num_envs): contact_sensor = ContactSensor( ContactSensorCfg( - prim_path="/World/envs/env_.*/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", track_pose=False, debug_vis=False, update_period=0.0, diff --git a/source/isaaclab_ov/test/sensors/test_frame_transformer.py b/source/isaaclab_ov/test/sensors/test_frame_transformer.py index f89948a77f7c..407a50fc38d9 100644 --- a/source/isaaclab_ov/test/sensors/test_frame_transformer.py +++ b/source/isaaclab_ov/test/sensors/test_frame_transformer.py @@ -645,7 +645,7 @@ def test_frame_transformer_all_bodies(device): prim_path="{ENV_REGEX_NS}/Robot/base", target_frames=[ FrameTransformerCfg.FrameCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", ), ], ) @@ -739,7 +739,7 @@ def test_sensor_print(device): prim_path="{ENV_REGEX_NS}/Robot/base", target_frames=[ FrameTransformerCfg.FrameCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", ), ], ) diff --git a/source/isaaclab_ov/test/sensors/test_imu.py b/source/isaaclab_ov/test/sensors/test_imu.py index d7e0fc4390b1..274916a3416b 100644 --- a/source/isaaclab_ov/test/sensors/test_imu.py +++ b/source/isaaclab_ov/test/sensors/test_imu.py @@ -104,8 +104,8 @@ def _spawn_balls(num_envs: int, height: float = 0.5) -> RigidObject: Returns the :class:`RigidObject` whose binding pattern matches all spawned instances. The :class:`RigidObject` does the per-env spawning itself when ``spawn`` is set; we only have to create the env Xform containers first - (handled by :func:`_spawn_envs`). Note the ovphysx pattern uses an - fnmatch glob (``env_*``), not a regex. + (handled by :func:`_spawn_envs`). The prim path is a regex; the ovphysx + binding pattern underneath it is an fnmatch glob. """ spawn_cfg = sim_utils.SphereCfg( radius=0.25, @@ -115,7 +115,7 @@ def _spawn_balls(num_envs: int, height: float = 0.5) -> RigidObject: visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), ) cfg = RigidObjectCfg( - prim_path="/World/env_*/ball", + prim_path="/World/env_[^/]+/ball", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height)), ) @@ -132,7 +132,7 @@ def _spawn_cubes(num_envs: int, height: float = 0.5) -> RigidObject: visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), ) cfg = RigidObjectCfg( - prim_path="/World/env_*/cube", + prim_path="/World/env_[^/]+/cube", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -2.0, height)), ) @@ -147,7 +147,7 @@ def _spawn_anymal(num_envs: int) -> Articulation: The :class:`Articulation` performs the per-env spawn itself once the env Xform containers exist; :func:`_spawn_envs` must be called first. """ - cfg = ANYMAL_C_CFG.replace(prim_path="/World/env_.*/robot") + cfg = ANYMAL_C_CFG.replace(prim_path="/World/env_[^/]+/robot") cfg.init_state.pos = (0.0, 2.0, 1.0) # bump solver iteration counts to match the PhysX test's scene cfg cfg.spawn.articulation_props.solver_position_iteration_count = 32 @@ -246,8 +246,8 @@ def test_constant_velocity(sim_ctx, device): _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) cubes = _spawn_cubes(NUM_ENVS) - imu_ball = _make_imu("/World/env_*/ball") - imu_cube = _make_imu("/World/env_*/cube") + imu_ball = _make_imu("/World/env_[^/]+/ball") + imu_cube = _make_imu("/World/env_[^/]+/cube") sim_ctx.reset() prev_lin_acc_ball = torch.zeros((NUM_ENVS, 3), dtype=torch.float32, device=device) @@ -297,7 +297,7 @@ def test_constant_acceleration(sim_ctx, device): """Test the IMU sensor with a constant acceleration.""" _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) - imu_ball = _make_imu("/World/env_*/ball") + imu_ball = _make_imu("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -353,9 +353,9 @@ def test_offset_calculation(sim_ctx, device): """ _spawn_envs(NUM_ENVS) robot = _spawn_anymal(NUM_ENVS) - imu_robot_imu_link = _make_imu("/World/env_*/robot/base/imu_link") + imu_robot_imu_link = _make_imu("/World/env_[^/]+/robot/base/imu_link") imu_robot_base = _make_imu( - "/World/env_*/robot/base", + "/World/env_[^/]+/robot/base", offset=ImuCfg.OffsetCfg(pos=POS_OFFSET, rot=ROT_OFFSET), ) sim_ctx.reset() @@ -397,7 +397,7 @@ def test_env_ids_propagation(sim_ctx, device): """Test that ``env_ids`` argument propagates through update and reset methods.""" _spawn_envs(NUM_ENVS) robot = _spawn_anymal(NUM_ENVS) - imu_robot_imu_link = _make_imu("/World/env_*/robot/base/imu_link") + imu_robot_imu_link = _make_imu("/World/env_[^/]+/robot/base/imu_link") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -429,7 +429,7 @@ def test_sensor_initialization(sim_ctx, device): """Test that the OVPhysX IMU sensor initializes correctly.""" _spawn_envs(NUM_ENVS) _spawn_balls(NUM_ENVS) - imu_ball = _make_imu("/World/env_*/ball") + imu_ball = _make_imu("/World/env_[^/]+/ball") sim_ctx.reset() assert imu_ball.num_instances == NUM_ENVS @@ -452,7 +452,7 @@ def test_gravity_at_rest(sim_ctx, device): """ _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) - imu_ball = _make_imu("/World/env_*/ball") + imu_ball = _make_imu("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -488,7 +488,7 @@ def test_freefall_acceleration(sim_ctx, device): """ _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS, height=5.0) - imu_ball = _make_imu("/World/env_*/ball") + imu_ball = _make_imu("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -520,7 +520,7 @@ def test_reset(sim_ctx, device): """ _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) - imu_ball = _make_imu("/World/env_*/ball") + imu_ball = _make_imu("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -596,8 +596,8 @@ def test_indirect_attachment_usd(sim_ctx, device): sub_rot = (0.5, 0.5, 0.5, 0.5) for i in range(NUM_ENVS): sim_utils.create_prim(f"/World/env_{i}/ball/imu_sub", "Xform", translation=sub_pos, orientation=sub_rot) - imu_indirect = _make_imu("/World/env_*/ball/imu_sub") - imu_direct = _make_imu("/World/env_*/ball", offset=ImuCfg.OffsetCfg(pos=sub_pos, rot=sub_rot)) + imu_indirect = _make_imu("/World/env_[^/]+/ball/imu_sub") + imu_direct = _make_imu("/World/env_[^/]+/ball", offset=ImuCfg.OffsetCfg(pos=sub_pos, rot=sub_rot)) sim_ctx.reset() torch.testing.assert_close( @@ -665,14 +665,14 @@ def test_sensor_print(sim_ctx, device): """Test ``__str__`` is implemented and exposes the prim path and binding pattern.""" _spawn_envs(NUM_ENVS) _spawn_balls(NUM_ENVS) - imu_ball = _make_imu("/World/env_*/ball") + imu_ball = _make_imu("/World/env_[^/]+/ball") sim_ctx.reset() s = str(imu_ball) print(s) - assert "Imu sensor @ '/World/env_*/ball'" in s + assert "Imu sensor @ '/World/env_[^/]+/ball'" in s assert "binding pattern" in s - assert "/World/env_*/ball" in s + assert "/World/env_[^/]+/ball" in s assert "number of sensors : 2" in s diff --git a/source/isaaclab_ov/test/sensors/test_pva.py b/source/isaaclab_ov/test/sensors/test_pva.py index e9cd6dc92251..f7f31d45ab86 100644 --- a/source/isaaclab_ov/test/sensors/test_pva.py +++ b/source/isaaclab_ov/test/sensors/test_pva.py @@ -93,8 +93,8 @@ def _spawn_balls(num_envs: int, height: float = 0.5) -> RigidObject: Returns the :class:`RigidObject` whose binding pattern matches all spawned instances. The :class:`RigidObject` does the per-env spawning itself when ``spawn`` is set; we only have to create the env Xform containers first - (handled by :func:`_spawn_envs`). Note the ovphysx pattern uses an - fnmatch glob (``env_*``), not a regex. + (handled by :func:`_spawn_envs`). The prim path is a regex; the ovphysx + binding pattern underneath it is an fnmatch glob. """ spawn_cfg = sim_utils.SphereCfg( radius=0.25, @@ -104,7 +104,7 @@ def _spawn_balls(num_envs: int, height: float = 0.5) -> RigidObject: visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), ) cfg = RigidObjectCfg( - prim_path="/World/env_*/ball", + prim_path="/World/env_[^/]+/ball", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height)), ) @@ -121,7 +121,7 @@ def _spawn_cubes(num_envs: int, height: float = 0.5) -> RigidObject: visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), ) cfg = RigidObjectCfg( - prim_path="/World/env_*/cube", + prim_path="/World/env_[^/]+/cube", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -2.0, height)), ) @@ -232,8 +232,8 @@ def test_constant_velocity(sim_ctx, device): _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) cubes = _spawn_cubes(NUM_ENVS) - pva_ball = _make_pva("/World/env_*/ball") - pva_cube = _make_pva("/World/env_*/cube") + pva_ball = _make_pva("/World/env_[^/]+/ball") + pva_cube = _make_pva("/World/env_[^/]+/cube") sim_ctx.reset() prev_lin_acc_ball = torch.zeros((NUM_ENVS, 3), dtype=torch.float32, device=device) @@ -322,7 +322,7 @@ def test_constant_acceleration(sim_ctx, device): """ _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) - pva_ball = _make_pva("/World/env_*/ball") + pva_ball = _make_pva("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -378,9 +378,9 @@ def test_offset_calculation(sim_ctx, device): _spawn_envs(NUM_ENVS) cubes = _spawn_cubes(NUM_ENVS) _add_pva_mount_xforms(NUM_ENVS) - pva_child = _make_pva("/World/env_*/cube/pva_mount") + pva_child = _make_pva("/World/env_[^/]+/cube/pva_mount") pva_direct = _make_pva( - "/World/env_*/cube", + "/World/env_[^/]+/cube", offset=PvaCfg.OffsetCfg(pos=MOUNT_POS_OFFSET, rot=MOUNT_ROT_OFFSET), ) sim_ctx.reset() @@ -455,7 +455,7 @@ def test_env_ids_propagation(sim_ctx, device): """Test that ``env_ids`` argument propagates through update and reset methods.""" _spawn_envs(NUM_ENVS) cubes = _spawn_cubes(NUM_ENVS) - pva_cube = _make_pva("/World/env_*/cube") + pva_cube = _make_pva("/World/env_[^/]+/cube") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -495,7 +495,7 @@ def test_sensor_initialization(sim_ctx, device): """Test that the OVPhysX PVA sensor initializes correctly.""" _spawn_envs(NUM_ENVS) _spawn_balls(NUM_ENVS) - pva_ball = _make_pva("/World/env_*/ball") + pva_ball = _make_pva("/World/env_[^/]+/ball") sim_ctx.reset() assert pva_ball.num_instances == NUM_ENVS @@ -527,7 +527,7 @@ def test_pose_w_packing(sim_ctx, device): """ _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) - pva_ball = _make_pva("/World/env_*/ball") + pva_ball = _make_pva("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -556,7 +556,7 @@ def test_projected_gravity_at_rest(sim_ctx, device): """ _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) - pva_ball = _make_pva("/World/env_*/ball") + pva_ball = _make_pva("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -583,7 +583,7 @@ def test_freefall_lin_acc(sim_ctx, device): """ _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS, height=5.0) - pva_ball = _make_pva("/World/env_*/ball") + pva_ball = _make_pva("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -615,7 +615,7 @@ def test_reset(sim_ctx, device): """ _spawn_envs(NUM_ENVS) balls = _spawn_balls(NUM_ENVS) - pva_ball = _make_pva("/World/env_*/ball") + pva_ball = _make_pva("/World/env_[^/]+/ball") sim_ctx.reset() dt = sim_ctx.get_physics_dt() @@ -719,8 +719,8 @@ def test_indirect_attachment_usd(sim_ctx, device): sub_rot = (0.5, 0.5, 0.5, 0.5) for i in range(NUM_ENVS): sim_utils.create_prim(f"/World/env_{i}/ball/pva_sub", "Xform", translation=sub_pos, orientation=sub_rot) - pva_indirect = _make_pva("/World/env_*/ball/pva_sub") - pva_direct = _make_pva("/World/env_*/ball", offset=PvaCfg.OffsetCfg(pos=sub_pos, rot=sub_rot)) + pva_indirect = _make_pva("/World/env_[^/]+/ball/pva_sub") + pva_direct = _make_pva("/World/env_[^/]+/ball", offset=PvaCfg.OffsetCfg(pos=sub_pos, rot=sub_rot)) sim_ctx.reset() torch.testing.assert_close( @@ -818,14 +818,14 @@ def test_sensor_print(sim_ctx, device): """Test ``__str__`` is implemented and exposes the prim path and binding pattern.""" _spawn_envs(NUM_ENVS) _spawn_balls(NUM_ENVS) - pva_ball = _make_pva("/World/env_*/ball") + pva_ball = _make_pva("/World/env_[^/]+/ball") sim_ctx.reset() s = str(pva_ball) print(s) - assert "Pva sensor @ '/World/env_*/ball'" in s + assert "Pva sensor @ '/World/env_[^/]+/ball'" in s assert "binding pattern" in s - assert "/World/env_*/ball" in s + assert "/World/env_[^/]+/ball" in s assert "number of sensors : 2" in s diff --git a/source/isaaclab_ov/test/sensors/test_ray_caster.py b/source/isaaclab_ov/test/sensors/test_ray_caster.py index 37fa93ad4038..3b333975f6cc 100644 --- a/source/isaaclab_ov/test/sensors/test_ray_caster.py +++ b/source/isaaclab_ov/test/sensors/test_ray_caster.py @@ -36,10 +36,10 @@ def create_tensor_binding(self, *, pattern, tensor_type): class _DummyRayCaster(ray_caster_module._OvPhysxRayCasterMixin): def __init__(self): - self.cfg = SimpleNamespace(prim_path="/World/envs/env_.*/Robot/base/ray") + self.cfg = SimpleNamespace(prim_path="/World/envs/env_[^/]+/Robot/base/ray") self._device = "cpu" self._resolved = ( - "/World/envs/env_.*/Robot/base", + "/World/envs/env_[^/]+/Robot/base", (0.1, 0.2, 0.3), (0.0, 0.0, 0.0, 1.0), ) diff --git a/source/isaaclab_ov/test/sim/test_views_xform_prim_ovphysx.py b/source/isaaclab_ov/test/sim/test_views_xform_prim_ovphysx.py index 67e060c41a2c..e60da304b818 100644 --- a/source/isaaclab_ov/test/sim/test_views_xform_prim_ovphysx.py +++ b/source/isaaclab_ov/test/sim/test_views_xform_prim_ovphysx.py @@ -74,7 +74,7 @@ def test_world_attached_source_prim_expands_from_clone_plan(): sim_utils.standardize_xform_ops(prim) prim.GetAttribute("xformOp:translate").Set(Gf.Vec3d(0.25, -0.5, 1.0)) - view = FrameView("/World/envs/env_.*/WorldCamera", device=device) + view = FrameView("/World/envs/env_[^/]+/WorldCamera", device=device) assert not stage.GetPrimAtPath("/World/envs/env_1/WorldCamera").IsValid() assert view.count == scene.num_envs @@ -192,7 +192,7 @@ def _build(num_envs: int, device: str) -> ViewBundle: prim.GetAttribute("xformOp:orient").Set(Gf.Quatd(1.0, 0.0, 0.0, 0.0)) sim.reset() - view = OvPhysxFrameView("/World/envs/env_.*/Cube/CameraMount", device=device) + view = OvPhysxFrameView("/World/envs/env_[^/]+/Cube/CameraMount", device=device) # Capture binding row order, populate _pose_buf once with the live spawn poses, # then detach the binding so subsequent reads do not overwrite the buffer. diff --git a/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py b/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py index 871730c398b0..5dec8d02dba9 100644 --- a/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py +++ b/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py @@ -118,8 +118,8 @@ def test_setup_deformable_bindings_binds_surface_mesh_points(monkeypatch: pytest """Surface deformable registry entries create OVRTX ``points`` array bindings.""" renderer, backend = _make_renderer_without_backend() entry = SimpleNamespace( - prim_path="/World/envs/env_.*/Deformable", - vis_mesh_prim_path="/World/envs/env_.*/Deformable/mesh", + prim_path="/World/envs/env_[^/]+/Deformable", + vis_mesh_prim_path="/World/envs/env_[^/]+/Deformable/mesh", deformable_type="surface", particle_offsets=[7], particles_per_body=3, @@ -149,8 +149,8 @@ def test_setup_deformable_bindings_binds_volume_mesh_points(monkeypatch: pytest. """Volume deformable registry entries create OVRTX ``points`` bindings.""" renderer, backend = _make_renderer_without_backend() entry = SimpleNamespace( - prim_path="/World/envs/env_.*/Deformable", - vis_mesh_prim_path="/World/envs/env_.*/Deformable/mesh", + prim_path="/World/envs/env_[^/]+/Deformable", + vis_mesh_prim_path="/World/envs/env_[^/]+/Deformable/mesh", deformable_type="volume", particle_offsets=[7], particles_per_body=3, @@ -171,15 +171,15 @@ def test_setup_deformable_bindings_binds_mixed_surface_and_volume_entries(monkey """Surface and volume deformable registry entries bind together with distinct offsets.""" renderer, backend = _make_renderer_without_backend() surface_entry = SimpleNamespace( - prim_path="/World/envs/env_.*/DeformableSurface", - vis_mesh_prim_path="/World/envs/env_.*/DeformableSurface/mesh", + prim_path="/World/envs/env_[^/]+/DeformableSurface", + vis_mesh_prim_path="/World/envs/env_[^/]+/DeformableSurface/mesh", deformable_type="surface", particle_offsets=[0, 3], particles_per_body=3, ) volume_entry = SimpleNamespace( - prim_path="/World/envs/env_.*/DeformableVolume", - vis_mesh_prim_path="/World/envs/env_.*/DeformableVolume/mesh", + prim_path="/World/envs/env_[^/]+/DeformableVolume", + vis_mesh_prim_path="/World/envs/env_[^/]+/DeformableVolume/mesh", deformable_type="volume", particle_offsets=[6, 9], particles_per_body=3, @@ -203,8 +203,8 @@ def test_setup_deformable_bindings_works_without_stage(monkeypatch: pytest.Monke """Deformable bindings are created from registry metadata without a USD stage.""" renderer, backend = _make_renderer_without_backend() entry = SimpleNamespace( - prim_path="/World/envs/env_.*/Deformable", - vis_mesh_prim_path="/World/envs/env_.*/Deformable/mesh", + prim_path="/World/envs/env_[^/]+/Deformable", + vis_mesh_prim_path="/World/envs/env_[^/]+/Deformable/mesh", deformable_type="surface", particle_offsets=[0], particles_per_body=3, @@ -224,8 +224,8 @@ def test_setup_deformable_bindings_binds_all_surface_mesh_instances(monkeypatch: """Surface deformable registry entries bind every cloned visual mesh instance.""" renderer, backend = _make_renderer_without_backend() entry = SimpleNamespace( - prim_path="/World/envs/env_.*/Deformable", - vis_mesh_prim_path="/World/envs/env_.*/Deformable/mesh", + prim_path="/World/envs/env_[^/]+/Deformable", + vis_mesh_prim_path="/World/envs/env_[^/]+/Deformable/mesh", deformable_type="surface", particle_offsets=[0, 3, 6, 9], particles_per_body=3, @@ -284,15 +284,15 @@ def test_setup_deformable_bindings_rejects_offset_count_mismatch(monkeypatch: py """Registry entries must provide one particle offset per environment, listing every bad entry.""" renderer, _backend = _make_renderer_without_backend() bad_entry = SimpleNamespace( - prim_path="/World/envs/env_.*/Deformable", - vis_mesh_prim_path="/World/envs/env_.*/Deformable/mesh", + prim_path="/World/envs/env_[^/]+/Deformable", + vis_mesh_prim_path="/World/envs/env_[^/]+/Deformable/mesh", deformable_type="surface", particle_offsets=[0], particles_per_body=3, ) other_bad_entry = SimpleNamespace( - prim_path="/World/envs/env_.*/DeformableOther", - vis_mesh_prim_path="/World/envs/env_.*/DeformableOther/mesh", + prim_path="/World/envs/env_[^/]+/DeformableOther", + vis_mesh_prim_path="/World/envs/env_[^/]+/DeformableOther/mesh", deformable_type="surface", particle_offsets=[0, 3, 6], particles_per_body=3, diff --git a/source/isaaclab_ov/test/test_randomize_rigid_body_material_mdp.py b/source/isaaclab_ov/test/test_randomize_rigid_body_material_mdp.py index db9661d11049..419bad5ba894 100644 --- a/source/isaaclab_ov/test/test_randomize_rigid_body_material_mdp.py +++ b/source/isaaclab_ov/test/test_randomize_rigid_body_material_mdp.py @@ -70,7 +70,7 @@ def _make_cubes(num_cubes: int, device: str) -> RigidObject: for i in range(num_cubes): sim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=(i * 1.0, 0.0, 1.0)) cfg = RigidObjectCfg( - prim_path="/World/Table_*/Object", + prim_path="/World/Table_[^/]+/Object", spawn=sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd"), init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 1.0)), ) diff --git a/source/isaaclab_physx/changelog.d/octi-prim-path-real-regex-matcher.rst b/source/isaaclab_physx/changelog.d/octi-prim-path-real-regex-matcher.rst new file mode 100644 index 000000000000..44e9f4675203 --- /dev/null +++ b/source/isaaclab_physx/changelog.d/octi-prim-path-real-regex-matcher.rst @@ -0,0 +1,15 @@ +Changed +^^^^^^^ + +* Changed prim path expressions to spell a single path segment ``[^/]`` rather than ``.``, so each + pattern selects what it selected before now that ``.`` matches ``/`` in + :func:`~isaaclab.sim.utils.find_matching_prims`. + +Fixed +^^^^^ + +* Fixed physics views receiving a regular expression where the engine expects a glob. The + conversion rewrote only ``.*`` and left a segment-safe wildcard untouched, so the view matched + no bodies; it now goes through :func:`~isaaclab.sim.utils.path_expr_to_glob`. +* Fixed :class:`~isaaclab_physx.sensors.FrameTransformer` corrupting a prim path expression while + stripping the environment segment, which split a ``[^/]`` character class in half. diff --git a/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py b/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py index c5875271f0e3..b482704fff91 100644 --- a/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py +++ b/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py @@ -24,7 +24,7 @@ from isaaclab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator from isaaclab.assets.articulation import ordering_kernels from isaaclab.assets.articulation.base_articulation import BaseArticulation -from isaaclab.sim.utils.queries import find_first_matching_prim, resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import find_first_matching_prim, path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.string import resolve_matching_names, resolve_matching_names_values from isaaclab.utils.types import ArticulationActions from isaaclab.utils.version import get_isaac_sim_version, has_kit @@ -4207,7 +4207,7 @@ def has_articulation_root_api(prim) -> bool: resolve_kwargs = {"predicate": has_articulation_root_api, "expected_num_matches": 1} _, root_prim_path_expr = resolve_matching_prims_from_source(self.cfg.prim_path, **resolve_kwargs)[0] # -- articulation - self._root_view = self._physics_sim_view.create_articulation_view(root_prim_path_expr.replace(".*", "*")) + self._root_view = self._physics_sim_view.create_articulation_view(path_expr_to_glob(root_prim_path_expr)) # check if the articulation was created if self.root_view._backend is None: diff --git a/source/isaaclab_physx/isaaclab_physx/assets/deformable_object/deformable_object.py b/source/isaaclab_physx/isaaclab_physx/assets/deformable_object/deformable_object.py index d3b2486e8363..58abfdb614e9 100644 --- a/source/isaaclab_physx/isaaclab_physx/assets/deformable_object/deformable_object.py +++ b/source/isaaclab_physx/isaaclab_physx/assets/deformable_object/deformable_object.py @@ -644,12 +644,12 @@ def has_deformable_body_api(prim) -> bool: if self._deformable_type == "surface": # surface deformable self._root_physx_view = self._physics_sim_view.create_surface_deformable_body_view( - root_prim_path_expr.replace(".*", "*") + sim_utils.path_expr_to_glob(root_prim_path_expr) ) elif self._deformable_type == "volume": # volume deformable self._root_physx_view = self._physics_sim_view.create_volume_deformable_body_view( - root_prim_path_expr.replace(".*", "*") + sim_utils.path_expr_to_glob(root_prim_path_expr) ) else: raise RuntimeError( @@ -677,7 +677,7 @@ def has_deformable_body_api(prim) -> bool: material_prim_path_expr = material_prim_path # -- material view self._material_physx_view = self._physics_sim_view.create_deformable_material_view( - material_prim_path_expr.replace(".*", "*") + sim_utils.path_expr_to_glob(material_prim_path_expr) ) else: self._material_physx_view = None diff --git a/source/isaaclab_physx/isaaclab_physx/assets/rigid_object/rigid_object.py b/source/isaaclab_physx/isaaclab_physx/assets/rigid_object/rigid_object.py index d4606ee959cc..23951536c649 100644 --- a/source/isaaclab_physx/isaaclab_physx/assets/rigid_object/rigid_object.py +++ b/source/isaaclab_physx/isaaclab_physx/assets/rigid_object/rigid_object.py @@ -17,7 +17,7 @@ import isaaclab.utils.string as string_utils from isaaclab.assets.rigid_object.base_rigid_object import BaseRigidObject -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.warp import ProxyArray from isaaclab.utils.wrench_composer import WrenchComposer @@ -1004,7 +1004,7 @@ def has_rigid_body_api(prim) -> bool: resolve_kwargs = {"predicate": has_rigid_body_api, "expected_num_matches": 1} _, root_prim_path_expr = resolve_matching_prims_from_source(self.cfg.prim_path, **resolve_kwargs)[0] # -- object view - self._root_view = self._physics_sim_view.create_rigid_body_view(root_prim_path_expr.replace(".*", "*")) + self._root_view = self._physics_sim_view.create_rigid_body_view(path_expr_to_glob(root_prim_path_expr)) # check if the rigid body was created if self.root_view._backend is None: diff --git a/source/isaaclab_physx/isaaclab_physx/assets/rigid_object_collection/rigid_object_collection.py b/source/isaaclab_physx/isaaclab_physx/assets/rigid_object_collection/rigid_object_collection.py index a4b9cdca6c88..f7b41249097f 100644 --- a/source/isaaclab_physx/isaaclab_physx/assets/rigid_object_collection/rigid_object_collection.py +++ b/source/isaaclab_physx/isaaclab_physx/assets/rigid_object_collection/rigid_object_collection.py @@ -1353,7 +1353,7 @@ def has_rigid_body_api(prim) -> bool: root_prim_path_exprs = [] for name, obj_cfg in self.cfg.rigid_objects.items(): _, root_expr = sim_utils.resolve_matching_prims_from_source(obj_cfg.prim_path, **resolve_kwargs)[0] - root_prim_path_exprs.append(root_expr.replace(".*", "*")) + root_prim_path_exprs.append(sim_utils.path_expr_to_glob(root_expr)) self._body_names_list.append(name) # -- object view diff --git a/source/isaaclab_physx/isaaclab_physx/assets/surface_gripper/surface_gripper.py b/source/isaaclab_physx/isaaclab_physx/assets/surface_gripper/surface_gripper.py index 0d134b13a990..504da0459127 100644 --- a/source/isaaclab_physx/isaaclab_physx/assets/surface_gripper/surface_gripper.py +++ b/source/isaaclab_physx/isaaclab_physx/assets/surface_gripper/surface_gripper.py @@ -6,7 +6,6 @@ from __future__ import annotations import logging -import re import warnings from typing import TYPE_CHECKING @@ -469,11 +468,7 @@ def is_surface_gripper(prim) -> bool: f"found {len(gripper_matches)}: {matched}." ) _, self._prim_expr = gripper_matches[0] - # ``GripperView`` (XformPrim.resolve_paths) requires explicit regex (".*") and rejects the - # legacy "*" wildcard that the clone-plan destination glob (e.g. "/World/envs/env_*") can - # carry. Convert any bare "*" to ".*" (a "*" already preceded by "." is left untouched). - self._prim_expr = re.sub(r"(? bool: self._process_cfg() # Initialize gripper view and set properties. + # ``GripperView`` (XformPrim.resolve_paths) matches one regex per path segment, so a + # segment wildcard has to be spelled ``.*`` there: ``[^/]`` holds a separator and would + # be split across two segments. self._gripper_view = GripperView( - self._prim_expr, + sim_utils.path_expr_to_glob(self._prim_expr).replace("*", ".*"), ) self.update_gripper_properties_index( max_grip_distance=wp.clone(self._max_grip_distance), diff --git a/source/isaaclab_physx/isaaclab_physx/sensors/contact_sensor/contact_sensor.py b/source/isaaclab_physx/isaaclab_physx/sensors/contact_sensor/contact_sensor.py index 33d0a8eb5c12..b7d20aefaedc 100644 --- a/source/isaaclab_physx/isaaclab_physx/sensors/contact_sensor/contact_sensor.py +++ b/source/isaaclab_physx/isaaclab_physx/sensors/contact_sensor/contact_sensor.py @@ -21,7 +21,7 @@ from isaaclab.app.settings_manager import get_settings_manager from isaaclab.markers import VisualizationMarkers from isaaclab.sensors.contact_sensor import BaseContactSensor -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source, split_path_expr from isaaclab.utils.warp import ProxyArray from isaaclab_physx.physics import PhysxManager as SimulationManager @@ -315,7 +315,9 @@ def _initialize_impl(self): self._physics_sim_view = SimulationManager.get_physics_sim_view() # Split the configured prim path into a parent expression and a leaf-name regex. - parent_expr, leaf_pattern = self.cfg.prim_path.rsplit("/", 1) + # split on separators only: a trailing ``[^/]`` class holds a ``/`` that is not one + *parent_segments, leaf_pattern = split_path_expr(self.cfg.prim_path) + parent_expr = "/".join(parent_segments) name_pattern = re.compile(leaf_pattern) def has_contact_report(prim) -> bool: @@ -339,8 +341,8 @@ def has_contact_report(prim) -> bool: # parent-level name alternation cannot address them. # note: with a list of patterns, the views order bodies pattern-major: # view_id = body_id * num_envs + env_id - body_path_globs = [expr.replace(".*", "*") for _, expr in body_matches] - filter_prim_paths_glob = [expr.replace(".*", "*") for expr in self.cfg.filter_prim_paths_expr] + body_path_globs = [path_expr_to_glob(expr) for _, expr in body_matches] + filter_prim_paths_glob = [path_expr_to_glob(expr) for expr in self.cfg.filter_prim_paths_expr] # create a rigid prim view for the sensor self._body_physx_view = self._physics_sim_view.create_rigid_body_view(body_path_globs) diff --git a/source/isaaclab_physx/isaaclab_physx/sensors/frame_transformer/frame_transformer.py b/source/isaaclab_physx/isaaclab_physx/sensors/frame_transformer/frame_transformer.py index ab3a6e821078..98257c165c20 100644 --- a/source/isaaclab_physx/isaaclab_physx/sensors/frame_transformer/frame_transformer.py +++ b/source/isaaclab_physx/isaaclab_physx/sensors/frame_transformer/frame_transformer.py @@ -18,7 +18,7 @@ from isaaclab.markers import VisualizationMarkers from isaaclab.sensors.frame_transformer import BaseFrameTransformer -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.math import is_identity_pose, normalize, quat_from_angle_axis from isaaclab_physx.physics import PhysxManager as SimulationManager @@ -247,7 +247,7 @@ def has_rigid_body_api(prim) -> bool: # Plan-mode dest expressions use ``env_.*`` (regex), legacy mode produces concrete # ``env_0`` paths; chain both substitutions so each mode normalises to ``env_*``. body_names_regex = [ - tracked_prim_path.replace(".*", "*").replace("env_0", "env_*") for tracked_prim_path in tracked_prim_paths + path_expr_to_glob(tracked_prim_path).replace("env_0", "env_*") for tracked_prim_path in tracked_prim_paths ] # obtain global simulation view @@ -605,5 +605,8 @@ def _get_relative_body_path(prim_path: str) -> str: Returns: The prim path with `/envs/env_/` removed, preserving `/envs/`. """ - pattern = re.compile(r"/envs/env_[^/]+/") + # the input may be a concrete path or an expression, so the environment segment can be a + # concrete id or a wildcard standing for one; a wildcard written as a character class holds + # a '/' that is not a separator and would otherwise be split by the one-segment alternative + pattern = re.compile(r"/envs/env_(?:\[\^/\][*+]|\.\*|[^/]+)/") return pattern.sub("/envs/", prim_path) diff --git a/source/isaaclab_physx/isaaclab_physx/sensors/imu/imu.py b/source/isaaclab_physx/isaaclab_physx/sensors/imu/imu.py index a111a9a14e18..d54cf06cb0db 100644 --- a/source/isaaclab_physx/isaaclab_physx/sensors/imu/imu.py +++ b/source/isaaclab_physx/isaaclab_physx/sensors/imu/imu.py @@ -14,6 +14,7 @@ import isaaclab.utils.math as math_utils from isaaclab.sensors.imu import BaseImu +from isaaclab.sim.utils.queries import path_expr_to_glob from isaaclab_physx.physics import PhysxManager as SimulationManager @@ -129,7 +130,7 @@ def _initialize_impl(self): self._physics_sim_view = SimulationManager.get_physics_sim_view() self._rigid_parent_expr, fixed_pos_b, fixed_quat_b = self._resolve_rigid_body_ancestor_expr() - self._view = self._physics_sim_view.create_rigid_body_view(self._rigid_parent_expr.replace(".*", "*")) + self._view = self._physics_sim_view.create_rigid_body_view(path_expr_to_glob(self._rigid_parent_expr)) # Query world gravity and compute accelerometer bias (real IMUs always measure gravity) gravity = self._physics_sim_view.get_gravity() diff --git a/source/isaaclab_physx/isaaclab_physx/sensors/joint_wrench/joint_wrench_sensor.py b/source/isaaclab_physx/isaaclab_physx/sensors/joint_wrench/joint_wrench_sensor.py index c2d77e4dd445..3099790ac5f9 100644 --- a/source/isaaclab_physx/isaaclab_physx/sensors/joint_wrench/joint_wrench_sensor.py +++ b/source/isaaclab_physx/isaaclab_physx/sensors/joint_wrench/joint_wrench_sensor.py @@ -17,7 +17,7 @@ from pxr import Usd, UsdPhysics from isaaclab.sensors.joint_wrench import BaseJointWrenchSensor -from isaaclab.sim.utils.queries import resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab_physx.physics import PhysxManager as SimulationManager @@ -131,7 +131,7 @@ def has_articulation_root_api(prim) -> bool: resolve_kwargs = {"predicate": has_articulation_root_api, "expected_num_matches": 1} _, root_prim_path_expr = resolve_matching_prims_from_source(self.cfg.prim_path, **resolve_kwargs)[0] - self._root_view = self._physics_sim_view.create_articulation_view(root_prim_path_expr.replace(".*", "*")) + self._root_view = self._physics_sim_view.create_articulation_view(path_expr_to_glob(root_prim_path_expr)) if self._root_view._backend is None: raise RuntimeError(f"Failed to create articulation view at: {root_prim_path_expr}. Check PhysX logs.") diff --git a/source/isaaclab_physx/isaaclab_physx/sensors/pva/pva.py b/source/isaaclab_physx/isaaclab_physx/sensors/pva/pva.py index ca1a5e498806..1018dbeec21b 100644 --- a/source/isaaclab_physx/isaaclab_physx/sensors/pva/pva.py +++ b/source/isaaclab_physx/isaaclab_physx/sensors/pva/pva.py @@ -17,6 +17,7 @@ import isaaclab.utils.math as math_utils from isaaclab.markers import VisualizationMarkers from isaaclab.sensors.pva import BasePva +from isaaclab.sim.utils.queries import path_expr_to_glob from isaaclab.utils.warp import ProxyArray from isaaclab_physx.physics import PhysxManager as SimulationManager @@ -155,7 +156,7 @@ def _initialize_impl(self): self._rigid_parent_expr, fixed_pos_b, fixed_quat_b = self._resolve_rigid_body_ancestor_expr() # Create the rigid body view on the ancestor - self._view = self._physics_sim_view.create_rigid_body_view(self._rigid_parent_expr.replace(".*", "*")) + self._view = self._physics_sim_view.create_rigid_body_view(path_expr_to_glob(self._rigid_parent_expr)) # Get world gravity gravity = self._physics_sim_view.get_gravity() diff --git a/source/isaaclab_physx/isaaclab_physx/sensors/ray_caster/ray_caster.py b/source/isaaclab_physx/isaaclab_physx/sensors/ray_caster/ray_caster.py index b1cb54de49b9..ca74b3562dfe 100644 --- a/source/isaaclab_physx/isaaclab_physx/sensors/ray_caster/ray_caster.py +++ b/source/isaaclab_physx/isaaclab_physx/sensors/ray_caster/ray_caster.py @@ -32,7 +32,7 @@ def _has_rigid_body_api(prim) -> bool: def _physx_body_glob(body_expr: str) -> str: """Convert internal env regex/template expressions to PhysX glob syntax.""" - return body_expr.replace("{}", "*").replace(".*", "*") + return sim_utils.path_expr_to_glob(body_expr.replace("{}", "*")) class _PhysXRayCasterMixin: @@ -128,29 +128,12 @@ def _create_tracked_target_view(self: Any, target_prim_paths: str | list[str]): """Create a PhysX rigid-body view for dynamic multi-mesh targets.""" if isinstance(target_prim_paths, str): target_prim_paths = [target_prim_paths] - body_paths = [] - for target_prim_path in target_prim_paths: - prims = sim_utils.find_matching_prims(target_prim_path) - if len(prims) == 0: - # ClonePlan-backed targets may not have destination mesh prims. - # In that case BaseMultiMeshRayCaster passes the destination owner-body expression. - body_paths.append(target_prim_path) - continue - for prim in prims: - body = sim_utils.get_first_matching_ancestor_prim(prim.GetPath(), predicate=_has_rigid_body_api) - if body is None: - raise RuntimeError( - f"Cannot track non-physics ray-cast target '{target_prim_path}' with PhysX. " - "Set track_mesh_transforms=False for static targets, or apply RigidBodyAPI to dynamic targets." - ) - body_paths.append(body.GetPath().pathString) - - if len(body_paths) == 0: + if not target_prim_paths: raise RuntimeError(f"No tracked target bodies resolved from: {target_prim_paths}") physics_sim_view = PhysxManager.get_physics_sim_view() if physics_sim_view is None: raise RuntimeError("PhysX simulation view is not initialized.") - return physics_sim_view.create_rigid_body_view([_physx_body_glob(path) for path in body_paths]) + return physics_sim_view.create_rigid_body_view([_physx_body_glob(path) for path in target_prim_paths]) def _update_mesh_transforms(self: Any) -> None: """Refresh dynamic multi-mesh targets directly from PhysX views.""" diff --git a/source/isaaclab_physx/test/assets/test_articulation.py b/source/isaaclab_physx/test/assets/test_articulation.py index d1a3847a5c35..427d2329e2a5 100644 --- a/source/isaaclab_physx/test/assets/test_articulation.py +++ b/source/isaaclab_physx/test/assets/test_articulation.py @@ -202,7 +202,7 @@ def generate_articulation( # Create Top-level Xforms, one for each articulation for i in range(num_articulations): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) - articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_.*/Robot")) + articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_[^/]*/Robot")) return articulation, translations @@ -234,7 +234,7 @@ def _setup_franka_at_home_pose(sim, *, zero_actuator_pd: bool = False, enable_ri Returns: Tuple of ``(robot, ee_frame_idx, ee_jacobi_idx, arm_joint_ids)``. """ - cfg = FRANKA_PANDA_HIGH_PD_CFG.copy().replace(prim_path="/World/Env_.*/Robot") + cfg = FRANKA_PANDA_HIGH_PD_CFG.copy().replace(prim_path="/World/Env_[^/]*/Robot") if zero_actuator_pd: cfg.actuators["panda_shoulder"].stiffness = 0.0 cfg.actuators["panda_shoulder"].damping = 0.0 diff --git a/source/isaaclab_physx/test/assets/test_deformable_object.py b/source/isaaclab_physx/test/assets/test_deformable_object.py index 7a884fe4e0e7..d1bd725bd618 100644 --- a/source/isaaclab_physx/test/assets/test_deformable_object.py +++ b/source/isaaclab_physx/test/assets/test_deformable_object.py @@ -96,7 +96,7 @@ def generate_cubes_scene( ) # Create deformable object cube_object_cfg = DeformableObjectCfg( - prim_path="/World/Table_.*/Object", + prim_path="/World/Table_[^/]*/Object", spawn=spawn_cfg, init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height), rot=initial_rot), ) diff --git a/source/isaaclab_physx/test/assets/test_newton_actuators_physx.py b/source/isaaclab_physx/test/assets/test_newton_actuators_physx.py index 4a0917500081..abfedaad6f99 100644 --- a/source/isaaclab_physx/test/assets/test_newton_actuators_physx.py +++ b/source/isaaclab_physx/test/assets/test_newton_actuators_physx.py @@ -175,7 +175,7 @@ def _run_simulation( sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) art_cfg = ANYMAL_C_CFG.replace( actuators=actuators, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", joint_ordering=joint_ordering, ) articulation = Articulation(art_cfg) @@ -301,7 +301,7 @@ def _assert_newton_actuator_uses_current_joint_state( sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) art_cfg = ANYMAL_C_CFG.replace( actuators=actuators, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", joint_ordering=joint_ordering, ) articulation = Articulation(art_cfg) @@ -562,10 +562,10 @@ def _run_anymal_and_cartpole(use_newton_actuators: bool, *, num_steps: int = NUM for i in range(NUM_ENVS): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 6.0, 0, 0)) - anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_.*/Anymal") + anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_[^/]*/Anymal") cartpole_cfg = CARTPOLE_CFG.replace( actuators=CARTPOLE_EXPLICIT_ACTUATORS, - prim_path="/World/Env_.*/Cartpole", + prim_path="/World/Env_[^/]*/Cartpole", ) cartpole_cfg.init_state = cartpole_cfg.init_state.replace(pos=(0.0, 3.0, 2.0)) @@ -728,7 +728,7 @@ def test_single_articulation(self): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) art_cfg = ANYMAL_C_CFG.replace( actuators=IDEAL_PD_ACTUATORS, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", ) anymal = Articulation(art_cfg) sim.reset() @@ -775,10 +775,10 @@ def test_two_articulations(self): for i in range(NUM_ENVS): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 6.0, 0, 0)) - anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_.*/Anymal") + anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_[^/]*/Anymal") cartpole_cfg = CARTPOLE_CFG.replace( actuators=CARTPOLE_EXPLICIT_ACTUATORS, - prim_path="/World/Env_.*/Cartpole", + prim_path="/World/Env_[^/]*/Cartpole", ) cartpole_cfg.init_state = cartpole_cfg.init_state.replace(pos=(0.0, 3.0, 2.0)) anymal = Articulation(anymal_cfg) @@ -869,7 +869,7 @@ def _build_and_warm(self, *, use_newton_actuators: bool): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) art_cfg = ANYMAL_C_CFG.replace( actuators=DELAYED_PD_ACTUATORS, - prim_path="/World/Env_.*/Robot", + prim_path="/World/Env_[^/]*/Robot", ) articulation = Articulation(art_cfg) sim.reset() diff --git a/source/isaaclab_physx/test/assets/test_rigid_object.py b/source/isaaclab_physx/test/assets/test_rigid_object.py index 183f533fef7f..40b9b80fb33a 100644 --- a/source/isaaclab_physx/test/assets/test_rigid_object.py +++ b/source/isaaclab_physx/test/assets/test_rigid_object.py @@ -89,7 +89,7 @@ def generate_cubes_scene( # Create rigid object cube_object_cfg = RigidObjectCfg( - prim_path="/World/Table_.*/Object", + prim_path="/World/Table_[^/]*/Object", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height)), ) diff --git a/source/isaaclab_physx/test/assets/test_rigid_object_collection.py b/source/isaaclab_physx/test/assets/test_rigid_object_collection.py index 7fad208a4db6..6286c017e2e9 100644 --- a/source/isaaclab_physx/test/assets/test_rigid_object_collection.py +++ b/source/isaaclab_physx/test/assets/test_rigid_object_collection.py @@ -84,7 +84,7 @@ def generate_cubes_scene( cube_config_dict = {} for i in range(num_cubes): cube_object_cfg = RigidObjectCfg( - prim_path=f"/World/Table_.*/Object_{i}", + prim_path=f"/World/Table_[^/]*/Object_{i}", spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 3 * i, height)), ) diff --git a/source/isaaclab_physx/test/assets/test_surface_gripper.py b/source/isaaclab_physx/test/assets/test_surface_gripper.py index 86382dff282a..e61c7c22a766 100644 --- a/source/isaaclab_physx/test/assets/test_surface_gripper.py +++ b/source/isaaclab_physx/test/assets/test_surface_gripper.py @@ -117,8 +117,8 @@ def generate_surface_gripper( # Create Top-level Xforms, one for each articulation for i in range(num_surface_grippers): sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) - articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_.*/Robot")) - surface_gripper_cfg = surface_gripper_cfg.replace(prim_path="/World/Env_.*/Robot/Gripper/SurfaceGripper") + articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_[^/]*/Robot")) + surface_gripper_cfg = surface_gripper_cfg.replace(prim_path="/World/Env_[^/]*/Robot/Gripper/SurfaceGripper") surface_gripper = SurfaceGripper(surface_gripper_cfg) return surface_gripper, articulation, translations @@ -126,7 +126,7 @@ def generate_surface_gripper( def generate_grippable_object(sim, num_grippable_objects: int): object_cfg = RigidObjectCfg( - prim_path="/World/Env_.*/Object", + prim_path="/World/Env_[^/]*/Object", spawn=sim_utils.CuboidCfg( size=(1.0, 1.0, 1.0), rigid_props=sim_utils.RigidBodyPropertiesCfg(), diff --git a/source/isaaclab_physx/test/sensors/check_contact_sensor.py b/source/isaaclab_physx/test/sensors/check_contact_sensor.py index e8892f02e5e2..ad2d79ed5a2c 100644 --- a/source/isaaclab_physx/test/sensors/check_contact_sensor.py +++ b/source/isaaclab_physx/test/sensors/check_contact_sensor.py @@ -94,12 +94,12 @@ def main(): # Design props design_scene() # Spawn things into the scene - robot_cfg = ANYMAL_C_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot_cfg = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") robot_cfg.spawn.activate_contact_sensors = True robot = Articulation(cfg=robot_cfg) # Contact sensor contact_sensor_cfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/Robot/.*_FOOT", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*_FOOT", track_air_time=True, track_contact_points=True, track_friction_forces=True, diff --git a/source/isaaclab_physx/test/sensors/check_pva_sensor.py b/source/isaaclab_physx/test/sensors/check_pva_sensor.py index e9bd00997fd9..b7df51fb23c7 100644 --- a/source/isaaclab_physx/test/sensors/check_pva_sensor.py +++ b/source/isaaclab_physx/test/sensors/check_pva_sensor.py @@ -93,7 +93,7 @@ def design_scene(sim: SimulationContext, num_envs: int = 2048) -> RigidObject: collision_props=sim_utils.CollisionPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), ), - prim_path="/World/envs/env_.*/ball", + prim_path="{ENV_REGEX_NS}/ball", init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 5.0)), ) balls = RigidObject(cfg) @@ -130,7 +130,7 @@ def main(): # Create a pva sensor pva_cfg = PvaCfg( - prim_path="/World/envs/env_.*/ball", + prim_path="{ENV_REGEX_NS}/ball", debug_vis=args_cli.visualize, ) # increase scale of the arrows for better visualization diff --git a/source/isaaclab_physx/test/sensors/test_contact_sensor.py b/source/isaaclab_physx/test/sensors/test_contact_sensor.py index 690cb644a2b2..637d864c01aa 100644 --- a/source/isaaclab_physx/test/sensors/test_contact_sensor.py +++ b/source/isaaclab_physx/test/sensors/test_contact_sensor.py @@ -512,7 +512,7 @@ def test_nested_rigid_body_hierarchy(setup_simulation, device, num_envs): _author_nested_chain(f"/World/envs/env_{env_id}/Robot") contact_sensor = ContactSensor( ContactSensorCfg( - prim_path="/World/envs/env_.*/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", track_pose=True, debug_vis=False, update_period=0.0, diff --git a/source/isaaclab_physx/test/sensors/test_frame_transformer.py b/source/isaaclab_physx/test/sensors/test_frame_transformer.py index f33d1878b389..adf36e382ff4 100644 --- a/source/isaaclab_physx/test/sensors/test_frame_transformer.py +++ b/source/isaaclab_physx/test/sensors/test_frame_transformer.py @@ -532,7 +532,7 @@ def test_frame_transformer_all_bodies(sim): prim_path="{ENV_REGEX_NS}/Robot/base", target_frames=[ FrameTransformerCfg.FrameCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", ), ], ) @@ -624,7 +624,7 @@ def test_sensor_print(sim): prim_path="{ENV_REGEX_NS}/Robot/base", target_frames=[ FrameTransformerCfg.FrameCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", ), ], ) diff --git a/source/isaaclab_physx/test/sim/test_cloner.py b/source/isaaclab_physx/test/sim/test_cloner.py index f1c278e39383..ad0677dd1c7c 100644 --- a/source/isaaclab_physx/test/sim/test_cloner.py +++ b/source/isaaclab_physx/test/sim/test_cloner.py @@ -633,7 +633,7 @@ def test_disabled_fabric_change_notifies_speedup_regression(): def _body(i: int) -> RigidObjectCfg: return RigidObjectCfg( - prim_path=f"/World/envs/env_.*/Body_{i}", + prim_path=f"/World/envs/env_[^/]+/Body_{i}", spawn=sim_utils.SphereCfg(radius=0.1, rigid_props=sim_utils.RigidBodyPropertiesCfg()), init_state=RigidObjectCfg.InitialStateCfg(pos=(0.3 * (i % 4), 0.3 * (i // 4), 0.5)), ) diff --git a/source/isaaclab_physx/test/sim/test_views_xform_prim_fabric.py b/source/isaaclab_physx/test/sim/test_views_xform_prim_fabric.py index 3cc92147827c..f7e20b56999a 100644 --- a/source/isaaclab_physx/test/sim/test_views_xform_prim_fabric.py +++ b/source/isaaclab_physx/test/sim/test_views_xform_prim_fabric.py @@ -107,7 +107,7 @@ def factory(num_envs: int, device: str) -> ViewBundle: sim_utils.create_prim(f"/World/Parent_{i}/Child", "Camera", translation=CHILD_OFFSET, stage=stage) sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.01, device=device, use_fabric=True)) - view = FrameView("/World/Parent_.*/Child", device=device) + view = FrameView("/World/Parent_[^/]*/Child", device=device) # close() is idempotent, so this is safe even for tests that close (or # tear down) themselves; it keeps views from being reaped by garbage # collection, which would log the missing-close() warning per test. @@ -368,7 +368,7 @@ def test_garbage_collection_removes_index_attributes_and_warns(device, caplog): sim_utils.create_prim(f"/World/Parent_{i}", "Xform", translation=PARENT_POS, stage=stage_usd) sim_utils.create_prim(f"/World/Parent_{i}/Child", "Camera", translation=CHILD_OFFSET, stage=stage_usd) sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.01, device=device, use_fabric=True)) - view = FrameView("/World/Parent_.*/Child", device=device) + view = FrameView("/World/Parent_[^/]*/Child", device=device) view.get_world_poses() child_attr = view._child_index_attr @@ -549,7 +549,7 @@ def _build_rotated_parent_view(device: str) -> "FrameView": ) sim_utils.create_prim("/World/Parent_0/Child", "Camera", translation=(0.0, 0.0, 0.0), stage=stage) sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.01, device=device, use_fabric=True)) - view = FrameView("/World/Parent_.*/Child", device=device) + view = FrameView("/World/Parent_[^/]*/Child", device=device) view.get_world_poses() # force Fabric init and USD→Fabric seed return view @@ -630,7 +630,7 @@ def test_initial_seed_with_scaled_parent(device): stage=stage, ) sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.01, device=device, use_fabric=True)) - view = FrameView("/World/Parent_.*/Child", device=device) + view = FrameView("/World/Parent_[^/]*/Child", device=device) world_pos, _ = view.get_world_poses() torch.testing.assert_close( @@ -674,8 +674,8 @@ def test_multi_view_writer_isolation(device): sim_utils.create_prim("/World/EnvB_0/ChildB", "Camera", translation=(0.2, 0.0, 0.0), stage=stage) sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.01, device=device, use_fabric=True)) - view_a = FrameView("/World/EnvA_.*/ChildA", device=device) - view_b = FrameView("/World/EnvB_.*/ChildB", device=device) + view_a = FrameView("/World/EnvA_[^/]*/ChildA", device=device) + view_b = FrameView("/World/EnvB_[^/]*/ChildB", device=device) expected_a0 = torch.tensor([[0.1, 0.0, 1.0]], dtype=torch.float32, device=device) expected_b0 = torch.tensor([[0.2, 0.0, 2.0]], dtype=torch.float32, device=device) @@ -829,7 +829,7 @@ def _build_two_child_view(device: str) -> "FrameView": ) sim_utils.create_prim(f"/World/Parent_{i}/Child", "Camera", translation=(0.0, 0.0, 0.0), stage=stage) sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.01, device=device, use_fabric=True)) - view = FrameView("/World/Parent_.*/Child", device=device) + view = FrameView("/World/Parent_[^/]*/Child", device=device) view.get_world_poses() # force init return view diff --git a/source/isaaclab_tasks/changelog.d/octi-prim-path-real-regex-matcher.rst b/source/isaaclab_tasks/changelog.d/octi-prim-path-real-regex-matcher.rst new file mode 100644 index 000000000000..98b0493cabd1 --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/octi-prim-path-real-regex-matcher.rst @@ -0,0 +1,6 @@ +Changed +^^^^^^^ + +* Changed prim path expressions to spell a single path segment ``[^/]`` rather than ``.``, so each + pattern selects what it selected before now that ``.`` matches ``/`` in + :func:`~isaaclab.sim.utils.find_matching_prims`. diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env_cfg.py index 630be7618d3f..fc55a9fc653a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env_cfg.py @@ -95,9 +95,9 @@ class AnymalCFlatEnvCfg(DirectRLEnvCfg): events: EventCfg = EventCfg() # robot - robot: ArticulationCfg = ANYMAL_C_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot: ArticulationCfg = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") contact_sensor: ContactSensorCfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/Robot/.*", history_length=3, update_period=0.005, track_air_time=True + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", history_length=3, update_period=0.005, track_air_time=True ) # reward scales @@ -148,7 +148,7 @@ class AnymalCRoughEnvCfg(AnymalCFlatEnvCfg): # we add a height scanner for perceptive locomotion height_scanner = RayCasterCfg( - prim_path="/World/envs/env_.*/Robot/base", + prim_path="{ENV_REGEX_NS}/Robot/base", offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), ray_alignment="yaw", pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]), diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/config/camera_config.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/config/camera_config.py index fbd9e9fe248c..0c7e5d0a113e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/config/camera_config.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/config/camera_config.py @@ -25,7 +25,7 @@ class CameraBaseCfg: @classmethod def get_camera_config( cls, - prim_path: str = "/World/envs/env_.*/Robot/d435_link/front_cam", + prim_path: str = "{ENV_REGEX_NS}/Robot/d435_link/front_cam", update_period: float = 0.02, height: int = 480, width: int = 640, @@ -96,7 +96,7 @@ def g1_front_camera(cls, **overrides) -> CameraCfg: def left_dex3_wrist_camera(cls, **overrides) -> CameraCfg: """left wrist camera configuration""" params = { - "prim_path": "/World/envs/env_.*/Robot/left_hand_camera_base_link/left_wrist_camera", + "prim_path": "{ENV_REGEX_NS}/Robot/left_hand_camera_base_link/left_wrist_camera", "height": 224, "width": 224, "update_period": 0.02, @@ -115,7 +115,7 @@ def left_dex3_wrist_camera(cls, **overrides) -> CameraCfg: def right_dex3_wrist_camera(cls, **overrides) -> CameraCfg: """right wrist camera configuration""" params = { - "prim_path": "/World/envs/env_.*/Robot/right_hand_camera_base_link/right_wrist_camera", + "prim_path": "{ENV_REGEX_NS}/Robot/right_hand_camera_base_link/right_wrist_camera", "height": 224, "width": 224, "update_period": 0.02, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env.py index d0df54be63d5..7864dc8c8e02 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env.py @@ -253,7 +253,7 @@ def _setup_scene(self): # spawn a usd file of a table into the scene cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd") cfg.func( - "/World/envs/env_.*/Table", cfg, translation=(0.55, 0.0, 0.0), orientation=(0.0, 0.0, 0.70711, 0.70711) + "/World/envs/env_[^/]+/Table", cfg, translation=(0.55, 0.0, 0.0), orientation=(0.0, 0.0, 0.70711, 0.70711) ) self._robot = Articulation(self.cfg.robot) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env_cfg.py index 6e83de596c50..40c528bdcbea 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env_cfg.py @@ -130,7 +130,7 @@ class AssemblyEnvCfg(DirectRLEnvCfg): scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=128, env_spacing=2.0) robot = ArticulationCfg( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", spawn=sim_utils.UsdFileCfg( usd_path=f"{ASSET_DIR}/franka_mimic.usd", # usd_path=f'{ASSET_DIR}/automate_franka.usd', @@ -199,5 +199,5 @@ class AssemblyEnvCfg(DirectRLEnvCfg): }, ) # contact_sensor: ContactSensorCfg = ContactSensorCfg( - # prim_path="/World/envs/env_.*/Robot/.*", update_period=0.0, history_length=1, debug_vis=True + # prim_path="{ENV_REGEX_NS}/Robot/[^/]*", update_period=0.0, history_length=1, debug_vis=True # ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_tasks_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_tasks_cfg.py index 635cedc27143..888bb3814e7e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_tasks_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_tasks_cfg.py @@ -206,7 +206,7 @@ class Insertion(AssemblyTask): fixed_asset: ArticulationCfg = ArticulationCfg( # fixed_asset: RigidObjectCfg = RigidObjectCfg( - prim_path="/World/envs/env_.*/FixedAsset", + prim_path="{ENV_REGEX_NS}/FixedAsset", spawn=sim_utils.UsdFileCfg( usd_path=f"{assembly_dir}{fixed_asset_cfg.usd_path}", activate_contact_sensors=True, @@ -240,7 +240,7 @@ class Insertion(AssemblyTask): ) # held_asset: ArticulationCfg = ArticulationCfg( held_asset: RigidObjectCfg = RigidObjectCfg( - prim_path="/World/envs/env_.*/HeldAsset", + prim_path="{ENV_REGEX_NS}/HeldAsset", spawn=sim_utils.UsdFileCfg( usd_path=f"{assembly_dir}{held_asset_cfg.usd_path}", activate_contact_sensors=True, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env.py index 1082758f065f..c115b8ae9b5d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env.py @@ -176,7 +176,7 @@ def _setup_scene(self): # spawn a usd file of a table into the scene cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd") cfg.func( - "/World/envs/env_.*/Table", cfg, translation=(0.55, 0.0, 0.0), orientation=(0.0, 0.0, 0.70711, 0.70711) + "/World/envs/env_[^/]+/Table", cfg, translation=(0.55, 0.0, 0.0), orientation=(0.0, 0.0, 0.70711, 0.70711) ) self._robot = Articulation(self.cfg.robot) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env_cfg.py index 2880902455f0..730d38685e3e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env_cfg.py @@ -131,7 +131,7 @@ class DisassemblyEnvCfg(DirectRLEnvCfg): scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=128, env_spacing=2.0) robot = ArticulationCfg( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", spawn=sim_utils.UsdFileCfg( usd_path=f"{ASSET_DIR}/franka_mimic.usd", activate_contact_sensors=True, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_tasks_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_tasks_cfg.py index 53307fd40341..74b9c174c4f4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_tasks_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_tasks_cfg.py @@ -156,7 +156,7 @@ class Extraction(DisassemblyTask): fixed_asset: ArticulationCfg = ArticulationCfg( # fixed_asset: RigidObjectCfg = RigidObjectCfg( - prim_path="/World/envs/env_.*/FixedAsset", + prim_path="{ENV_REGEX_NS}/FixedAsset", spawn=sim_utils.UsdFileCfg( usd_path=f"{assembly_dir}{fixed_asset_cfg.usd_path}", activate_contact_sensors=True, @@ -190,7 +190,7 @@ class Extraction(DisassemblyTask): ) # held_asset: ArticulationCfg = ArticulationCfg( held_asset: RigidObjectCfg = RigidObjectCfg( - prim_path="/World/envs/env_.*/HeldAsset", + prim_path="{ENV_REGEX_NS}/HeldAsset", spawn=sim_utils.UsdFileCfg( usd_path=f"{assembly_dir}{held_asset_cfg.usd_path}", activate_contact_sensors=True, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/cartpole_showcase/cartpole_camera/cartpole_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/cartpole_showcase/cartpole_camera/cartpole_camera_env_cfg.py index 48371def8349..940ffa21e5f3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/cartpole_showcase/cartpole_camera/cartpole_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/cartpole_showcase/cartpole_camera/cartpole_camera_env_cfg.py @@ -25,7 +25,7 @@ def get_tiled_camera_cfg(data_type: str, width: int = 100, height: int = 100) -> CameraCfg: return CameraCfg( - prim_path="/World/envs/env_.*/Camera", + prim_path="{ENV_REGEX_NS}/Camera", offset=CameraCfg.OffsetCfg(pos=(-5.0, 0.0, 2.0), rot=(0.0, 0.0, 0.0, 1.0), convention="world"), data_types=[data_type], spawn=sim_utils.PinholeCameraCfg( @@ -53,7 +53,7 @@ class CartpoleCameraEnvCfg(DirectRLEnvCfg): sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) # robot - robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") cart_dof_name = "slider_to_cart" pole_dof_name = "cart_to_pole" diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/walk_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/walk_env_cfg.py index b43a5186ca22..dd4cfb56e5d2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/walk_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/walk_env_cfg.py @@ -46,13 +46,13 @@ class DrLegsContactSensorCfg(PresetCfg): """Backend-specific foot contact sensor configuration.""" default = NewtonContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/foot_.*", + prim_path="{ENV_REGEX_NS}/Robot/foot_[^/]*", history_length=3, track_air_time=True, ) newton_kamino = default physx = PhysXContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/foot_.*", + prim_path="{ENV_REGEX_NS}/Robot/foot_[^/]*", history_length=3, track_air_time=True, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/navigation/config/arl_robot_1/navigation_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/navigation/config/arl_robot_1/navigation_env_cfg.py index 35d603746ad3..d220caf4465f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/navigation/config/arl_robot_1/navigation_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/navigation/config/arl_robot_1/navigation_env_cfg.py @@ -97,7 +97,7 @@ class ArlNavigationSceneCfg(InteractiveSceneCfg): ) contact_forces = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*", update_period=0.0, history_length=10, debug_vis=False, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env.py index e85b6835292f..2f115f09ea7e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env.py @@ -89,7 +89,7 @@ def _setup_scene(self): # spawn a usd file of a table into the scene cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd") cfg.func( - "/World/envs/env_.*/Table", cfg, translation=(0.55, 0.0, 0.0), orientation=(0.0, 0.0, 0.70711, 0.70711) + "/World/envs/env_[^/]+/Table", cfg, translation=(0.55, 0.0, 0.0), orientation=(0.0, 0.0, 0.70711, 0.70711) ) self._robot = Articulation(self.cfg.robot) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env_cfg.py index 159abaa907e5..9a82fafbcbe7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env_cfg.py @@ -133,7 +133,7 @@ class FactoryEnvCfg(DirectRLEnvCfg): scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=128, env_spacing=2.0, clone_in_fabric=True) robot = ArticulationCfg( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", spawn=sim_utils.UsdFileCfg( usd_path=f"{ASSET_DIR}/franka_mimic.usd", activate_contact_sensors=True, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_tasks_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_tasks_cfg.py index 4841f58d161b..705ecc1b1845 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_tasks_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_tasks_cfg.py @@ -132,7 +132,7 @@ class PegInsert(FactoryTask): engage_threshold: float = 0.9 fixed_asset: ArticulationCfg = ArticulationCfg( - prim_path="/World/envs/env_.*/FixedAsset", + prim_path="{ENV_REGEX_NS}/FixedAsset", spawn=sim_utils.UsdFileCfg( usd_path=fixed_asset_cfg.usd_path, activate_contact_sensors=True, @@ -157,7 +157,7 @@ class PegInsert(FactoryTask): actuators={}, ) held_asset: ArticulationCfg = ArticulationCfg( - prim_path="/World/envs/env_.*/HeldAsset", + prim_path="{ENV_REGEX_NS}/HeldAsset", spawn=sim_utils.UsdFileCfg( usd_path=held_asset_cfg.usd_path, activate_contact_sensors=True, @@ -212,7 +212,7 @@ class GearMesh(FactoryTask): large_gear_usd = f"{ASSET_DIR}/factory_gear_large.usd" small_gear_cfg: ArticulationCfg = ArticulationCfg( - prim_path="/World/envs/env_.*/SmallGearAsset", + prim_path="{ENV_REGEX_NS}/SmallGearAsset", spawn=sim_utils.UsdFileCfg( usd_path=small_gear_usd, activate_contact_sensors=True, @@ -238,7 +238,7 @@ class GearMesh(FactoryTask): ) large_gear_cfg: ArticulationCfg = ArticulationCfg( - prim_path="/World/envs/env_.*/LargeGearAsset", + prim_path="{ENV_REGEX_NS}/LargeGearAsset", spawn=sim_utils.UsdFileCfg( usd_path=large_gear_usd, activate_contact_sensors=True, @@ -290,7 +290,7 @@ class GearMesh(FactoryTask): engage_threshold: float = 0.9 fixed_asset: ArticulationCfg = ArticulationCfg( - prim_path="/World/envs/env_.*/FixedAsset", + prim_path="{ENV_REGEX_NS}/FixedAsset", spawn=sim_utils.UsdFileCfg( usd_path=fixed_asset_cfg.usd_path, activate_contact_sensors=True, @@ -315,7 +315,7 @@ class GearMesh(FactoryTask): actuators={}, ) held_asset: ArticulationCfg = ArticulationCfg( - prim_path="/World/envs/env_.*/HeldAsset", + prim_path="{ENV_REGEX_NS}/HeldAsset", spawn=sim_utils.UsdFileCfg( usd_path=held_asset_cfg.usd_path, activate_contact_sensors=True, @@ -396,7 +396,7 @@ class NutThread(FactoryTask): keypoint_scale: float = 0.05 fixed_asset: ArticulationCfg = ArticulationCfg( - prim_path="/World/envs/env_.*/FixedAsset", + prim_path="{ENV_REGEX_NS}/FixedAsset", spawn=sim_utils.UsdFileCfg( usd_path=fixed_asset_cfg.usd_path, activate_contact_sensors=True, @@ -421,7 +421,7 @@ class NutThread(FactoryTask): actuators={}, ) held_asset: ArticulationCfg = ArticulationCfg( - prim_path="/World/envs/env_.*/HeldAsset", + prim_path="{ENV_REGEX_NS}/HeldAsset", spawn=sim_utils.UsdFileCfg( usd_path=held_asset_cfg.usd_path, activate_contact_sensors=True, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env_cfg.py index 9901d45454f0..36ac3bd3637e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env_cfg.py @@ -61,7 +61,7 @@ class HumanoidAmpEnvCfg(DirectRLEnvCfg): scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=4096, env_spacing=10.0, replicate_physics=True) # robot - robot: ArticulationCfg = HUMANOID_28_CFG.replace(prim_path="/World/envs/env_.*/Robot").replace( + robot: ArticulationCfg = HUMANOID_28_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot").replace( actuators={ "body": ImplicitActuatorCfg( joint_names_expr=[".*"], diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/fixed_base_upper_body_ik_g1_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/fixed_base_upper_body_ik_g1_env_cfg.py index 835e30d44567..71161a10af48 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/fixed_base_upper_body_ik_g1_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/fixed_base_upper_body_ik_g1_env_cfg.py @@ -265,12 +265,12 @@ class FixedBaseUpperBodyIKG1SceneCfg(InteractiveSceneCfg): # haptics (see HapticFeedbackCfg below). Requires activate_contact_sensors # on the robot spawn, enabled in the env __post_init__. left_hand_contact = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/left_hand_.*_link", + prim_path="{ENV_REGEX_NS}/Robot/left_hand_[^/]*_link", update_period=0.0, history_length=3, ) right_hand_contact = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/right_hand_.*_link", + prim_path="{ENV_REGEX_NS}/Robot/right_hand_[^/]*_link", update_period=0.0, history_length=3, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/locomanipulation_g1_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/locomanipulation_g1_env_cfg.py index b748cf8ac81b..112946a225ff 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/locomanipulation_g1_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/locomanipulation_g1_env_cfg.py @@ -313,12 +313,12 @@ class LocomanipulationG1SceneCfg(InteractiveSceneCfg): # haptics (see HapticFeedbackCfg below). Requires activate_contact_sensors # on the robot spawn, enabled in the env __post_init__. left_hand_contact = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/left_hand_.*_link", + prim_path="{ENV_REGEX_NS}/Robot/left_hand_[^/]*_link", update_period=0.0, history_length=3, ) right_hand_contact = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/right_hand_.*_link", + prim_path="{ENV_REGEX_NS}/Robot/right_hand_[^/]*_link", update_period=0.0, history_length=3, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_env_cfg.py index 4f7b7e5ee7b0..c89ec4d9b3e4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_env_cfg.py @@ -339,13 +339,13 @@ class ObjectTableSceneCfg(InteractiveSceneCfg): # Contact reporting is already enabled on the robot by GR1T2_HIGH_PD_CFG # (``spawn.activate_contact_sensors=True``), so it is not set again here. left_hand_contact = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*L_(index|middle|ring|pinky|thumb).*_link", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*L_(index|middle|ring|pinky|thumb)[^/]*_link", filter_prim_paths_expr=[_STEERING_WHEEL_BODY], update_period=0.0, history_length=3, ) right_hand_contact = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/.*R_(index|middle|ring|pinky|thumb).*_link", + prim_path="{ENV_REGEX_NS}/Robot/[^/]*R_(index|middle|ring|pinky|thumb)[^/]*_link", filter_prim_paths_expr=[_STEERING_WHEEL_BODY], update_period=0.0, history_length=3, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py index e737e7b123da..6d3087cc01c7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py @@ -379,7 +379,7 @@ def __post_init__(self): # add contact force sensor for grasped checking self.scene.contact_grasp = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/right_.*_Pad_Link", + prim_path="{ENV_REGEX_NS}/Robot/right_[^/]*_Pad_Link", update_period=0.05, history_length=6, debug_vis=True, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py index 656155a22552..bf59f79ea00d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py @@ -183,7 +183,7 @@ def __post_init__(self): # add contact force sensor for grasped checking self.scene.contact_grasp = ContactSensorCfg( - prim_path="{ENV_REGEX_NS}/Robot/right_.*_Pad_Link", + prim_path="{ENV_REGEX_NS}/Robot/right_[^/]*_Pad_Link", update_period=0.0, history_length=6, debug_vis=True, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py index aaf4c2101192..3f3ed473d55f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py @@ -24,7 +24,7 @@ class CartpoleTiledCameraCfg(PresetCfg): @configclass class BaseCartpoleTiledCameraCfg(CameraCfg): - prim_path: str = "/World/envs/env_.*/Camera" + prim_path: str = "{ENV_REGEX_NS}/Camera" offset: CameraCfg.OffsetCfg = CameraCfg.OffsetCfg( pos=(-5.0, 0.0, 2.0), rot=(0.0, 0.0, 0.0, 1.0), convention="world" ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py index 751fc6d23ac3..8206bb303958 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py @@ -67,7 +67,7 @@ class CartpoleEnvCfg(DirectRLEnvCfg): sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation, physics=CartpolePhysicsCfg()) # robot - robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") cart_dof_name = "slider_to_cart" pole_dof_name = "cart_to_pole" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py index 6f97da4ac278..fddcefc69c0d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py @@ -36,7 +36,7 @@ class CartpoleTiledCameraCfg(PresetCfg): @configclass class BaseCartpoleTiledCameraCfg(CameraCfg): - prim_path: str = "/World/envs/env_.*/Camera" + prim_path: str = "{ENV_REGEX_NS}/Camera" offset: CameraCfg.OffsetCfg = CameraCfg.OffsetCfg( pos=(-5.0, 0.0, 2.0), rot=(0.0, 0.0, 0.0, 1.0), convention="world" ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py index b9edaeb1af28..5ca49da11404 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py @@ -185,12 +185,12 @@ def _shadow_hand_cfg( # Per-hand presets shared by the Direct environment and the manager scene. RIGHT_HAND_CFG = _shadow_hand_cfg( - prim_path="/World/envs/env_.*/RightRobot", + prim_path="{ENV_REGEX_NS}/RightRobot", init_pos=(0.0, 0.0, 0.5), init_rot=(0.0, 0.0, 0.0, 1.0), ) LEFT_HAND_CFG = _shadow_hand_cfg( - prim_path="/World/envs/env_.*/LeftRobot", + prim_path="{ENV_REGEX_NS}/LeftRobot", init_pos=(0.0, -1.0, 0.5), init_rot=(0.0, 0.0, 1.0, 0.0), ) @@ -209,7 +209,7 @@ class ObjectCfg(PresetCfg): """ physx = RigidObjectCfg( - prim_path="/World/envs/env_.*/object", + prim_path="{ENV_REGEX_NS}/object", spawn=sim_utils.SphereCfg( radius=OBJECT_RADIUS, visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 1.0, 0.0)), @@ -230,7 +230,7 @@ class ObjectCfg(PresetCfg): init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.39, 0.54), rot=(0.0, 0.0, 0.0, 1.0)), ) newton_mjwarp = RigidObjectCfg( - prim_path="/World/envs/env_.*/object", + prim_path="{ENV_REGEX_NS}/object", spawn=sim_utils.SphereCfg( radius=OBJECT_RADIUS, visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 1.0, 0.0)), diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py index 42909ae56537..a5c22a356016 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py @@ -60,7 +60,7 @@ class PhysicsCfg(PresetCfg): ls_iterations=20, integrator="implicitfast", ), - bodies=[r"/World/envs/env_.*/Robot", r"/World/envs/env_.*/Support(Neg|Pos)Y"], + bodies=[r"/World/envs/env_[^/]+/Robot", r"/World/envs/env_[^/]+/Support(Neg|Pos)Y"], ), CouplerEntryCfg( name="soft", @@ -74,9 +74,9 @@ class PhysicsCfg(PresetCfg): source="rigid", destination="soft", bodies=[ - r"/World/envs/env_.*/Robot/Geometry/.*panda_hand", - r"/World/envs/env_.*/Robot/Geometry/.*panda_(left|right)finger", - r"/World/envs/env_.*/Support(Neg|Pos)Y", + r"/World/envs/env_[^/]+/Robot/Geometry/.*panda_hand", + r"/World/envs/env_[^/]+/Robot/Geometry/.*panda_(left|right)finger", + r"/World/envs/env_[^/]+/Support(Neg|Pos)Y", ], collide_interval=1, collision_pipeline=NewtonCollisionPipelineCfg( diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py index d463a11435c5..1fe189d3f140 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py @@ -83,7 +83,7 @@ FRANKA_CAMERA_CFG = CameraCfg( - prim_path="/World/envs/env_.*/Camera", + prim_path="{ENV_REGEX_NS}/Camera", offset=CameraCfg.OffsetCfg( pos=(0.85, -0.55, 0.42), rot=(0.5080, 0.2114, 0.318, 0.7720), @@ -153,7 +153,7 @@ class PhysicsCfg(PresetCfg): ls_iterations=20, integrator="implicitfast", ), - bodies=[r"/World/envs/env_.*/Robot"], + bodies=[r"/World/envs/env_[^/]+/Robot"], ), CouplerEntryCfg( name="soft", @@ -167,8 +167,8 @@ class PhysicsCfg(PresetCfg): source="rigid", destination="soft", bodies=[ - r"/World/envs/env_.*/Robot/Geometry/.*panda_hand", - r"/World/envs/env_.*/Robot/Geometry/.*panda_(left|right)finger", + r"/World/envs/env_[^/]+/Robot/Geometry/.*panda_hand", + r"/World/envs/env_[^/]+/Robot/Geometry/.*panda_(left|right)finger", ], collide_interval=1, collision_pipeline=NewtonCollisionPipelineCfg( @@ -202,12 +202,12 @@ class _FrankaSoftSceneCfg(InteractiveSceneCfg): # end-effector frame for reward shaping ee_frame: FrameTransformerCfg = FrameTransformerCfg( - prim_path="/World/envs/env_.*/Robot/Geometry/panda_link0", + prim_path="{ENV_REGEX_NS}/Robot/Geometry/panda_link0", debug_vis=False, target_frames=[ FrameTransformerCfg.FrameCfg( prim_path=( - "/World/envs/env_.*/Robot/Geometry/panda_link0/panda_link1/panda_link2/panda_link3/" + "{ENV_REGEX_NS}/Robot/Geometry/panda_link0/panda_link1/panda_link2/panda_link3/" "panda_link4/panda_link5/panda_link6/panda_link7/panda_hand" ), name="end_effector", diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py index cf749386ddce..4f51aaeced92 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py @@ -23,7 +23,7 @@ BASE_CAMERA_CFG = CameraCfg( - prim_path="/World/envs/env_.*/Camera", + prim_path="{ENV_REGEX_NS}/Camera", offset=CameraCfg.OffsetCfg( pos=(0.57, -0.8, 0.5), rot=(0.6124, 0.3536, 0.3536, 0.6124), @@ -37,7 +37,7 @@ ) WRIST_CAMERA_CFG = CameraCfg( - prim_path="/World/envs/env_.*/Robot/ee_link/palm_link/Camera", + prim_path="{ENV_REGEX_NS}/Robot/ee_link/palm_link/Camera", offset=CameraCfg.OffsetCfg( pos=(0.038, -0.38, -0.18), rot=(0.641, 0.641, -0.299, 0.299), @@ -54,7 +54,7 @@ RAYCASTER_CAMERA_MESH_PRIM_PATHS = [ MultiMeshRayCasterCameraCfg.RaycastTargetCfg( - prim_expr="/World/envs/env_.*/table", + prim_expr="{ENV_REGEX_NS}/table", track_mesh_transforms=False, ), MultiMeshRayCasterCameraCfg.RaycastTargetCfg( @@ -62,17 +62,17 @@ track_mesh_transforms=False, ), MultiMeshRayCasterCameraCfg.RaycastTargetCfg( - prim_expr="/World/envs/env_.*/Object", + prim_expr="{ENV_REGEX_NS}/Object", track_mesh_transforms=True, ), MultiMeshRayCasterCameraCfg.RaycastTargetCfg( - prim_expr="/World/envs/env_.*/Robot/.*/visuals", + prim_expr="{ENV_REGEX_NS}/Robot/[^/]*/visuals", track_mesh_transforms=True, ), ] BASE_RAYCASTER_CAMERA_CFG = MultiMeshRayCasterCameraCfg( - prim_path="/World/envs/env_.*/Camera", + prim_path="{ENV_REGEX_NS}/Camera", offset=MultiMeshRayCasterCameraCfg.OffsetCfg( pos=(0.57, -0.8, 0.5), rot=(0.6124, 0.3536, 0.3536, 0.6124), @@ -85,7 +85,7 @@ ) WRIST_RAYCASTER_CAMERA_CFG = MultiMeshRayCasterCameraCfg( - prim_path="/World/envs/env_.*/Robot/ee_link/palm_link/Camera", + prim_path="{ENV_REGEX_NS}/Robot/ee_link/palm_link/Camera", offset=MultiMeshRayCasterCameraCfg.OffsetCfg( pos=(0.038, -0.38, -0.18), rot=(0.641, 0.641, -0.299, 0.299), diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py index 9b482365e4ee..a7f0c1d1e42d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py @@ -86,12 +86,12 @@ class AntEnvCfg(DirectRLEnvCfg): ) # robot - robot: ArticulationCfg = ANT_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot: ArticulationCfg = ANT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") # effort scale per joint, keyed by joint name expression joint_gears: dict[str, float] = {".*": 15.0} # sensors - joint_wrench: JointWrenchSensorCfg = JointWrenchSensorCfg(prim_path="/World/envs/env_.*/Robot") + joint_wrench: JointWrenchSensorCfg = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") feet_body_names: list[str] = ["front_left_foot", "front_right_foot", "left_back_foot", "right_back_foot"] # walk target, relative to the environment origin diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py index 19fe73f607fc..bc0929b86d1e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py @@ -78,7 +78,7 @@ class HumanoidEnvCfg(DirectRLEnvCfg): ) # robot - robot: ArticulationCfg = HUMANOID_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot: ArticulationCfg = HUMANOID_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") # effort scale per joint, keyed by joint name expression joint_gears: dict[str, float] = { @@ -94,7 +94,7 @@ class HumanoidEnvCfg(DirectRLEnvCfg): } # sensors - joint_wrench: JointWrenchSensorCfg = JointWrenchSensorCfg(prim_path="/World/envs/env_.*/Robot") + joint_wrench: JointWrenchSensorCfg = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") feet_body_names: list[str] = ["left_foot", "right_foot"] # walk target, relative to the environment origin diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_env_cfg.py index b5194da53279..2ea05825664c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_env_cfg.py @@ -30,7 +30,7 @@ class PendulumEnvCfg(DirectMARLEnvCfg): sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) # robot - robot_cfg: ArticulationCfg = CART_DOUBLE_PENDULUM_CFG.replace(prim_path="/World/envs/env_.*/Robot") + robot_cfg: ArticulationCfg = CART_DOUBLE_PENDULUM_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") cart_dof_name = "slider_to_cart" pole_dof_name = "cart_to_pole" pendulum_dof_name = "pole_to_pendulum" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py index 36cbd5b51ac6..abd9fb678ebe 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py @@ -24,11 +24,10 @@ from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG -ALLEGRO_HAND_ROBOT_CFG = ALLEGRO_HAND_CFG.replace(prim_path="/World/envs/env_.*/Robot") - +ALLEGRO_HAND_ROBOT_CFG = ALLEGRO_HAND_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") CUBE_CFG = RigidObjectCfg( - prim_path="/World/envs/env_.*/object", + prim_path="{ENV_REGEX_NS}/object", spawn=sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", rigid_props=sim_utils.RigidBodyPropertiesCfg( diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py index 74c4f0f3db77..604d7b5f2887 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py @@ -154,7 +154,7 @@ class ShadowHandManagerEventPresetCfg(PresetCfg): @configclass class ShadowHandRobotCfg(PresetCfg): - physx = SHADOW_HAND_CFG.replace(prim_path="/World/envs/env_.*/Robot").replace( + physx = SHADOW_HAND_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot").replace( init_state=ArticulationCfg.InitialStateCfg( pos=(0.0, 0.0, 0.5), rot=(0.0, 0.0, 0.0, 1.0), @@ -165,9 +165,9 @@ class ShadowHandRobotCfg(PresetCfg): # Newton robot lives in the asset (see isaaclab_assets.robots.shadow_hand); reorient # uses its default gains. The handover task consumes the same asset cfg and overrides # only the finger gains. - newton_mjwarp = SHADOW_HAND_NEWTON_CFG.replace(prim_path="/World/envs/env_.*/Robot") + newton_mjwarp = SHADOW_HAND_NEWTON_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") ovphysx = SHADOW_HAND_CFG.replace( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", # OVPhysX does not expose the fixed-tendon runtime API, so spawn without tendon overrides. spawn=SHADOW_HAND_CFG.spawn.replace(fixed_tendons_props=None), init_state=ArticulationCfg.InitialStateCfg( @@ -180,7 +180,7 @@ class ShadowHandRobotCfg(PresetCfg): CUBE_CFG = RigidObjectCfg( - prim_path="/World/envs/env_.*/object", + prim_path="{ENV_REGEX_NS}/object", spawn=sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", rigid_props=sim_utils.RigidBodyPropertiesCfg( diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py index ceeaa6a5d8c7..da8416cfc452 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py @@ -69,7 +69,7 @@ class _ShadowHandBaseTiledCameraCfg(CameraCfg): still be selected via the ``presets`` CLI argument. """ - prim_path: str = "/World/envs/env_.*/Camera" + prim_path: str = "{ENV_REGEX_NS}/Camera" offset: CameraCfg.OffsetCfg = CameraCfg.OffsetCfg( pos=(0, -0.35, 1.0), rot=(0.0, 0.7071, 0.0, 0.7071), convention="world" ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py index 4133c7d3e8f9..446f170d9bb4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py @@ -110,7 +110,7 @@ class MySceneCfg(InteractiveSceneCfg): mesh_prim_paths=["/World/ground"], global_world_only=True, ) - contact_forces = ContactSensorCfg(prim_path="{ENV_REGEX_NS}/Robot/.*", history_length=3, track_air_time=True) + contact_forces = ContactSensorCfg(prim_path="{ENV_REGEX_NS}/Robot/[^/]*", history_length=3, track_air_time=True) # lights sky_light = AssetBaseCfg( prim_path="/World/skyLight", diff --git a/source/isaaclab_tasks/test/rendering_test_utils.py b/source/isaaclab_tasks/test/rendering_test_utils.py index 654f0bdaea6a..0531b6d2b8c4 100644 --- a/source/isaaclab_tasks/test/rendering_test_utils.py +++ b/source/isaaclab_tasks/test/rendering_test_utils.py @@ -1477,7 +1477,7 @@ class _CartpoleTiledCameraTestCfg(CartpoleTiledCameraCfg): @configclass class _BaseCartpoleCameraEnvTestCfg(CartpoleCameraEnvCfg.BaseCartpoleCameraEnvCfg): robot_cfg = CARTPOLE_CFG.replace( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", spawn=CARTPOLE_CFG.spawn.replace(semantic_tags=[("class", "cartpole")]), ) diff --git a/source/isaaclab_visualizers/changelog.d/octi-prim-path-real-regex-matcher.skip b/source/isaaclab_visualizers/changelog.d/octi-prim-path-real-regex-matcher.skip new file mode 100644 index 000000000000..ba04fc637ca0 --- /dev/null +++ b/source/isaaclab_visualizers/changelog.d/octi-prim-path-real-regex-matcher.skip @@ -0,0 +1 @@ +Test-only update to use the segment-safe environment expression; no visualizer behavior changed. diff --git a/source/isaaclab_visualizers/test/test_newton_adapter.py b/source/isaaclab_visualizers/test/test_newton_adapter.py index 80010d3b1421..a376013fe818 100644 --- a/source/isaaclab_visualizers/test/test_newton_adapter.py +++ b/source/isaaclab_visualizers/test/test_newton_adapter.py @@ -216,7 +216,7 @@ def test_newton_visualizer_auto_creates_streaming_camera_when_scene_camera_exist existing_camera = SimpleNamespace( _view=SimpleNamespace(count=4), cfg=SimpleNamespace( - prim_path="/World/envs/env_.*/Camera", + prim_path="/World/envs/env_[^/]+/Camera", renderer_cfg=SimpleNamespace(renderer_type="newton_warp"), ), )