Skip to content

[Bug Report] V3.0.0-Beta: Observation ModifierCfg func is not resolved from ResolvableString before signature validation #6067

@ecstayalive

Description

@ecstayalive

[Bug Report] Observation ModifierCfg func is not resolved from ResolvableString before signature validation

Descri

When using a class-based observation modifier through ModifierCfg, environment creation fails if the config has gone through Hydra / from_dict() and the modifier func is represented as a callable string / ResolvableString.

The failure happens in ObservationManager._prepare_terms() when validating modifier parameters. For regular observation term functions, Isaac Lab resolves string callables via string_to_callable, but modifier functions do not appear to receive the same resolution before inspect.signature(mod_cfg.func) is called.

As a result, inspect.signature() sees ResolvableString.__call__(*args, **kwargs) instead of the modifier class or modifier instance signature. This makes the manager believe the modifier expects a mandatory parameter named kwargs.

Observed error:

ValueError: Modifier 'ModifierCfg(func='{repo}.mdp.observation:DelayedObsModifier', params={'min_delay': 1, 'max_delay': 3})' of observation term 'joint_pos' expects mandatory parameters: ['kwargs'] and optional parameters: [], but received: ['min_delay', 'max_delay'].

Steps to reproduce

Use a class-based observation modifier in an observation term:

from collections.abc import Sequence

import torch
from isaaclab.utils.buffers import DelayBuffer
from isaaclab.utils.modifiers import ModifierBase, ModifierCfg


class DelayedObsModifier(ModifierBase):
    def __init__(self, cfg: ModifierCfg, data_dim: tuple[int, ...], device: str) -> None:
        super().__init__(cfg, data_dim, device)
        self.max_delay = cfg.params.get("max_delay", 0)
        self.min_delay = cfg.params.get("min_delay", 0)
        self.delay_buffer = DelayBuffer(
            history_length=self.max_delay,
            batch_size=data_dim[0],
            device=device,
        )

    def reset(self, env_ids: Sequence[int] | None = None):
        self.delay_buffer.reset(env_ids)

    def __call__(self, data: torch.Tensor) -> torch.Tensor:
        return self.delay_buffer.compute(data)

Configure an observation term with the modifier:

joint_pos = ObsTerm(
    func=mdp.joint_pos,
    modifiers=[
        ModifierCfg(
            func=DelayedObsModifier,
            params={"min_delay": 1, "max_delay": 3},
        )
    ],
)

Run the task through the Hydra training flow, so that the environment config is serialized and restored through env_cfg.from_dict(hydra_cfg["env"]).

During environment creation, the modifier config becomes similar to:

ModifierCfg(
    func='my_package.my_module:DelayedObsModifier',
    params={'min_delay': 1, 'max_delay': 3},
)

Then ObservationManager fails during _prepare_terms() with:

ValueError: Modifier 'ModifierCfg(func='my_package.my_module:DelayedObsModifier', params={'min_delay': 1, 'max_delay': 3})' of observation term 'joint_pos' expects mandatory parameters: ['kwargs'] and optional parameters: [], but received: ['min_delay', 'max_delay'].

A minimal root-cause check:

import inspect
from isaaclab.utils.modifiers import ModifierCfg
from isaaclab.utils.string import ResolvableString

cfg = ModifierCfg(
    func=ResolvableString("my_package.my_module:DelayedObsModifier"),
    params={"min_delay": 1, "max_delay": 3},
)

print(inspect.signature(cfg.func))
# Actual: (*args, **kwargs)

Expected behavior: mod_cfg.func should be resolved to the actual modifier class before class checks and signature validation. The class-based modifier should then be instantiated, and its call signature should be (data: torch.Tensor) -> torch.Tensor.

System Info

  • tag: v3.0.0-beta
  • Isaac Sim Version: 6.0
  • OS: Ubuntu 24.04
  • GPU: RTX 5060 mobile
  • CUDA: 12.8
  • GPU Driver: 595.71.05

Additional context

The issue seems to be caused by missing callable-string resolution for observation modifiers.

In isaaclab/managers/manager_base.py, normal manager term functions are resolved when term_cfg.func is a string. However, in isaaclab/managers/observation_manager.py, modifier functions are checked with inspect.isclass(mod_cfg.func) and later inspect.signature(mod_cfg.func) without first resolving string / ResolvableString callables.

A potential fix would be to resolve mod_cfg.func before the class check in ObservationManager._prepare_terms():

from isaaclab.utils import string_to_callable

...

if isinstance(mod_cfg.func, str):
    mod_cfg.func = string_to_callable(str(mod_cfg.func))

if inspect.isclass(mod_cfg.func):
    ...

This would make class-based modifiers behave consistently whether the config is used directly or after Hydra / from_dict() processing.

Checklist

  • I have checked that there is no similar issue in the repo (required)
  • I have checked that the issue is not in running Isaac Sim itself and is related to the repo

Acceptance Criteria

  • ModifierCfg.func for observation modifiers is resolved from callable strings / ResolvableString before class checks and signature validation.
  • Class-based observation modifiers work both when config objects are used directly and when configs are restored through Hydra / from_dict().
  • Existing function-based modifiers continue to pass parameter validation unchanged.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions