Skip to content

Match prim path expressions as whole-path regular expressions - #6841

Draft
ooctipus wants to merge 1 commit into
isaac-sim:developfrom
ooctipus:octi/prim-path-real-regex-matcher
Draft

Match prim path expressions as whole-path regular expressions#6841
ooctipus wants to merge 1 commit into
isaac-sim:developfrom
ooctipus:octi/prim-path-real-regex-matcher

Conversation

@ooctipus

@ooctipus ooctipus commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Description

find_matching_prims matches each /-separated token against prim names at that depth. find_first_matching_prim compiles the same argument as one regex over the full path. Both take a parameter named prim_path_regex and document the identical contract:

prim_path_regex: The regex expression for prim path.

On one stage, one expression:

"/World/Robot/.*link2"

find_matching_prims      -> []
find_first_matching_prim -> /World/Robot/link1/link2

This makes both match the whole path as a plain Python regular expression, and has find_first_matching_prim delegate to find_matching_prims. Two further changes follow, because .* had been carrying structure rather than meaning what regex says.

Whole-path matching does not mean traversing the whole stage

The expression bounds the walk: its longest literal prefix gives the traversal root, and when no wildcard can span a /, the separator count is an exact depth bound. On a synthetic 512-env stage (93,186 prims):

query matches visited time
env_[^/]+/Robot 512 ~1,025 2.3 ms
env_[^/]+/Robot/link[0-9]+ 30,720 62 ms
env_[^/]+/Robot/.* 92,160 all 239 ms
naive whole-stage walk, first query 512 93,186 143 ms

Cost scales with what was asked for: unbounded only when the expression itself is unbounded.

The clone slot comes from the template, not from the text

make_clone_plan derived an asset's destination template with prim_path.replace(".*", "{}"). str.replace is positionally blind, so a second wildcard below the environment slot produces two slots:

/World/envs/env_.*/Robot/.*  ->  /World/envs/env_{}/Robot/{}  ->  .format(3)  ->  IndexError

Latent today only because an unrelated hasattr(cfg, "spawn") filter excludes the sensor cfgs that would reach it. The environment root was hardcoded in the same function, so a non-default namespace silently excluded every asset from the plan.

CloneCfg now carries clone_template and the plan splits the cfg path at the known environment depth. A template always yields a regex; recovering a template from a regex requires guessing which part of the text is the wildcard. CloneCfg.clone_regex is removed — its value is clone_template.format("[^/]+"), so the two can no longer disagree.

The environment slot is segment-safe

With .* free to mean what regex says, the namespace spells its slot [^/]+:

{ENV_REGEX_NS}/Robot   ->  env_0/Robot, env_1/Robot, env_2/Robot     (Table/Robot excluded)

path.match accepts a character class in the clone slot for this, since the literal text [^/]+ contains a / and so cannot match the one-segment alternative. Both spellings still resolve, so no existing caller changes.

Breaking change

Expressions relying on a trailing token meaning "direct children only" should spell it [^/]+. Classifying every production path literal:

class count when behaviour changes
no wildcard 2,842 never
trailing wildcard 8 deterministically — "direct children" becomes "whole subtree"
mid-pattern wildcard 38 only if a same-named prim exists deeper under the environment

Seven of the eight reach the namespace through {ENV_REGEX_NS} and are covered by the slot change; they are contact-sensor body patterns ({ENV_REGEX_NS}/Robot/foot_.*) whose targets are leaf prims. CloneCfg.clone_regex is removed rather than deprecated — worth a maintainer's call whether that needs a shim.

Note green tests are weak evidence here: the trailing-wildcard class changes meaning unconditionally, and the mid-pattern class only on stage shapes the suite does not construct.

Not in this change

resolve_matching_prims_from_source still has a legacy no-clone-plan branch that infers an environment boundary heuristically and returns a different shape than the plan branch. 23 ad-hoc multi-instance test scenes (/World/Env_.*/Anymal) depend on that inference, so replacing it is a separate decision. Until it goes, the ~17 expr.replace(".*", "*") calls that build physics-view globs must stay — they are no-ops whenever a clone plan exists, but the legacy branch can still emit .*.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist

  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have added tests that prove my fix is effective or that my feature works
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Aug 1, 2026
@ooctipus
ooctipus force-pushed the octi/prim-path-real-regex-matcher branch 15 times, most recently from 48d4f06 to 18795e8 Compare August 2, 2026 00:45
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.
@ooctipus
ooctipus force-pushed the octi/prim-path-real-regex-matcher branch from 18795e8 to 66b4930 Compare August 2, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant