Skip to content

Commit 5681bb8

Browse files
committed
Match prim path expressions as whole-path regular expressions
find_matching_prims matched each '/'-separated token against prim names at that depth, while find_first_matching_prim compiled the same argument as one regex over the full path. Both take a parameter named prim_path_regex and document the same contract, so an expression such as '/World/Robot/.*link2' returned nothing from the first and a depth-2 prim from the second. Match the whole path in both, and have find_first_matching_prim delegate. Whole-path matching does not imply traversing the whole stage: the expression's longest literal prefix gives the traversal root, and when no wildcard can span a '/', the separator count bounds the descent. On a 93k-prim stage a per-environment query visits about 1k prims. Two changes follow, because '.*' had been carrying structure rather than meaning what regex says it means. make_clone_plan derived an asset's destination template by substituting '.*' for '{}' in the configured prim path. str.replace is positionally blind, so a second wildcard below the environment slot produced a template with two slots that raised IndexError when formatted; the environment root was hardcoded besides, so a non-default namespace excluded every asset from the plan. Take the slot from the environment template instead, which CloneCfg now carries directly: a template always yields a regex, whereas recovering a template from a regex requires guessing which part of the text is the wildcard. With '.*' free to mean what regex says, the environment namespace spells its slot '[^/]+' so it cannot match across a '/' and select a prim nested deeper under an environment. path.match accepts a character class in the clone slot for that, since the text '[^/]+' contains a '/' and so cannot match the one-segment alternative.
1 parent 3c9d970 commit 5681bb8

151 files changed

Lines changed: 662 additions & 437 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
Added
2+
^^^^^
3+
4+
* Added :attr:`~isaaclab.cloner.CloneCfg.clone_template` for the replicated environment prim path,
5+
with ``{}`` marking the environment index. It replaces ``CloneCfg.clone_regex``, whose value is
6+
now ``clone_template.format(".*")``.
7+
* Added an ``env_template`` argument to :func:`~isaaclab.cloner.make_clone_plan` and
8+
:class:`~isaaclab.cloner.ReplicateSession`.
9+
* Added :func:`~isaaclab.sim.utils.path_expr_to_glob` and
10+
:func:`~isaaclab.sim.utils.split_path_expr`, for converting a prim path expression to the glob
11+
the physics engines accept and for splitting one without cutting a character class in half.
12+
13+
Changed
14+
^^^^^^^
15+
16+
* **Breaking:** Changed :func:`~isaaclab.sim.utils.find_matching_prims` to match the whole prim
17+
path as a plain regular expression instead of one token per path segment. ``.`` now matches
18+
``/``, so ``/World/Robot/.*`` selects descendants at any depth; use ``[^/]+`` for a single
19+
segment.
20+
* Changed :func:`~isaaclab.sim.utils.find_first_matching_prim` to delegate to
21+
:func:`~isaaclab.sim.utils.find_matching_prims`, so both read an expression the same way.
22+
* Changed the environment namespace to spell its slot ``[^/]+`` rather than ``.*``, so
23+
``{ENV_REGEX_NS}/Robot`` no longer also selects a ``Robot`` nested deeper under an environment.
24+
* Changed :func:`~isaaclab.cloner.path.match` to accept a character class in the clone slot, so a
25+
segment-safe namespace resolves against a destination template.
26+
27+
* Changed prim path expressions throughout the repository to spell a single path segment
28+
``[^/]`` rather than ``.``, so each pattern selects what it selected before now that ``.``
29+
matches ``/``.
30+
31+
Removed
32+
^^^^^^^
33+
34+
* Removed the legacy glob-wildcard rewrite from prim path expressions. A bare ``*`` is a regular
35+
expression quantifier and is no longer rewritten to ``.*``; the rewrite could not tell a glob
36+
star from a quantifier and corrupted ``[^/]*`` into ``[^/].*``. Patterns relying on ``*`` as a
37+
standalone wildcard should spell it ``.*`` (any depth) or ``[^/]*`` (one path segment).
38+
39+
Fixed
40+
^^^^^
41+
42+
* Fixed :func:`~isaaclab.cloner.make_clone_plan` raising ``IndexError`` for a prim path holding
43+
more than one wildcard, and ignoring a non-default environment namespace.
44+
* Fixed :class:`~isaaclab.sensors.MultiMeshRayCaster` expanding ``{ENV_REGEX_NS}`` with a
45+
hardcoded namespace instead of the shared default.

source/isaaclab/isaaclab/cloner/clone_plan.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import itertools
2424
import math
25+
import re
2526
from collections.abc import Callable, Iterable, Sequence
2627
from dataclasses import dataclass, field
2728
from typing import Any
@@ -30,7 +31,7 @@
3031

3132
import isaaclab.sim as sim_utils
3233

33-
from .cloner_cfg import InclusionSet
34+
from .cloner_cfg import DEFAULT_ENV_TEMPLATE, InclusionSet
3435
from .cloner_strategies import sequential
3536
from .path import split
3637

@@ -224,6 +225,7 @@ def make_clone_plan(
224225
*,
225226
clone_strategy: Callable = sequential,
226227
valid_set: torch.Tensor | None = None,
228+
env_template: str = DEFAULT_ENV_TEMPLATE,
227229
) -> ClonePlan:
228230
"""Build a :class:`ClonePlan` from asset cfgs.
229231
@@ -266,21 +268,35 @@ def set_spawn_paths(spawn_cfg: Any, paths: list[str | None]) -> None:
266268
raise ValueError("Single spawner expects exactly one planned source path.")
267269
spawn_cfg.spawn_path = active[0]
268270

269-
env_root_marker = "/World/envs/"
270-
env_template = "/World/envs/env_{}"
271+
# the env root sits at a known depth, so the destination is the env template plus whatever the
272+
# cfg authored below it -- no need to find the clone slot by substituting in the cfg's own path
273+
env_prefix, _ = split(env_template)
274+
275+
def env_destination(prim_path: str) -> str | None:
276+
"""Rebase an env-scoped cfg path onto the env template, or None when it is global.
277+
278+
The cfg may name one environment (``env_0``) or all of them (``env_.*``, ``env_[^/]+``);
279+
only the text before the slot is fixed, so that is what identifies an env-scoped path.
280+
"""
281+
if not prim_path.startswith(env_prefix):
282+
return None
283+
# blank out character classes so a '/' inside one does not read as a separator; the
284+
# replacement is the same length, so the index carries back to the original string
285+
masked = re.sub(r"\[\^?[^]]*\]", lambda match: "\x00" * len(match.group()), prim_path)
286+
cut = masked.find("/", len(env_prefix))
287+
return env_template if cut == -1 else env_template + prim_path[cut:]
271288

272289
# 1) Build per-group records: (cfg, spawn_cfg, destination_template, num_variants).
273290
groups: list[tuple[Any, Any, str, int]] = []
274291
for cfg in cfgs:
275292
if not hasattr(cfg, "prim_path") or not hasattr(cfg, "spawn") or cfg.spawn is None:
276293
continue
277294
prim_path = cfg.prim_path
278-
if env_root_marker not in prim_path:
295+
if (destination := env_destination(prim_path)) is None:
279296
continue
280297
count = num_spawn_variants(cfg.spawn)
281298
if count <= 0:
282299
raise ValueError(f"Spawner at '{prim_path}' must have at least one variant.")
283-
destination = prim_path.replace(".*", "{}")
284300
groups.append((cfg, cfg.spawn, destination, count))
285301

286302
env_ids = torch.arange(num_clones, dtype=torch.long, device=device)

source/isaaclab/isaaclab/cloner/cloner_cfg.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
from .cloner_strategies import sequential
1414

15+
DEFAULT_ENV_TEMPLATE = "/World/envs/env_{}"
16+
"""Default path template for a replicated env prim; ``{}`` marks the environment index."""
17+
1518

1619
@configclass
1720
class InclusionSet:
@@ -46,8 +49,12 @@ class CloneCfg:
4649
device: str = "cpu"
4750
"""Torch device on which mapping buffers are allocated."""
4851

49-
clone_regex: str = "/World/envs/env_.*"
50-
"""Regex matching every replicated env prim. Used to expand ``{ENV_REGEX_NS}`` cfg macros."""
52+
clone_template: str = DEFAULT_ENV_TEMPLATE
53+
"""Path template for every replicated env prim, where ``{}`` is the environment index.
54+
55+
The regex form used to expand ``{ENV_REGEX_NS}`` cfg macros is
56+
``clone_template.format("[^/]+")``, which confines the slot to one path segment.
57+
"""
5158

5259
replicate_physics: bool = True
5360
"""Whether physics replication clones each environment. Default is True.

source/isaaclab/isaaclab/cloner/path.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ def split(template: str) -> tuple[str, str]:
5656
def match(path_expr: str, template: str) -> TemplateMatch | None:
5757
"""Match ``path_expr`` against a destination template, capturing the instance slot.
5858
59-
The ``"{}"`` slot matches one path segment's worth of text, whether a concrete id (``3``)
60-
or a wildcard (``.*``). Recovering that text is the only way to tell which instance a
59+
The ``"{}"`` slot matches one path segment's worth of text: a concrete id (``3``) or a
60+
wildcard standing for one segment (``.*``, ``[^/]+``). Recovering that text is the only way to tell which instance a
6161
concrete clone path belongs to without slicing the string by hand.
6262
6363
Args:
@@ -73,7 +73,10 @@ def match(path_expr: str, template: str) -> TemplateMatch | None:
7373
TemplateMatch(instance='3', suffix='/base')
7474
"""
7575
prefix, template_suffix = split(template)
76-
pattern = re.compile(re.escape(prefix) + r"([^/]+)" + re.escape(template_suffix))
76+
# the slot holds one segment's worth of text: a concrete id, or a wildcard standing for one.
77+
# A segment-safe wildcard is written as a character class, whose text contains a '/' that is
78+
# not a separator, so it is matched as a class rather than by the one-segment alternative.
79+
pattern = re.compile(re.escape(prefix) + r"(\[\^?[^]]*\][*+?]?|[^/]+)" + re.escape(template_suffix))
7780
matched = pattern.match(path_expr)
7881
if matched is None:
7982
return None

source/isaaclab/isaaclab/cloner/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def iter_sources(plan: ClonePlan, path_expr: str) -> Iterator[tuple[str, str, st
214214
Example:
215215
For a row with prototype root ``"/World/source/Robot"``, destination template
216216
``"/World/scenes/{}/Robot"`` and env ids ``(0, 2)``, querying
217-
``"/World/scenes/.*/Robot/base"`` yields ``("/World/source/Robot",
217+
``"/World/scenes/[^/]*/Robot/base"`` yields ``("/World/source/Robot",
218218
"/World/scenes/{}/Robot", "/World/source/Robot/base", (0, 2))``.
219219
220220
Args:

source/isaaclab/isaaclab/cloner/replicate_session.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from isaaclab.utils.version import has_kit
1717

1818
from .clone_plan import make_clone_plan
19+
from .cloner_cfg import DEFAULT_ENV_TEMPLATE
1920
from .cloner_strategies import sequential
2021
from .usd import UsdReplicateContext
2122

@@ -140,6 +141,7 @@ def __init__(
140141
clone_strategy: Callable = sequential,
141142
valid_set: torch.Tensor | None = None,
142143
replicate_physics: bool = True,
144+
env_template: str = DEFAULT_ENV_TEMPLATE,
143145
):
144146
"""Capture arguments for :func:`make_clone_plan` and :func:`replicate`.
145147
@@ -154,6 +156,7 @@ def __init__(
154156
prototype combinations; ``None`` uses the full cartesian product.
155157
replicate_physics: Whether physics replication clones each environment;
156158
forwarded to :func:`replicate`.
159+
env_template: Path template for a replicated env prim, ``{}`` marking the env index.
157160
"""
158161
self._cfgs = cfgs
159162
self._stage = stage
@@ -164,6 +167,7 @@ def __init__(
164167
device=device,
165168
clone_strategy=clone_strategy,
166169
valid_set=valid_set,
170+
env_template=env_template,
167171
)
168172
self._plan: ClonePlan | None = None
169173

source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ def has_rigid_body_api(prim) -> bool:
355355
if not rigid_matches:
356356
raise ValueError(f"No descendant rigid body found under the expression: '{self._asset.cfg.prim_path}'.")
357357
_, root_rigidbody_path = rigid_matches[0]
358-
task_frame_transformer_path = "/World/envs/env_.*/" + self.cfg.task_frame_rel_path
358+
task_frame_transformer_path = "/World/envs/env_[^/]*/" + self.cfg.task_frame_rel_path
359359
task_frame_transformer_cfg = FrameTransformerCfg(
360360
prim_path=root_rigidbody_path,
361361
target_frames=[

source/isaaclab/isaaclab/envs/utils/camera_view.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ def env_path_from_template(path_template: str, env_id: int) -> str:
6363
if "{}" in path:
6464
return path.format(env_id)
6565
path = path.replace("/World/envs/*", f"/World/envs/env_{env_id}")
66-
path = path.replace("/World/envs/env_.*", f"/World/envs/env_{env_id}")
67-
path = path.replace("/World/envs/env_.*/", f"/World/envs/env_{env_id}/")
66+
path = path.replace("/World/envs/env_[^/]*", f"/World/envs/env_{env_id}")
67+
path = path.replace("/World/envs/env_[^/]*/", f"/World/envs/env_{env_id}/")
6868
return path
6969

7070

@@ -142,7 +142,7 @@ def create_visualizer_camera(
142142
attr = cam_prim.CreateAttribute("omni:scenePartition", Sdf.ValueTypeNames.Token)
143143
attr.Set(path.split("/")[-2])
144144
cfg = CameraCfg(
145-
prim_path=f"/World/envs/env_.*/{camera_name}",
145+
prim_path=f"/World/envs/env_[^/]*/{camera_name}",
146146
update_period=0.0,
147147
height=int(height),
148148
width=int(width),

source/isaaclab/isaaclab/scene/interactive_scene.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,10 @@ def __init__(self, cfg: InteractiveSceneCfg):
159159
self.cloner_cfg = copy.deepcopy(self.cfg.clone_cfg)
160160
self.cloner_cfg.device = self.device
161161
self.cloner_cfg.replicate_physics = self.cfg.replicate_physics
162-
self._env_regex_ns = self.cloner_cfg.clone_regex
163-
self._env_fmt = self._env_regex_ns.replace(".*", "{}")
164-
self._env_ns = self._env_regex_ns.rsplit("/", 1)[0]
162+
# the template is authoritative; the regex form is the same namespace spelled for matching
163+
self._env_fmt = self.cloner_cfg.clone_template
164+
self._env_regex_ns = self._env_fmt.format("[^/]+")
165+
self._env_ns = self._env_fmt.rsplit("/", 1)[0]
165166
self.env_prim_paths = [self._env_fmt.format(i) for i in range(self.cfg.num_envs)]
166167
self._scene_asset_names: list[str] = []
167168
self._clone_valid_set: torch.Tensor | None = None
@@ -189,6 +190,7 @@ def __init__(self, cfg: InteractiveSceneCfg):
189190
num_clones=self.num_envs,
190191
env_spacing=self.cfg.env_spacing,
191192
device=self.device,
193+
env_template=self._env_fmt,
192194
stage=self.stage,
193195
clone_strategy=self.cloner_cfg.clone_strategy,
194196
valid_set=self._clone_valid_set,
@@ -225,7 +227,7 @@ def _collect_asset_cfgs(self) -> list[Any]:
225227
)
226228
for child in children:
227229
if hasattr(child, "prim_path"):
228-
child.prim_path = child.prim_path.format(ENV_REGEX_NS=self.cloner_cfg.clone_regex)
230+
child.prim_path = child.prim_path.format(ENV_REGEX_NS=self._env_regex_ns)
229231
if hasattr(child, "spawn") and child.spawn is not None and self.env_ns in child.prim_path:
230232
clone_asset_names.append(asset_name)
231233
variant_counts.append(cloner.num_spawn_variants(child.spawn))

source/isaaclab/isaaclab/scene_data/deformable_discovery.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def path_to_env_wildcard(path: str) -> str:
327327

328328
def path_to_env_regex(path: str) -> str:
329329
"""Rewrite ``env_<id>`` segments to ``env_.*`` for Isaac Lab asset regex paths."""
330-
return re.sub(r"/World/envs/env_\d+", "/World/envs/env_.*", path)
330+
return re.sub(r"/World/envs/env_\d+", "/World/envs/env_[^/]*", path)
331331

332332

333333
def build_deformable_vertex_count_lookup(entries: list[DeformableStageEntry]) -> dict[str, int]:

0 commit comments

Comments
 (0)