Skip to content

Commit 5cf4636

Browse files
authored
Merge branch 'develop' into frlai/improve_leapp_export
2 parents 2c3853c + 7bf6959 commit 5cf4636

44 files changed

Lines changed: 633 additions & 177 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.

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ Guidelines for modifications:
123123
* Jiwen Cai
124124
* Johnson Sun
125125
* Juana Du
126+
* Kai Pei
126127
* Kaixi Bao
127128
* Kourosh Darvish
128129
* Kousheek Chakraborty
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed :attr:`~isaaclab.envs.mimic_env_cfg.DataGenConfig.max_num_failures` being ignored. The field
5+
documented a cap on failed generation attempts but was never read, so a run with
6+
:attr:`~isaaclab.envs.mimic_env_cfg.DataGenConfig.generation_guarantee` enabled retried without
7+
bound on a task with a low success rate. Its default is now ``None`` (no limit) and setting it to
8+
an integer stops generation once that many attempts have failed.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed function and class-based observation modifiers failing after a configuration dictionary round-trip.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Test-only: added a source checkout root fixture for tests that inspect repository artifacts.

source/isaaclab/isaaclab/envs/mimic_env_cfg.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,21 @@ class DataGenConfig:
3838
Keeping failed demonstrations is useful for visualizing and debugging low success rates.
3939
"""
4040

41-
max_num_failures: int = 50
42-
"""Maximum number of failures allowed before stopping generation."""
41+
max_num_failures: int | None = None
42+
"""Maximum number of failed generation attempts before stopping, or None for no limit.
43+
44+
Only applies together with :attr:`generation_guarantee`. With the guarantee enabled, generation
45+
keeps retrying until :attr:`generation_num_trials` demos succeed, so a task whose success rate is
46+
low can run for an unbounded number of attempts; this caps that. Defaults to None so the
47+
guarantee keeps its usual meaning unless a limit is asked for.
48+
49+
With the guarantee disabled, generation already stops after :attr:`generation_num_trials`
50+
attempts and this field is ignored, so setting it cannot cut a fixed-attempt run short.
51+
52+
The bound is read once per simulation step. Attempts that end on the step that crosses it are
53+
already complete, so a run over ``num_envs`` parallel environments can record up to
54+
``num_envs - 1`` failures beyond the bound; it is exact whenever attempts end on separate steps.
55+
"""
4356

4457
seed: int = 1
4558
"""Seed for randomization to ensure reproducibility."""

source/isaaclab/isaaclab/managers/manager_base.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
import weakref
1111
from abc import ABC, abstractmethod
1212
from collections.abc import Sequence
13+
from dataclasses import fields
1314
from typing import TYPE_CHECKING, Any
1415

1516
import isaaclab.utils.string as string_utils
1617
from isaaclab.physics import PhysicsEvent, PhysicsManager
1718
from isaaclab.utils import class_to_dict, string_to_callable
19+
from isaaclab.utils.modifiers import ModifierCfg
1820

1921
from .manager_term_cfg import ManagerTermBaseCfg
2022
from .scene_entity_cfg import SceneEntityCfg
@@ -334,8 +336,7 @@ def _resolve_common_term_cfg(self, term_name: str, term_cfg: ManagerTermBaseCfg,
334336
)
335337

336338
# get the corresponding function or functional class
337-
if isinstance(term_cfg.func, str):
338-
term_cfg.func = string_to_callable(term_cfg.func)
339+
term_cfg.func = self._resolve_param_value(term_name, "func", term_cfg.func, resolve_callable=True)
339340
# check if function is callable
340341
if not callable(term_cfg.func):
341342
raise AttributeError(f"The term '{term_name}' is not callable. Received: {term_cfg.func}")
@@ -381,7 +382,7 @@ def _process_term_cfg_at_play(self, term_name: str, term_cfg: ManagerTermBaseCfg
381382
This function is called when the simulation starts playing. It is used to process the term
382383
configuration at runtime. This includes:
383384
384-
* Resolving the scene entity configuration for the term.
385+
* Resolving scene entity configurations and nested terms throughout the term configuration.
385386
* Initializing the term if it is a class.
386387
387388
Since the above steps rely on PhysX to parse over the simulation scene, they are deferred
@@ -391,27 +392,49 @@ def _process_term_cfg_at_play(self, term_name: str, term_cfg: ManagerTermBaseCfg
391392
term_name: The name of the term.
392393
term_cfg: The term configuration.
393394
"""
394-
for key, value in term_cfg.params.items():
395-
self._resolve_param_value(term_name, key, value)
395+
for field in fields(term_cfg):
396+
value = getattr(term_cfg, field.name)
397+
resolved_value = self._resolve_param_value(
398+
term_name, field.name, value, resolve_callable=field.name == "func"
399+
)
400+
if resolved_value is not value:
401+
setattr(term_cfg, field.name, resolved_value)
396402

