Unverified Commit 41a9dd44 authored by jtigue-bdai's avatar jtigue-bdai Committed by GitHub

Allows configclass `to_dict` operation to handle a list of configclasses (#1227)

# Description

This PR add in the ability to properly convert configclass to dict if a
configclass instance contains a list of configclasses.

Fixes #1219 

## Type of change

- Bug fix (non-breaking change which fixes an issue)

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
parent 9c7238d2
[package] [package]
# Note: Semantic Versioning is used: https://semver.org/ # Note: Semantic Versioning is used: https://semver.org/
version = "0.27.2" version = "0.27.3"
# Description # Description
title = "Isaac Lab framework for Robot Learning" title = "Isaac Lab framework for Robot Learning"
......
Changelog Changelog
--------- ---------
0.27.3 (2024-10-22)
~~~~~~~~~~~~~~~~~~~
Fixed
^^^^^
* Fixed the issue with using list or tuples of ``configclass`` within a ``configclass``. Earlier, the list of
configclass objects were not converted to dictionary properly when ``to_dict`` function was called.
0.27.2 (2024-10-21) 0.27.2 (2024-10-21)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
......
...@@ -40,8 +40,10 @@ def class_to_dict(obj: object) -> dict[str, Any]: ...@@ -40,8 +40,10 @@ def class_to_dict(obj: object) -> dict[str, Any]:
# convert object to dictionary # convert object to dictionary
if isinstance(obj, dict): if isinstance(obj, dict):
obj_dict = obj obj_dict = obj
else: elif hasattr(obj, "__dict__"):
obj_dict = obj.__dict__ obj_dict = obj.__dict__
else:
return obj
# convert to dictionary # convert to dictionary
data = dict() data = dict()
...@@ -55,6 +57,8 @@ def class_to_dict(obj: object) -> dict[str, Any]: ...@@ -55,6 +57,8 @@ def class_to_dict(obj: object) -> dict[str, Any]:
# check if attribute is a dictionary # check if attribute is a dictionary
elif hasattr(value, "__dict__") or isinstance(value, dict): elif hasattr(value, "__dict__") or isinstance(value, dict):
data[key] = class_to_dict(value) data[key] = class_to_dict(value)
elif isinstance(value, (list, tuple)):
data[key] = type(value)([class_to_dict(v) for v in value])
else: else:
data[key] = value data[key] = value
return data return data
......
...@@ -23,7 +23,7 @@ import unittest ...@@ -23,7 +23,7 @@ import unittest
from collections.abc import Callable from collections.abc import Callable
from dataclasses import MISSING, asdict, field from dataclasses import MISSING, asdict, field
from functools import wraps from functools import wraps
from typing import ClassVar from typing import Any, ClassVar
from omni.isaac.lab.utils.configclass import configclass from omni.isaac.lab.utils.configclass import configclass
from omni.isaac.lab.utils.dict import class_to_dict, dict_to_md5_hash, update_class_from_dict from omni.isaac.lab.utils.dict import class_to_dict, dict_to_md5_hash, update_class_from_dict
...@@ -85,6 +85,11 @@ def double(x): ...@@ -85,6 +85,11 @@ def double(x):
return 2 * x return 2 * x
@configclass
class ModifierCfg:
params: dict[str, Any] = {"A": 1, "B": 2}
@configclass @configclass
class ViewerCfg: class ViewerCfg:
eye: list = [7.5, 7.5, 7.5] # field missing on purpose eye: list = [7.5, 7.5, 7.5] # field missing on purpose
...@@ -113,6 +118,7 @@ class BasicDemoCfg: ...@@ -113,6 +118,7 @@ class BasicDemoCfg:
device_id: int = 0 device_id: int = 0
env: EnvCfg = EnvCfg() env: EnvCfg = EnvCfg()
robot_default_state: RobotDefaultStateCfg = RobotDefaultStateCfg() robot_default_state: RobotDefaultStateCfg = RobotDefaultStateCfg()
list_config = [ModifierCfg(), ModifierCfg(params={"A": 3, "B": 4})]
@configclass @configclass
...@@ -381,6 +387,7 @@ basic_demo_cfg_correct = { ...@@ -381,6 +387,7 @@ basic_demo_cfg_correct = {
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], "dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
}, },
"device_id": 0, "device_id": 0,
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
} }
basic_demo_cfg_change_correct = { basic_demo_cfg_change_correct = {
...@@ -392,6 +399,7 @@ basic_demo_cfg_change_correct = { ...@@ -392,6 +399,7 @@ basic_demo_cfg_change_correct = {
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], "dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
}, },
"device_id": 0, "device_id": 0,
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
} }
basic_demo_cfg_change_with_none_correct = { basic_demo_cfg_change_with_none_correct = {
...@@ -403,6 +411,19 @@ basic_demo_cfg_change_with_none_correct = { ...@@ -403,6 +411,19 @@ basic_demo_cfg_change_with_none_correct = {
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], "dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
}, },
"device_id": 0, "device_id": 0,
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
}
basic_demo_cfg_change_with_tuple_correct = {
"env": {"num_envs": 56, "episode_length": 2000, "viewer": {"eye": [7.5, 7.5, 7.5], "lookat": [0.0, 0.0, 0.0]}},
"robot_default_state": {
"pos": (0.0, 0.0, 0.0),
"rot": (1.0, 0.0, 0.0, 0.0),
"dof_pos": (0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
},
"device_id": 0,
"list_config": [{"params": {"A": -1, "B": -2}}, {"params": {"A": -3, "B": -4}}],
} }
basic_demo_cfg_nested_dict_and_list = { basic_demo_cfg_nested_dict_and_list = {
...@@ -496,7 +517,7 @@ class TestConfigClass(unittest.TestCase): ...@@ -496,7 +517,7 @@ class TestConfigClass(unittest.TestCase):
def test_dict_conversion_order(self): def test_dict_conversion_order(self):
"""Tests that order is conserved when converting to dictionary.""" """Tests that order is conserved when converting to dictionary."""
true_outer_order = ["device_id", "env", "robot_default_state"] true_outer_order = ["device_id", "env", "robot_default_state", "list_config"]
true_env_order = ["num_envs", "episode_length", "viewer"] true_env_order = ["num_envs", "episode_length", "viewer"]
# create config # create config
cfg = BasicDemoCfg() cfg = BasicDemoCfg()
...@@ -514,7 +535,7 @@ class TestConfigClass(unittest.TestCase): ...@@ -514,7 +535,7 @@ class TestConfigClass(unittest.TestCase):
self.assertEqual(label, parsed_value) self.assertEqual(label, parsed_value)
# check ordering when copied # check ordering when copied
cfg_dict_copied = copy.deepcopy(cfg_dict) cfg_dict_copied = copy.deepcopy(cfg_dict)
cfg_dict_copied.pop("robot_default_state") cfg_dict_copied.pop("list_config")
# check ordering # check ordering
for label, parsed_value in zip(true_outer_order, cfg_dict_copied.keys()): for label, parsed_value in zip(true_outer_order, cfg_dict_copied.keys()):
self.assertEqual(label, parsed_value) self.assertEqual(label, parsed_value)
...@@ -551,6 +572,13 @@ class TestConfigClass(unittest.TestCase): ...@@ -551,6 +572,13 @@ class TestConfigClass(unittest.TestCase):
update_class_from_dict(cfg, cfg_dict) update_class_from_dict(cfg, cfg_dict)
self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_none_correct) self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_none_correct)
def test_config_update_dict_tuple(self):
"""Test updating configclass using a dictionary that modifies a tuple."""
cfg = BasicDemoCfg()
cfg_dict = {"list_config": [{"params": {"A": -1, "B": -2}}, {"params": {"A": -3, "B": -4}}]}
update_class_from_dict(cfg, cfg_dict)
self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_tuple_correct)
def test_config_update_nested_dict(self): def test_config_update_nested_dict(self):
"""Test updating configclass with sub-dictionaries.""" """Test updating configclass with sub-dictionaries."""
cfg = NestedDictAndListCfg() cfg = NestedDictAndListCfg()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment