Skip to content

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

Draft
ooctipus wants to merge 6 commits 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 6 commits 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 17 times, most recently from 66b4930 to 6019926 Compare August 2, 2026 07:47
@github-actions github-actions Bot added asset New asset feature or request isaac-mimic Related to Isaac Mimic team labels Aug 2, 2026
@ooctipus
ooctipus force-pushed the octi/prim-path-real-regex-matcher branch from 6019926 to 5681bb8 Compare August 2, 2026 07:54
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 5681bb8 to b853442 Compare August 2, 2026 09:53
A segment wildcard is written as the character class [^/], whose text
contains a '/' that is not a separator. Every caller that reached for
str.split("/") to take a path expression apart therefore cut a class in
half, yielding a truncated pattern that raised "unterminated character
set" at re.compile or a body name like "]+_FOOT". Route those callers
through split_path_expr, which splits on separators only.

The same conflation appeared where an expression names an index slot
rather than a wildcard. spawn_multi_asset and the clone decorator
substituted a literal ".*", so a slot spelled "[^/]*" silently survived
into the prim path. Normalize to glob first, which collapses every
spelling to the single '*' the index replaces.

Newton registered a tracked ray-cast target under one spelling of the
environment slot and looked it up under another, raising KeyError.
Spell it from the shared template on both sides.

Replace the hand-rolled regex-to-glob replace chains with
path_expr_to_glob so the engine boundary has one implementation, and
qualify the export so isaaclab.sim resolves it.
A segment wildcard has several spellings, and code that substitutes a
concrete environment index into a path expression was matching exactly
one of them. The visualizer camera view looked for "env_[^/]*" while the
namespace is built as "env_[^/]+", so str.replace found nothing and the
camera kept a wildcard where a concrete environment belonged, rendering
a different scene than the golden. The OVRTX deformable bindings had the
same shape against a literal ".*". Match the wildcard rather than a
spelling of it.

path_to_source reported its destination as a glob. Callers use it to
build the path expression its name promises and then convert to glob at
the engine boundary, so the star reached find_first_matching_prim as a
quantifier and matched nothing -- "Failed to find articulation root prim
at '/World/envs/env_*/Robot'". Extending the clone-slot match to accept
a character class is what began routing callers down this branch, so
report a path expression and let the boundary do the converting.
The ovphysx tests wrote their prim paths with a bare "*", which only
resolved because the matcher used to rewrite a lone star into ".*". That
rewrite is gone, since a star is a quantifier and the rewrite could not
tell the two apart, so spell the wildcard the way the expression is now
read. The ovphysx view "pattern=" arguments are fnmatch globs and keep
their stars.

The ovphysx frame transformer stripped the env prefix with a bare
"[^/]+" alternative, which consumed the opening half of a character
class and left a body name of "]*". Try the wildcard spellings first,
matching the PhysX copy.

Isaac Sim's XformPrim.resolve_paths applies one regex per path segment,
so a segment wildcard has to reach it as ".*" -- "[^/]" holds a
separator and gets split across two of its segments. Convert where the
gripper view is built rather than rewriting the expression the IsaacLab
matcher still needs.
The fabric particle sync substitutes the instance index into a
deformable's visual mesh path, first in the env slot and then for any
wildcard left over. The second pass still only recognised ".*", so a
path spelling the wildcard as a character class kept it and resolved to
no prim, leaving that instance unsynced.
@ooctipus
ooctipus force-pushed the octi/prim-path-real-regex-matcher branch from d60dd9e to c44ab0d Compare August 3, 2026 02:02
The imu and pva sensor tests still handed their sensor prim paths a bare
"*", so the sensors resolved nothing once the matcher stopped rewriting
a lone star into ".*". Their spawn paths were already converted; bring
the sensor paths with them, and correct the note that called the prim
path an fnmatch glob -- the glob is the ovphysx binding underneath it,
which IsaacLab derives.

The ovphysx view tests keep their stars: those strings are passed to
OvPhysxView as binding patterns, which really are fnmatch globs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

asset New asset feature or request isaac-lab Related to Isaac Lab team isaac-mimic Related to Isaac Mimic team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant