Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b4e6eb9
Cache find_joints and find_bodies on Articulation
hougantc-nvda Apr 7, 2026
057115b
Add cache safety tests for find_joints and find_bodies
hougantc-nvda Apr 7, 2026
e8aa47d
Use functools.cache for find_joints and find_bodies caching
hougantc-nvda Apr 8, 2026
126b545
Move resolve-matching-names caches to AssetBase
hougantc-nvda Apr 9, 2026
f6fc66e
Revert to reflect what is in develop
hougantc-nvda Apr 9, 2026
adff369
Add OVPhysX RigidObject design spec
AntoineRichard Apr 20, 2026
144a966
Add all OVPhysX integration design specs
AntoineRichard Apr 20, 2026
b383738
Init resolve_matching_names caches in classes that skip super().__init__
AntoineRichard Apr 20, 2026
bb535f5
Fix trailing whitespace in OVPhysX design spec
AntoineRichard Apr 20, 2026
790693c
Remove OVPhysX design spec files
AntoineRichard Apr 20, 2026
2a057d6
Add docs/superpowers/ to .gitignore
AntoineRichard Apr 20, 2026
b66af04
Port resolve-matching-names caching to OVPhysX articulation
AntoineRichard Apr 21, 2026
fe4b414
Move resolve_matching_names caching to function level
AntoineRichard Apr 21, 2026
eb032c5
Add ovphysx changelog for finder method unification
AntoineRichard Apr 21, 2026
05b9449
Address review feedback on changelogs and docstrings
AntoineRichard Apr 21, 2026
5ba6efc
Reclassify joint_subset type change as Changed in ovphysx changelog
AntoineRichard Apr 21, 2026
2ef9c49
Clear resolve_matching_names cache on SimulationContext teardown
AntoineRichard Apr 21, 2026
1fc3fa3
Merge remote-tracking branch 'origin/develop' into hougantc/cache-pic…
AntoineRichard Apr 23, 2026
dc609ec
Bound random spin in rigid object state test
AntoineRichard Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,6 @@ _build

# Isaac Lab CI environments in native mode
**/_isaaclab_install_ci_*

# Superpowers (Claude Code plugin artifacts)
docs/superpowers/
2 changes: 1 addition & 1 deletion source/isaaclab/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "4.6.11"
version = "4.6.12"

# Description
title = "Isaac Lab framework for Robot Learning"
Expand Down
11 changes: 11 additions & 0 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
---------

4.6.12 (2026-04-23)
~~~~~~~~~~~~~~~~~~~

Added
^^^^^

* Added caching to :func:`~isaaclab.utils.string.resolve_matching_names`,
avoiding repeated regex matching across ``find_bodies``, ``find_joints``,
and related calls.


4.6.11 (2026-04-22)
~~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 4 additions & 0 deletions source/isaaclab/isaaclab/sim/simulation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
resolve_scene_data_requirements,
)
from isaaclab.sim.utils import create_new_stage
from isaaclab.utils.string import clear_resolve_matching_names_cache
from isaaclab.utils.version import has_kit
from isaaclab.visualizers.base_visualizer import BaseVisualizer

Expand Down Expand Up @@ -835,6 +836,9 @@ def clear_instance(cls) -> None:
# close_stage() + app shutdown destroy the entire stage at once.
stage_utils.close_stage()

# Discard cached name-resolution data from destroyed assets
clear_resolve_matching_names_cache()

# Clear instance
cls._instance = None

Expand Down
2 changes: 2 additions & 0 deletions source/isaaclab/isaaclab/utils/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ __all__ = [
"string_to_callable",
"ResolvableString",
"resolve_matching_names",
"clear_resolve_matching_names_cache",
"resolve_matching_names_values",
"find_unique_string_name",
"find_root_prim_path_from_regex",
Expand Down Expand Up @@ -98,6 +99,7 @@ from .string import (
string_to_callable,
ResolvableString,
resolve_matching_names,
clear_resolve_matching_names_cache,
resolve_matching_names_values,
find_unique_string_name,
find_root_prim_path_from_regex,
Expand Down
123 changes: 78 additions & 45 deletions source/isaaclab/isaaclab/utils/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""Sub-module containing utilities for transforming strings and regular expressions."""

import ast
import functools
import importlib
import inspect
import re
Expand Down Expand Up @@ -247,50 +248,19 @@ def __deepcopy__(self, memo):
"""


def resolve_matching_names(
keys: str | Sequence[str],
list_of_strings: Sequence[str],
preserve_order: bool = False,
*,
raise_when_no_match: bool = True,
) -> tuple[list[int], list[str]]:
"""Match a list of query regular expressions against a list of strings and return the matched indices and names.

When a list of query regular expressions is provided, the function checks each target string against each
query regular expression and returns the indices of the matched strings and the matched strings.

If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order
of the provided list of strings. This means that the ordering is dictated by the order of the target strings
and not the order of the query regular expressions.

If the :attr:`preserve_order` is False, the ordering of the matched indices and names is the same as the order
of the provided list of query regular expressions.

For example, consider the list of strings is ['a', 'b', 'c', 'd', 'e'] and the regular expressions are ['a|c', 'b'].
If :attr:`preserve_order` is False, then the function will return the indices of the matched strings and the
strings as: ([0, 1, 2], ['a', 'b', 'c']). When :attr:`preserve_order` is True, it will return them as:
([0, 2, 1], ['a', 'c', 'b']).
@functools.cache
def _resolve_matching_names_impl(
Comment thread
hougantc-nvda marked this conversation as resolved.
keys: tuple[str, ...],
list_of_strings: tuple[str, ...],
preserve_order: bool,
raise_when_no_match: bool,
) -> tuple[tuple[int, ...], tuple[str, ...]]:
"""Cached implementation of :func:`resolve_matching_names`.

Note:
The function does not sort the indices. It returns the indices in the order they are found.

Args:
keys: A regular expression or a list of regular expressions to match the strings in the list.
list_of_strings: A list of strings to match.
preserve_order: Whether to preserve the order of the query keys in the returned values. Defaults to False.
raise_when_no_match: Whether to raise a ``ValueError`` when not all regular expressions are matched.
Defaults to True. When False, returns empty lists instead of raising.

Returns:
A tuple of lists containing the matched indices and names.

Raises:
ValueError: When multiple matches are found for a string in the list.
ValueError: When not all regular expressions are matched and :attr:`raise_when_no_match` is True.
All arguments are hashable so that ``functools.cache`` can store results.
Returns tuples (immutable) to protect the cached data from mutation;
the public wrapper converts these back to fresh lists for each caller.
"""
# resolve name keys
if isinstance(keys, str):
keys = [keys]
# find matching patterns
index_list = []
names_list = []
Expand Down Expand Up @@ -337,7 +307,7 @@ def resolve_matching_names(
# check that all regular expressions are matched
if not all(keys_match_found):
if not raise_when_no_match:
return [], []
return (), ()
# make this print nicely aligned for debugging
msg = "\n"
for key, value in zip(keys, keys_match_found):
Expand All @@ -347,8 +317,66 @@ def resolve_matching_names(
raise ValueError(
f"Not all regular expressions are matched! Please check that the regular expressions are correct: {msg}"
)
# return
return index_list, names_list
# return immutable tuples for safe caching
return tuple(index_list), tuple(names_list)


def resolve_matching_names(
keys: str | Sequence[str],
list_of_strings: Sequence[str],
preserve_order: bool = False,
*,
raise_when_no_match: bool = True,
) -> tuple[list[int], list[str]]:
"""Match a list of query regular expressions against a list of strings and return the matched indices and names.

When a list of query regular expressions is provided, the function checks each target string against each
query regular expression and returns the indices of the matched strings and the matched strings.

If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order
of the provided list of strings. This means that the ordering is dictated by the order of the target strings
and not the order of the query regular expressions.

If the :attr:`preserve_order` is False, the ordering of the matched indices and names is the same as the order
of the provided list of query regular expressions.

For example, consider the list of strings is ['a', 'b', 'c', 'd', 'e'] and the regular expressions are ['a|c', 'b'].
If :attr:`preserve_order` is False, then the function will return the indices of the matched strings and the
strings as: ([0, 1, 2], ['a', 'b', 'c']). When :attr:`preserve_order` is True, it will return them as:
([0, 2, 1], ['a', 'c', 'b']).

Results are cached internally — repeated calls with the same arguments avoid redundant regex matching.

Note:
The function does not sort the indices. It returns the indices in the order they are found.

Args:
keys: A regular expression or a list of regular expressions to match the strings in the list.
list_of_strings: A list of strings to match.
preserve_order: Whether to preserve the order of the query keys in the returned values. Defaults to False.
raise_when_no_match: Whether to raise a ``ValueError`` when not all regular expressions are matched.
Defaults to True. When False, returns empty lists instead of raising.

Returns:
A tuple of lists containing the matched indices and names.

Raises:
ValueError: When multiple matches are found for a string in the list.
ValueError: When not all regular expressions are matched and :attr:`raise_when_no_match` is True.
"""
_keys = (keys,) if isinstance(keys, str) else tuple(keys)
idx, names = _resolve_matching_names_impl(_keys, tuple(list_of_strings), preserve_order, raise_when_no_match)
return list(idx), list(names)


def clear_resolve_matching_names_cache() -> None:
"""Discard all cached results from :func:`resolve_matching_names`.

Call this when the simulation scene is torn down so that cached
name-resolution entries from destroyed assets do not accumulate
across scene rebuilds in long-lived processes.
"""
_resolve_matching_names_impl.cache_clear()


def resolve_matching_names_values(
Expand All @@ -360,6 +388,11 @@ def resolve_matching_names_values(
"""Match a list of regular expressions in a dictionary against a list of strings and return
the matched indices, names, and values.

Note:
Unlike :func:`resolve_matching_names`, this function is not cached. Current callers
use it during initialization only (e.g. action/actuator config resolution), so caching
would add complexity without a measurable benefit.

If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order
of the provided list of strings. This means that the ordering is dictated by the order of the target strings
and not the order of the query regular expressions.
Expand Down
66 changes: 66 additions & 0 deletions source/isaaclab/test/assets/test_articulation_iface.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,72 @@ def test_find_joints_single(self, backend, num_instances, num_joints, num_bodies
assert names == [first_joint]


# ---------------------------------------------------------------------------
# Tests: resolve_matching_names caching behavior
# ---------------------------------------------------------------------------


_non_mock_backends = pytest.mark.parametrize("backend", [b for b in BACKENDS if b != "mock"], indirect=False)


class TestResolveMatchingNamesCache:
"""Test that resolve_matching_names caching returns correct, isolated results."""

@_non_mock_backends
@pytest.mark.parametrize("num_instances, num_joints, num_bodies", [(2, 6, 7)])
@_default_devices
def test_unmatched_regex_raises(self, backend, num_instances, num_joints, num_bodies, device):
"""ValueError from resolve_matching_names propagates correctly."""
art, _ = get_articulation(backend, num_instances, num_joints, num_bodies, device=device)
with pytest.raises(ValueError):
art.find_bodies("nonexistent_body_xyz")
with pytest.raises(ValueError):
art.find_joints("nonexistent_joint_xyz")

@_backends
@pytest.mark.parametrize("num_instances, num_joints, num_bodies", [(2, 6, 7)])
@_default_devices
def test_mutating_result_does_not_corrupt_cache(
self, backend, num_instances, num_joints, num_bodies, device, articulation_iface
):
"""Mutating returned lists must not affect future cached results."""
art, _ = articulation_iface

for finder, expected_len in [("find_bodies", num_bodies), ("find_joints", num_joints)]:
idx1, names1 = getattr(art, finder)(".*")
assert len(idx1) == expected_len

idx1.clear()
names1.append("corrupted")

idx2, names2 = getattr(art, finder)(".*")
assert len(idx2) == expected_len
assert "corrupted" not in names2

@_non_mock_backends
@pytest.mark.parametrize("num_instances, num_joints, num_bodies", [(2, 6, 7)])
@_default_devices
def test_find_with_multiple_patterns(self, backend, num_instances, num_joints, num_bodies, device):
"""Passing a list of regex patterns works correctly."""
art, _ = get_articulation(backend, num_instances, num_joints, num_bodies, device=device)
idx, names = art.find_joints(["joint_0", "joint_1"])
assert "joint_0" in names
assert "joint_1" in names
assert len(names) == 2

@_non_mock_backends
@pytest.mark.parametrize("num_instances, num_joints, num_bodies", [(2, 6, 7)])
@_default_devices
def test_find_with_preserve_order(self, backend, num_instances, num_joints, num_bodies, device):
"""preserve_order=True returns names in the order of the input patterns."""
art, _ = get_articulation(backend, num_instances, num_joints, num_bodies, device=device)
idx_fwd, names_fwd = art.find_joints(["joint_1", "joint_0"], preserve_order=True)
assert names_fwd == ["joint_1", "joint_0"]

idx_rev, names_rev = art.find_joints(["joint_0", "joint_1"], preserve_order=True)
assert names_rev == ["joint_0", "joint_1"]


# ---------------------------------------------------------------------------
# Tests: ArticulationData root state properties
# ---------------------------------------------------------------------------
Expand Down
20 changes: 20 additions & 0 deletions source/isaaclab/test/utils/test_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pytest

import isaaclab.utils.string as string_utils
from isaaclab.utils.string import _resolve_matching_names_impl


def test_resolvable_string_metadata_is_non_eager():
Expand Down Expand Up @@ -251,3 +252,22 @@ def test_resolve_matching_names_values_with_basic_strings_and_preserved_order():
query_names = {"a|c": 1, "b": 0, "f": 2}
with pytest.raises(ValueError):
_ = string_utils.resolve_matching_names_values(query_names, target_names, preserve_order=True)


def test_clear_resolve_matching_names_cache():
"""Clearing the cache discards previously cached entries."""
target_names = ["a", "b", "c"]
# Populate the cache
string_utils.resolve_matching_names("a", target_names)
info_before = _resolve_matching_names_impl.cache_info()
assert info_before.currsize > 0

# Clear the cache
string_utils.clear_resolve_matching_names_cache()
info_after = _resolve_matching_names_impl.cache_info()
assert info_after.currsize == 0

# Results are still correct after clearing
idx, names = string_utils.resolve_matching_names("a", target_names)
assert idx == [0]
assert names == ["a"]
2 changes: 1 addition & 1 deletion source/isaaclab_newton/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "0.5.20"
version = "0.5.21"

# Description
title = "Newton simulation interfaces for IsaacLab core package"
Expand Down
10 changes: 10 additions & 0 deletions source/isaaclab_newton/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Changelog
---------

0.5.21 (2026-04-23)
~~~~~~~~~~~~~~~~~~~

Fixed
^^^^^

* Fixed flakiness in ``test_body_root_state_properties`` by bounding the random spin velocity so
numerical drift stays within the position tolerance over the simulated trajectory.


0.5.20 (2026-04-22)
~~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 2 additions & 2 deletions source/isaaclab_newton/test/assets/test_rigid_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,9 +948,9 @@ def test_body_root_state_properties(num_cubes, device, with_offset):
# check center of mass has been set
torch.testing.assert_close(wp.to_torch(cube_object.data.body_com_pos_b).squeeze(1), offset)

# random z spin velocity
# random z spin velocity (bounded to keep numerical drift within the position tolerance below)
spin_twist = torch.zeros(6, device=device)
spin_twist[5] = torch.randn(1, device=device)
spin_twist[5] = 0.5 * torch.randn(1, device=device).clamp(-1.0, 1.0)

# Simulate physics
for _ in range(100):
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab_ovphysx/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "0.1.0"
version = "0.1.1"

# Description
title = "OvPhysX simulation interfaces for IsaacLab core package"
Expand Down
Loading
Loading