Skip to content

Fix contact sensor filter matching geometry children with same name#6556

Open
ooctipus wants to merge 3 commits into
isaac-sim:developfrom
ooctipus:octi/fix-contact-filter-rigid-body-only
Open

Fix contact sensor filter matching geometry children with same name#6556
ooctipus wants to merge 3 commits into
isaac-sim:developfrom
ooctipus:octi/fix-contact-filter-rigid-body-only

Conversation

@ooctipus

@ooctipus ooctipus commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Wrap the leaf segment of each filter_prim_paths_expr in PhysX alternation syntax (leaf) before the .** conversion.

Root cause: URDF-to-USD conversion nests a rigid body and its geometry child under the same leaf name (e.g. .../Object body, .../Object/Object geometry). PhysX's glob engine prefix-matches a bare leaf pattern env_*/Object against both prims and raises a count-consistency error (expected 1, got 2). The same issue does not affect the sensor body resolution because body_names_glob already wraps leaf names in alternation (name1|name2) — anchoring the match to the correct path depth.

Fix:

# before
filter_prim_paths_glob = [expr.replace(".*", "*") for expr in self.cfg.filter_prim_paths_expr]

# after
filter_prim_paths_glob = [
    re.sub(r"([^/]+)$", r"(\1)", expr).replace(".*", "*")
    for expr in self.cfg.filter_prim_paths_expr
]

re.sub(r"([^/]+)$", r"(\1)", expr) captures the last path segment and wraps it in (). Example:

expression before after
env_.*/Object env_*/Object env_*/(Object)
env_.*/Robot/.*_FOOT env_*/Robot/*_FOOT env_*/Robot/(*_FOOT)

Not affected: Newton backend — it uses Python fnmatch against physics-model body labels, which never crosses /.

Test plan

  • Reproduce with a URDF asset where a link body and its geometry share a leaf name (standard URDF-to-USD output), configure a ContactSensorCfg pointing at another such asset in filter_prim_paths_expr, verify initialization no longer raises a count error.
  • Existing contact sensor tests pass.

🤖 Generated with Claude Code

@github-actions github-actions Bot added bug Something isn't working isaac-lab Related to Isaac Lab team labels Jul 16, 2026
@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where filter_prim_paths_expr in ContactSensor could trigger a count-consistency error from PhysX when URDF-to-USD conversion places a rigid body and its geometry child under the same leaf name. The fix pre-resolves filter expressions by walking the stage with a UsdPhysics.RigidBodyAPI predicate and wraps matched names in alternation groups — exactly mirroring the PhysxContactReportAPI pre-resolution already used for sensor bodies.

  • contact_sensor.py: New per-expression loop replaces the former one-liner, walking the stage with a RigidBodyAPI predicate and emitting env_*/(Name) globs to anchor the leaf segment in PhysX's pattern engine; a raw-expression fallback is included when no rigid-body prims are found.
  • changelog entry: Adds a concise fixed-item entry describing the root cause and the resolution approach.

Confidence Score: 3/5

The contact sensor fix handles the primary URDF rigid-body case correctly, but an unguarded rsplit will crash on any filter expression that contains no slash, and static bodies used as filter targets still receive the unfixed bare-leaf expression.

The rsplit unpack at line 349 is a hard crash for filter expressions without a slash, which is a definite regression over the old one-liner. Additionally, static collision bodies bypass the new pre-resolution entirely and fall back to the same prefix-matching pattern that motivated the fix. The core logic for the targeted dynamic-rigid-body case is sound, but these gaps mean specific real-world configurations will either crash or hit the original bug.

source/isaaclab_physx/isaaclab_physx/sensors/contact_sensor/contact_sensor.py — the new filter pre-resolution loop needs a guard for expressions without a slash and consideration of static (non-RigidBodyAPI) collision bodies.

Important Files Changed

Filename Overview
source/isaaclab_physx/isaaclab_physx/sensors/contact_sensor/contact_sensor.py Adds pre-resolution loop for filter_prim_paths_expr; contains an unguarded rsplit that will raise ValueError on paths with no "/" and a predicate that silently skips static collision bodies, leaving them on the unfixed raw-expression fallback path.
source/isaaclab_physx/changelog.d/octi-fix-contact-filter-rigid-body-only.rst New changelog fragment, accurate and well-written description of the fix.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Init as _initialize_impl
    participant Stage as USD Stage
    participant PhysX as PhysX create_rigid_contact_view

    Init->>Stage: resolve_matching_prims_from_source(parent_expr, has_contact_report)
    Stage-->>Init: "body_matches - body_names_glob = env_*/(Body)"

    loop for each filter_prim_paths_expr
        Init->>Init: rsplit slash 1 - filter_parent_expr, filter_leaf_str
        Init->>Init: compile(filter_leaf_str) - filter_leaf_re
        Init->>Stage: resolve_matching_prims_from_source(filter_parent_expr, _has_rigid_body)
        alt rigid body prims found
            Stage-->>Init: filter_matches (RigidBodyAPI prims only)
            Init->>Init: "build filter_prim_paths_glob = env_*/(Name)"
        else no rigid body prims
            Stage-->>Init: empty list
            Init->>Init: fallback raw expr - bare leaf prefix-match risk
        end
    end

    Init->>PhysX: create_rigid_contact_view(body_names_glob, filter_patterns)
    PhysX-->>Init: contact view (count verified against len(body_names))
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Init as _initialize_impl
    participant Stage as USD Stage
    participant PhysX as PhysX create_rigid_contact_view

    Init->>Stage: resolve_matching_prims_from_source(parent_expr, has_contact_report)
    Stage-->>Init: "body_matches - body_names_glob = env_*/(Body)"

    loop for each filter_prim_paths_expr
        Init->>Init: rsplit slash 1 - filter_parent_expr, filter_leaf_str
        Init->>Init: compile(filter_leaf_str) - filter_leaf_re
        Init->>Stage: resolve_matching_prims_from_source(filter_parent_expr, _has_rigid_body)
        alt rigid body prims found
            Stage-->>Init: filter_matches (RigidBodyAPI prims only)
            Init->>Init: "build filter_prim_paths_glob = env_*/(Name)"
        else no rigid body prims
            Stage-->>Init: empty list
            Init->>Init: fallback raw expr - bare leaf prefix-match risk
        end
    end

    Init->>PhysX: create_rigid_contact_view(body_names_glob, filter_patterns)
    PhysX-->>Init: contact view (count verified against len(body_names))
