Skip to content

Commit cfa7225

Browse files
committed
Use config type for stateful observation modifiers
1 parent cca25c2 commit cfa7225

12 files changed

Lines changed: 213 additions & 65 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Changed
2+
^^^^^^^
3+
4+
* **Breaking:** Required class-based observation modifiers to use
5+
:class:`~isaaclab.utils.modifiers.ModifierBaseCfg`, allowing their lazy callable references to be instantiated
6+
based on the configuration type. Replace ``ModifierCfg(func=MyModifier, params=...)`` with
7+
``ModifierBaseCfg(func=MyModifier, params=...)``; function modifiers continue to use
8+
:class:`~isaaclab.utils.modifiers.ModifierCfg`.
9+
10+
Fixed
11+
^^^^^
12+
13+
* Fixed function and class-based observation modifiers failing after a configuration dictionary round-trip turned
14+
their callable references into :class:`~isaaclab.utils.string.ResolvableString` values.

source/isaaclab/isaaclab/managers/manager_term_cfg.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ class ObservationTermCfg(ManagerTermBaseCfg):
168168
or stateful, and can be used to apply transformations to the observation data. For example,
169169
a modifier can be used to normalize the observation data or to apply a rolling average.
170170
171-
For more information on modifiers, see the :class:`~isaaclab.utils.modifiers.ModifierCfg` class.
171+
For more information on modifiers, see :class:`~isaaclab.utils.modifiers.ModifierCfg` and
172+
:class:`~isaaclab.utils.modifiers.ModifierBaseCfg`.
172173
"""
173174

174175
noise: NoiseCfg | NoiseModelCfg | None = None

source/isaaclab/isaaclab/managers/observation_manager.py

Lines changed: 39 additions & 46 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, modifiers.ModifierBaseCfg):
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:
@@ -577,52 +580,8 @@ def _prepare_terms(self):
577580

578581
# prepare modifiers for each observation
579582
if term_cfg.modifiers is not None:
580-
# initialize list of modifiers for term
581583
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:
596-
raise TypeError(
597-
f"Modifier configuration '{mod_cfg}' of observation term '{term_name}' is not of"
598-
f" required type ModifierCfg, Received: '{type(mod_cfg)}'"
599-
)
600-
601-
# check if function is callable
602-
if not callable(mod_cfg.func):
603-
raise AttributeError(
604-
f"Modifier '{mod_cfg}' of observation term '{term_name}' is not callable."
605-
f" Received: {mod_cfg.func}"
606-
)
607-
608-
# TODO(jichuanh): improvement can be made in two ways:
609-
# 1. modifier specific check can be done in the modifier class
610-
# 2. general param vs function matching check can be a common utility
611-
# check if term's arguments are matched by params
612-
term_params = list(mod_cfg.params.keys())
613-
args = inspect.signature(mod_cfg.func).parameters
614-
args_with_defaults = [arg for arg in args if args[arg].default is not inspect.Parameter.empty]
615-
args_without_defaults = [arg for arg in args if args[arg].default is inspect.Parameter.empty]
616-
args = args_without_defaults + args_with_defaults
617-
# ignore first two arguments for env and env_ids
618-
# Think: Check for cases when kwargs are set inside the function?
619-
if len(args) > 1:
620-
if set(args[1:]) != set(term_params + args_with_defaults):
621-
raise ValueError(
622-
f"Modifier '{mod_cfg}' of observation term '{term_name}' expects"
623-
f" mandatory parameters: {args_without_defaults[1:]}"
624-
f" and optional parameters: {args_with_defaults}, but received: {term_params}."
625-
)
584+
self._prepare_modifier(mod_cfg, term_name, obs_dims)
626585

627586
# prepare noise model classes
628587
if term_cfg.noise is not None and isinstance(term_cfg.noise, noise.NoiseModelCfg):
@@ -659,3 +618,37 @@ def _prepare_terms(self):
659618
term_cfg.func.reset()
660619
# add history buffers for each group
661620
self._group_obs_term_history_buffer[group_name] = group_entry_history_buffer
621+
622+
def _prepare_modifier(self, mod_cfg: modifiers.ModifierCfg, term_name: str, obs_dims: tuple[int, ...]) -> None:
623+
"""Validate a modifier configuration and construct its stateful implementation."""
624+
if not isinstance(mod_cfg, modifiers.ModifierCfg):
625+
raise TypeError(
626+
f"Modifier configuration '{mod_cfg}' of observation term '{term_name}' is not of"
627+
f" required type ModifierCfg, Received: '{type(mod_cfg)}'"
628+
)
629+
if not callable(mod_cfg.func):
630+
raise AttributeError(
631+
f"Modifier '{mod_cfg}' of observation term '{term_name}' is not callable. Received: {mod_cfg.func}"
632+
)
633+
634+
if isinstance(mod_cfg, modifiers.ModifierBaseCfg):
635+
mod_cfg.func = mod_cfg.func(cfg=mod_cfg, data_dim=obs_dims, device=self._env.device)
636+
if not isinstance(mod_cfg.func, modifiers.ModifierBase):
637+
raise TypeError(
638+
f"Modifier function '{mod_cfg.func}' for observation term '{term_name}' is not an instance of"
639+
f" 'ModifierBase'. Received: '{type(mod_cfg.func)}'."
640+
)
641+
self._group_obs_class_instances.append(mod_cfg.func)
642+
return
643+
644+
term_params = list(mod_cfg.params.keys())
645+
args = inspect.signature(mod_cfg.func).parameters
646+
args_with_defaults = [arg for arg in args if args[arg].default is not inspect.Parameter.empty]
647+
args_without_defaults = [arg for arg in args if args[arg].default is inspect.Parameter.empty]
648+
args = args_without_defaults + args_with_defaults
649+
if len(args) > 1 and set(args[1:]) != set(term_params + args_with_defaults):
650+
raise ValueError(
651+
f"Modifier '{mod_cfg}' of observation term '{term_name}' expects mandatory parameters:"
652+
f" {args_without_defaults[1:]} and optional parameters: {args_with_defaults}, but received:"
653+
f" {term_params}."
654+
)

source/isaaclab/isaaclab/utils/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ __all__ = [
3030
"convert_faces_to_triangles",
3131
"PRIMITIVE_MESH_TYPES",
3232
"ModifierCfg",
33+
"ModifierBaseCfg",
3334
"ModifierBase",
3435
"DigitalFilter",
3536
"DigitalFilterCfg",
@@ -80,6 +81,7 @@ from .mesh import (
8081
PRIMITIVE_MESH_TYPES,
8182
)
8283
from .modifiers import (
84+
ModifierBaseCfg,
8385
ModifierCfg,
8486
ModifierBase,
8587
DigitalFilter,

source/isaaclab/isaaclab/utils/modifiers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
cfg = modifiers.ModifierCfg(func=modifiers.clip, params={"bounds": (0.0, torch.inf)})
2929
3030
# apply the modifier
31-
my_modified_tensor = cfg.func(my_tensor, cfg)
31+
my_modified_tensor = cfg.func(my_tensor, **cfg.params)
3232
3333
3434
Usage with a class modifier:

source/isaaclab/isaaclab/utils/modifiers/__init__.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
__all__ = [
77
"ModifierCfg",
8+
"ModifierBaseCfg",
89
"DigitalFilterCfg",
910
"IntegratorCfg",
1011
"ModifierBase",
@@ -15,6 +16,6 @@ __all__ = [
1516
"scale",
1617
]
1718

18-
from .modifier_cfg import ModifierCfg, DigitalFilterCfg, IntegratorCfg
19+
from .modifier_cfg import ModifierBaseCfg, ModifierCfg, DigitalFilterCfg, IntegratorCfg
1920
from .modifier_base import ModifierBase
2021
from .modifier import DigitalFilter, Integrator, bias, clip, scale

source/isaaclab/isaaclab/utils/modifiers/modifier_base.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import torch
1313

1414
if TYPE_CHECKING:
15-
from .modifier_cfg import ModifierCfg
15+
from .modifier_cfg import ModifierBaseCfg
1616

1717

1818
class ModifierBase(ABC):
@@ -31,19 +31,19 @@ class ModifierBase(ABC):
3131
3232
from isaaclab.utils import modifiers
3333
34-
# define custom keyword arguments to pass to ModifierCfg
34+
# define custom configuration values for the stateful modifier
3535
kwarg_dict = {"arg_1": VAL_1, "arg_2": VAL_2}
3636
3737
# create modifier configuration object
38-
# func is the class name of the modifier and params is the dictionary of arguments
39-
modifier_config = modifiers.ModifierCfg(func=modifiers.ModifierBase, params=kwarg_dict)
38+
# func is the class name and params are available to its constructor through the config
39+
modifier_config = modifiers.ModifierBaseCfg(func=MyModifier, params=kwarg_dict)
4040
4141
# define modifier instance
42-
my_modifier = modifiers.ModifierBase(cfg=modifier_config)
42+
my_modifier = modifier_config.func(cfg=modifier_config, data_dim=(256, 128), device="cuda")
4343
4444
"""
4545

46-
def __init__(self, cfg: ModifierCfg, data_dim: tuple[int, ...], device: str) -> None:
46+
def __init__(self, cfg: ModifierBaseCfg, data_dim: tuple[int, ...], device: str) -> None:
4747
"""Initializes the modifier class.
4848
4949
Args:

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,34 @@
1616

1717
@configclass
1818
class ModifierCfg:
19-
"""Configuration parameters modifiers"""
19+
"""Configuration for a stateless function modifier.
20+
21+
Use :class:`ModifierBaseCfg` for a class-based stateful modifier.
22+
"""
2023

2124
func: Callable[..., torch.Tensor] = MISSING
22-
"""Function or callable class used by modifier.
25+
"""Function used by the modifier.
2326
2427
The function must take a torch tensor as the first argument. The remaining arguments are specified
2528
in the :attr:`params` attribute.
26-
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.
3029
"""
3130

3231
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."""
32+
"""The parameters passed to the function as keyword arguments. Defaults to an empty dictionary."""
33+
34+
35+
@configclass
36+
class ModifierBaseCfg(ModifierCfg):
37+
"""Configuration for a stateful modifier implemented by :class:`ModifierBase`.
38+
39+
The :attr:`func` field identifies the modifier class. The observation manager constructs the class with this
40+
configuration, the observation dimensions, and the device. The :attr:`params` values are available to the
41+
constructor through this configuration and are not forwarded to the modifier instance when it is called.
42+
"""
3543

3644

3745
@configclass
38-
class DigitalFilterCfg(ModifierCfg):
46+
class DigitalFilterCfg(ModifierBaseCfg):
3947
"""Configuration parameters for a digital filter modifier.
4048
4149
For more information, please check the :class:`DigitalFilter` class.
@@ -66,7 +74,7 @@ class DigitalFilterCfg(ModifierCfg):
6674

6775

6876
@configclass
69-
class IntegratorCfg(ModifierCfg):
77+
class IntegratorCfg(ModifierBaseCfg):
7078
"""Configuration parameters for an integrator modifier.
7179
7280
For more information, please check the :class:`Integrator` class.

source/isaaclab/isaaclab/utils/string.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ def _resolve(self) -> Callable:
220220
def __call__(self, *args, **kwargs):
221221
return self._resolve()(*args, **kwargs)
222222

223+
@property
224+
def __signature__(self) -> inspect.Signature:
225+
"""Return the signature of the lazily resolved callable."""
226+
return inspect.signature(self._resolve())
227+
223228
def _split_ref(self) -> tuple[str | None, str]:
224229
"""Parse ``module:attribute`` reference without importing."""
225230
value = str(self)

0 commit comments

Comments
 (0)