397-
# resolve string func references then initialize class-based terms
398-
if isinstance(term_cfg.func, str):
399-
term_cfg.func = string_to_callable(term_cfg.func)
403+
# initialize class-based terms
400404
if inspect.isclass(term_cfg.func):
401405
term_cfg.func = term_cfg.func(cfg=term_cfg, env=self._env)
402406

403-
def _resolve_param_value(self, term_name: str, key: str | int, value: Any):
404-
"""Recursively resolve a single param value (SceneEntityCfg, nested term cfgs, dicts, lists)."""
407+
def _resolve_param_value(
408+
self, term_name: str, key: str | int, value: Any, *, resolve_callable: bool = False
409+
) -> Any:
410+
"""Recursively resolve manager-owned values in a term configuration."""
411+
if resolve_callable and isinstance(value, str):
412+
return string_to_callable(value)
405413
if isinstance(value, SceneEntityCfg):
406414
try:
407415
value.resolve(self._env.scene)
408416
except ValueError as e:
409417
raise ValueError(f"Error while parsing '{term_name}:{key}'. {e}")
410418
elif isinstance(value, ManagerTermBaseCfg):
411419
self._process_term_cfg_at_play(f"{term_name}.{key}", value)
420+
elif isinstance(value, ModifierCfg):
421+
for field in fields(value):
422+
field_value = getattr(value, field.name)
423+
resolved_value = self._resolve_param_value(
424+
f"{term_name}.{key}", field.name, field_value, resolve_callable=field.name == "func"
425+
)
426+
if resolved_value is not field_value:
427+
setattr(value, field.name, resolved_value)
412428
elif isinstance(value, dict):
413429
for sub_key, sub_value in value.items():
414-
self._resolve_param_value(f"{term_name}.{key}", sub_key, sub_value)
415-
elif isinstance(value, (list, tuple)):
430+
value[sub_key] = self._resolve_param_value(f"{term_name}.{key}", sub_key, sub_value)
431+
elif isinstance(value, list):
416432
for i, item in enumerate(value):
417-
self._resolve_param_value(f"{term_name}.{key}", i, item)
433+
value[i] = self._resolve_param_value(f"{term_name}.{key}", i, item)
434+
elif isinstance(value, tuple):
435+
resolved_items = tuple(
436+
self._resolve_param_value(f"{term_name}.{key}", i, item) for i, item in enumerate(value)
437+
)
438+
if any(resolved is not original for resolved, original in zip(resolved_items, value, strict=True)):
439+
value = resolved_items
440+
return value

source/isaaclab/isaaclab/managers/observation_manager.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,10 @@ def compute_group(self, group_name: str, update_history: bool = False) -> torch.
396396
# apply post-processing
397397
if term_cfg.modifiers is not None:
398398
for modifier in term_cfg.modifiers:
399-
obs = modifier.func(obs, **modifier.params)
399+
if isinstance(modifier.func, modifiers.ModifierBase):
400+
obs = modifier.func(obs)
401+
else:
402+
obs = modifier.func(obs, **modifier.params)
400403
if isinstance(term_cfg.noise, noise.NoiseCfg):
401404
obs = term_cfg.noise.func(obs, term_cfg.noise)
402405
elif isinstance(term_cfg.noise, noise.NoiseModelCfg) and term_cfg.noise.func is not None:
@@ -579,25 +582,22 @@ def _prepare_terms(self):
579582
if term_cfg.modifiers is not None:
580583
# initialize list of modifiers for term
581584
for mod_cfg in term_cfg.modifiers:
582-
# check if class modifier and initialize with observation size when adding
583-
if isinstance(mod_cfg, modifiers.ModifierCfg):
584-
# to list of modifiers - instantiate class-based modifiers
585-
if inspect.isclass(mod_cfg.func):
586-
mod_cfg.func = mod_cfg.func(cfg=mod_cfg, data_dim=obs_dims, device=self._env.device)
587-
# verify the instance is the correct type
588-
if not isinstance(mod_cfg.func, modifiers.ModifierBase):
589-
raise TypeError(
590-
f"Modifier function '{mod_cfg.func}' for observation term '{term_name}'"
591-
f" is not an instance of 'ModifierBase'. Received: '{type(mod_cfg.func)}'."
592-
)
593-
# add to list of class modifiers
594-
self._group_obs_class_instances.append(mod_cfg.func)
595-
else:
585+
if not isinstance(mod_cfg, modifiers.ModifierCfg):
596586
raise TypeError(
597587
f"Modifier configuration '{mod_cfg}' of observation term '{term_name}' is not of"
598588
f" required type ModifierCfg, Received: '{type(mod_cfg)}'"
599589
)
600590

591+
# construct stateful modifiers with the observation size
592+
if inspect.isclass(mod_cfg.func):
593+
mod_cfg.func = mod_cfg.func(cfg=mod_cfg, data_dim=obs_dims, device=self._env.device)
594+
if not isinstance(mod_cfg.func, modifiers.ModifierBase):
595+
raise TypeError(
596+
f"Modifier function '{mod_cfg.func}' for observation term '{term_name}'"
597+
f" is not an instance of 'ModifierBase'. Received: '{type(mod_cfg.func)}'."
598+
)
599+
self._group_obs_class_instances.append(mod_cfg.func)
600+
601601
# check if function is callable
602602
if not callable(mod_cfg.func):
603603
raise AttributeError(

source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,38 @@
33
#
44
# SPDX-License-Identifier: BSD-3-Clause
55

6+
from __future__ import annotations
7+
68
from collections.abc import Callable
79
from dataclasses import MISSING
8-
from typing import Any
10+
from typing import TYPE_CHECKING, Any
911

1012
import torch
1113

1214
from isaaclab.utils.configclass import configclass
1315

14-
from . import modifier
16+
if TYPE_CHECKING:
17+
from .modifier import DigitalFilter, Integrator
18+
from .modifier_base import ModifierBase
1519

1620

1721
@configclass
1822
class ModifierCfg:
19-
"""Configuration parameters modifiers"""
20-
21-
func: Callable[..., torch.Tensor] = MISSING
22-
"""Function or callable class used by modifier.
23+
"""Configuration parameters for function and class modifiers."""
2324

24-
The function must take a torch tensor as the first argument. The remaining arguments are specified
25-
in the :attr:`params` attribute.
25+
func: Callable[..., torch.Tensor] | type[ModifierBase] | str = MISSING
26+
"""Function or :class:`ModifierBase` class used by the modifier.
2627
27-
It also supports `callable classes <https://docs.python.org/3/reference/datamodel.html#object.__call__>`_,
28-
i.e. classes that implement the ``__call__()`` method. In this case, the class should inherit from the
29-
:class:`ModifierBase` class and implement the required methods.
28+
Functions must take a tensor as their first argument. Classes must inherit from :class:`ModifierBase`; the
29+
observation manager constructs them with the configuration, observation dimensions, and device.
3030
"""
3131

3232
params: dict[str, Any] = dict()
33-
"""The parameters to be passed to the function or callable class as keyword arguments. Defaults to
34-
an empty dictionary."""
33+
"""Parameters used by the modifier. Defaults to an empty dictionary.
34+
35+
Function modifiers receive them as keyword arguments on each call. Class modifiers access them through this
36+
configuration during construction; they are not forwarded to the constructed instance.
37+
"""
3538

3639

3740
@configclass
@@ -41,7 +44,7 @@ class DigitalFilterCfg(ModifierCfg):
4144
For more information, please check the :class:`DigitalFilter` class.
4245
"""
4346

44-
func: type[modifier.DigitalFilter] = modifier.DigitalFilter
47+
func: type[DigitalFilter] | str = "{DIR}.modifier:DigitalFilter"
4548
"""The digital filter function to be called for applying the filter."""
4649

4750
A: list[float] = MISSING
@@ -72,7 +75,7 @@ class IntegratorCfg(ModifierCfg):
7275
For more information, please check the :class:`Integrator` class.
7376
"""
7477

75-
func: type[modifier.Integrator] = modifier.Integrator
78+
func: type[Integrator] | str = "{DIR}.modifier:Integrator"
7679
"""The integrator function to be called for applying the integrator."""
7780

7881
dt: float = MISSING

source/isaaclab/test/app/test_experience_files.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,17 @@
1212

1313
pytestmark = pytest.mark.unit
1414

15-
APPS_DIR = Path(__file__).resolve().parents[4] / "apps"
16-
1715
# ``.kit`` files repeat section headers, so they cannot be parsed as TOML.
1816
_SECTION_RE = re.compile(r"^\s*\[+(?P<name>[^\[\]]+)\]+\s*$")
1917
_DEPENDENCY_RE = re.compile(r'^\s*"(?P<name>[^"]+)"\s*=')
2018

2119

22-
def _kit_dependencies(experience: str) -> set[str]:
20+
def _kit_dependencies(apps_dir: Path, experience: str) -> set[str]:
2321
"""Collect the extension names declared in an experience file's dependency sections."""
2422
dependencies: set[str] = set()
2523
in_dependencies = False
2624

27-
for line in (APPS_DIR / experience).read_text(encoding="utf-8").splitlines():
25+
for line in (apps_dir / experience).read_text(encoding="utf-8").splitlines():
2826
section = _SECTION_RE.match(line)
2927
if section is not None:
3028
in_dependencies = section.group("name").strip() == "dependencies"
@@ -38,9 +36,9 @@ def _kit_dependencies(experience: str) -> set[str]:
3836

3937

4038
@pytest.mark.parametrize("experience", ["isaaclab.python.kit", "isaaclab.python.headless.kit"])
41-
def test_base_experiences_enable_native_storage(experience: str):
39+
def test_base_experiences_enable_native_storage(source_checkout_root: Path, experience: str):
4240
"""Test the base experiences load the extension that applies ``ISAACSIM_ASSET_ROOT``."""
43-
assert "isaacsim.storage.native" in _kit_dependencies(experience)
41+
assert "isaacsim.storage.native" in _kit_dependencies(source_checkout_root / "apps", experience)
4442

4543

4644
@pytest.mark.parametrize(
@@ -52,6 +50,6 @@ def test_base_experiences_enable_native_storage(experience: str):
5250
("isaaclab.python.xr.openxr.headless.kit", "isaaclab.python.xr.openxr"),
5351
],
5452
)
55-
def test_derived_experiences_inherit_native_storage(experience: str, base: str):
53+
def test_derived_experiences_inherit_native_storage(source_checkout_root: Path, experience: str, base: str):
5654
"""Test the derived experiences inherit native storage from a base experience."""
57-
assert base in _kit_dependencies(experience)
55+
assert base in _kit_dependencies(source_checkout_root / "apps", experience)

source/isaaclab/test/cli/test_install.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,11 +476,12 @@ class TestPinkIkStack:
476476
stack from there instead of mirroring the versions.
477477
"""
478478

479-
def test_stack_derived_from_root_pyproject_pins(self):
479+
def test_stack_derived_from_root_pyproject_pins(self, source_checkout_root: Path):
480480
"""The derived stack covers every stack package, exactly pinned, markers stripped."""
481481
from isaaclab.cli.commands import install
482482

483-
stack = install._pink_ik_stack()
483+
with mock.patch.object(install, "ISAACLAB_ROOT", source_checkout_root):
484+
stack = install._pink_ik_stack()
484485
assert [install._requirement_name(r) for r in stack] == list(install._PINK_IK_PACKAGES)
485486
assert any(r.startswith("pin-pink==") for r in stack), "pin-pink must stay exactly pinned"
486487
assert any(r.startswith("daqp==") for r in stack), "daqp must stay exactly pinned"

0 commit comments

Comments
 (0)