Loading

Reviews (1): Last reviewed commit: "Fix contact sensor filter matching geome..." | Re-trigger Greptile

# same name as the rigid body (a common URDF-to-USD conversion artifact).
filter_prim_paths_glob = []
for filter_expr in self.cfg.filter_prim_paths_expr:
filter_parent_expr, filter_leaf_str = filter_expr.rsplit("/", 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 rsplit crash when filter expression contains no /

filter_expr.rsplit("/", 1) returns a one-element list when the expression has no /, so the two-variable unpack raises ValueError: not enough values to unpack. The original one-liner (expr.replace(".*", "*")) handled any string safely. While all valid USD prim-path expressions start with /, programmatically generated expressions that are bare patterns (e.g. a plain "Object") would crash the sensor at initialization instead of falling through to the safe raw-expression path.

Comment on lines +352 to +353
def _has_rigid_body(prim, _pattern=filter_leaf_re) -> bool:
return bool(_pattern.fullmatch(prim.GetName())) and prim.HasAPI(UsdPhysics.RigidBodyAPI)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fix does not reach static collision bodies

_has_rigid_body requires UsdPhysics.RigidBodyAPI, so static (fixed) collision bodies used as filter targets — ground planes, walls, fixed URDF links — will never match and will fall through to the fallback at line 366. That fallback emits the bare-leaf expression, which is exactly the form that triggers PhysX prefix-matching and the count-consistency error the PR is fixing. If a user filters contacts against a static URDF asset whose collision geometry shares the same leaf name as its fixed link, they will still hit the original crash. Consider also accepting UsdPhysics.CollisionAPI (without RigidBodyAPI) as a second resolution pass, or filtering by CollisionAPI alone, to cover static bodies.

Comment on lines +359 to +363
filter_leaf_names = [prim.GetPath().pathString.rsplit("/", 1)[-1] for prim, _ in filter_matches]
_, filter_path_expr = filter_matches[0]
filter_parent_dest = filter_path_expr.rsplit("/", 1)[0]
filter_names_group = r"(" + "|".join(filter_leaf_names) + r")"
filter_prim_paths_glob.append(f"{filter_parent_dest}/{filter_names_group}".replace(".*", "*"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 filter_parent_dest anchored only to first match; deeper matches silently dropped

filter_parent_dest is derived exclusively from filter_matches[0]'s destination expression. If the filter expression resolves prims that live at different depths within the same env instance (e.g., env_.*/robot/Object and env_.*/Object both matching a broad pattern), only the first match's parent is used. The leaf names from all matches are joined into the alternation group, but the resulting glob path filter_parent_dest/(name1|name2) will not reach prims under a different parent. This is an edge case for narrow filter expressions, but worth a clarifying question: is the assumption that all filter expression matches always share the same parent guaranteed by the configuration API?

… name

Wrap the leaf segment of each filter_prim_paths_expr in PhysX alternation
syntax before the standard .* -> * conversion:

  re.sub(r"([^/]+)$", r"(\1)", expr).replace(".*", "*")

PhysX's glob engine prefix-matches bare leaf patterns, so env_*/Object
also matches env_0/Object/Object (a geometry child sharing the same name
-- a common URDF-to-USD artifact). Wrapping in (leaf) anchors the match
to that exact path depth.
@ooctipus
ooctipus force-pushed the octi/fix-contact-filter-rigid-body-only branch from d724c5c to af1e64f Compare July 17, 2026 01:57
# anchors the match to that exact path depth.
filter_prim_paths_glob = [
re.sub(r"([^/]+)$", r"(\1)", expr).replace(".*", "*") for expr in self.cfg.filter_prim_paths_expr
]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just need a positive and negative test for the change if it's failing before

- test_filter_prim_paths_glob_leaf_wrapping: parametrized unit test that
  directly exercises the re.sub + replace transformation. The expected_old
  column captures the pre-fix bare-leaf glob that prefix-matched geometry
  children; the expected_new column captures the anchored (leaf) form.

- test_contact_sensor_filter_same_name_geometry_child: integration test
  that authors the URDF-to-USD nesting pattern (link prim /Table with
  geometry child /Table/Table sharing the same name) and verifies that
  ContactSensor initialization succeeds and reports exactly 1 filter
  shape per env. Before the fix this raised RuntimeError because PhysX
  matched 2 prims per env instead of 1.
@ooctipus
ooctipus requested a review from a team July 18, 2026 01:14
…eometry

PhysX contact view body resolution requires the RigidBodyAPI on an Xform
parent prim; applying it directly to a UsdGeom.Cube shape left the body
glob unresolvable, giving a null contact_view backend.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants