Skip to content

Commit 002fec4

Browse files
authored
Fixes Gymnasium spaces issues due to Hydra/OmegaConf limitations (#1306)
# Description Fixed issues with defining Gymnasium spaces in Direct workflows due to Hydra/OmegaConf limitations with non-primitive types (see #1264 (reply in thread)) ``` omegaconf.errors.UnsupportedValueType: Value 'XXXXX' is not a supported primitive type ``` ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - 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` - [ ] 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 <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task -->
1 parent 4c91535 commit 002fec4

File tree

7 files changed

+211
-3
lines changed

7 files changed

+211
-3
lines changed

source/extensions/omni.isaac.lab/config/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "0.27.4"
4+
version = "0.27.5"
55

66
# Description
77
title = "Isaac Lab framework for Robot Learning"

source/extensions/omni.isaac.lab/docs/CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Changelog
22
---------
33

4+
0.27.5 (2024-10-25)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Added
8+
^^^^^
9+
10+
* Added utilities for serializing/deserializing Gymnasium spaces.
11+
12+
413
0.27.4 (2024-10-18)
514
~~~~~~~~~~~~~~~~~~~
615

source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/spaces.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# SPDX-License-Identifier: BSD-3-Clause
55

66
import gymnasium as gym
7+
import json
78
import numpy as np
89
import torch
910
from typing import Any
@@ -90,3 +91,131 @@ def tensorize(s, x):
9091

9192
sample = (gym.vector.utils.batch_space(space, batch_size) if batch_size > 0 else space).sample()
9293
return tensorize(space, sample)
94+
95+
96+
def serialize_space(space: SpaceType) -> str:
97+
"""Serialize a space specification as JSON.
98+
99+
Args:
100+
space: Space specification.
101+
102+
Returns:
103+
Serialized JSON representation.
104+
"""
105+
# Gymnasium spaces
106+
if isinstance(space, gym.spaces.Discrete):
107+
return json.dumps({"type": "gymnasium", "space": "Discrete", "n": int(space.n)})
108+
elif isinstance(space, gym.spaces.Box):
109+
return json.dumps({
110+
"type": "gymnasium",
111+
"space": "Box",
112+
"low": space.low.tolist(),
113+
"high": space.high.tolist(),
114+
"shape": space.shape,
115+
})
116+
elif isinstance(space, gym.spaces.MultiDiscrete):
117+
return json.dumps({"type": "gymnasium", "space": "MultiDiscrete", "nvec": space.nvec.tolist()})
118+
elif isinstance(space, gym.spaces.Tuple):
119+
return json.dumps({"type": "gymnasium", "space": "Tuple", "spaces": tuple(map(serialize_space, space.spaces))})
120+
elif isinstance(space, gym.spaces.Dict):
121+
return json.dumps(
122+
{"type": "gymnasium", "space": "Dict", "spaces": {k: serialize_space(v) for k, v in space.spaces.items()}}
123+
)
124+
# Python data types
125+
# Box
126+
elif isinstance(space, int) or (isinstance(space, list) and all(isinstance(x, int) for x in space)):
127+
return json.dumps({"type": "python", "space": "Box", "value": space})
128+
# Discrete
129+
elif isinstance(space, set) and len(space) == 1:
130+
return json.dumps({"type": "python", "space": "Discrete", "value": next(iter(space))})
131+
# MultiDiscrete
132+
elif isinstance(space, list) and all(isinstance(x, set) and len(x) == 1 for x in space):
133+
return json.dumps({"type": "python", "space": "MultiDiscrete", "value": [next(iter(x)) for x in space]})
134+
# composite spaces
135+
# Tuple
136+
elif isinstance(space, tuple):
137+
return json.dumps({"type": "python", "space": "Tuple", "value": [serialize_space(x) for x in space]})
138+
# Dict
139+
elif isinstance(space, dict):
140+
return json.dumps(
141+
{"type": "python", "space": "Dict", "value": {k: serialize_space(v) for k, v in space.items()}}
142+
)
143+
raise ValueError(f"Unsupported space ({space})")
144+
145+
146+
def deserialize_space(string: str) -> gym.spaces.Space:
147+
"""Deserialize a space specification encoded as JSON.
148+
149+
Args:
150+
string: Serialized JSON representation.
151+
152+
Returns:
153+
Space specification.
154+
"""
155+
obj = json.loads(string)
156+
# Gymnasium spaces
157+
if obj["type"] == "gymnasium":
158+
if obj["space"] == "Discrete":
159+
return gym.spaces.Discrete(n=obj["n"])
160+
elif obj["space"] == "Box":
161+
return gym.spaces.Box(low=np.array(obj["low"]), high=np.array(obj["high"]), shape=obj["shape"])
162+
elif obj["space"] == "MultiDiscrete":
163+
return gym.spaces.MultiDiscrete(nvec=np.array(obj["nvec"]))
164+
elif obj["space"] == "Tuple":
165+
return gym.spaces.Tuple(spaces=tuple(map(deserialize_space, obj["spaces"])))
166+
elif obj["space"] == "Dict":
167+
return gym.spaces.Dict(spaces={k: deserialize_space(v) for k, v in obj["spaces"].items()})
168+
else:
169+
raise ValueError(f"Unsupported space ({obj['spaces']})")
170+
# Python data types
171+
elif obj["type"] == "python":
172+
if obj["space"] == "Discrete":
173+
return {obj["value"]}
174+
elif obj["space"] == "Box":
175+
return obj["value"]
176+
elif obj["space"] == "MultiDiscrete":
177+
return [{x} for x in obj["value"]]
178+
elif obj["space"] == "Tuple":
179+
return tuple(map(deserialize_space, obj["value"]))
180+
elif obj["space"] == "Dict":
181+
return {k: deserialize_space(v) for k, v in obj["value"].items()}
182+
else:
183+
raise ValueError(f"Unsupported space ({obj['spaces']})")
184+
else:
185+
raise ValueError(f"Unsupported type ({obj['type']})")
186+
187+
188+
def replace_env_cfg_spaces_with_strings(env_cfg: object) -> object:
189+
"""Replace spaces objects with their serialized JSON representations in an environment config.
190+
191+
Args:
192+
env_cfg: Environment config instance.
193+
194+
Returns:
195+
Environment config instance with spaces replaced if any.
196+
"""
197+
for attr in ["observation_space", "action_space", "state_space"]:
198+
if hasattr(env_cfg, attr):
199+
setattr(env_cfg, attr, serialize_space(getattr(env_cfg, attr)))
200+
for attr in ["observation_spaces", "action_spaces"]:
201+
if hasattr(env_cfg, attr):
202+
setattr(env_cfg, attr, {k: serialize_space(v) for k, v in getattr(env_cfg, attr).items()})
203+
return env_cfg
204+
205+
206+
def replace_strings_with_env_cfg_spaces(env_cfg: object) -> object:
207+
"""Replace spaces objects with their serialized JSON representations in an environment config.
208+
209+
Args:
210+
env_cfg: Environment config instance.
211+
212+
Returns:
213+
Environment config instance with spaces replaced if any.
214+
"""
215+
for attr in ["observation_space", "action_space", "state_space"]:
216+
if hasattr(env_cfg, attr):
217+
setattr(env_cfg, attr, deserialize_space(getattr(env_cfg, attr)))
218+
for attr in ["observation_spaces", "action_spaces"]:
219+
if hasattr(env_cfg, attr):
220+
setattr(env_cfg, attr, {k: deserialize_space(v) for k, v in getattr(env_cfg, attr).items()})
221+
return env_cfg

source/extensions/omni.isaac.lab/test/envs/test_spaces_utils.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import unittest
2727
from gymnasium.spaces import Box, Dict, Discrete, MultiDiscrete, Tuple
2828

29-
from omni.isaac.lab.envs.utils.spaces import sample_space, spec_to_gym_space
29+
from omni.isaac.lab.envs.utils.spaces import deserialize_space, sample_space, serialize_space, spec_to_gym_space
3030

3131

3232
class TestSpacesUtils(unittest.TestCase):
@@ -104,6 +104,59 @@ def test_sample_space(self):
104104
self.assertIsInstance(sample, dict)
105105
self._check_tensorized(sample, batch_size=5)
106106

107+
def test_space_serialization_deserialization(self):
108+
# fundamental spaces
109+
# Box
110+
space = 1
111+
output = deserialize_space(serialize_space(space))
112+
self.assertEqual(space, output)
113+
space = [1, 2, 3, 4, 5]
114+
output = deserialize_space(serialize_space(space))
115+
self.assertEqual(space, output)
116+
space = Box(low=-1.0, high=1.0, shape=(1, 2))
117+
output = deserialize_space(serialize_space(space))
118+
self.assertIsInstance(output, Box)
119+
self.assertTrue((space.low == output.low).all())
120+
self.assertTrue((space.high == output.high).all())
121+
self.assertEqual(space.shape, output.shape)
122+
# Discrete
123+
space = {2}
124+
output = deserialize_space(serialize_space(space))
125+
self.assertEqual(space, output)
126+
space = Discrete(2)
127+
output = deserialize_space(serialize_space(space))
128+
self.assertIsInstance(output, Discrete)
129+
self.assertEqual(space.n, output.n)
130+
# MultiDiscrete
131+
space = [{1}, {2}, {3}]
132+
output = deserialize_space(serialize_space(space))
133+
self.assertEqual(space, output)
134+
space = MultiDiscrete(np.array([1, 2, 3]))
135+
output = deserialize_space(serialize_space(space))
136+
self.assertIsInstance(output, MultiDiscrete)
137+
self.assertTrue((space.nvec == output.nvec).all())
138+
# composite spaces
139+
# Tuple
140+
space = ([1, 2, 3, 4, 5], {2}, [{1}, {2}, {3}])
141+
output = deserialize_space(serialize_space(space))
142+
self.assertEqual(space, output)
143+
space = Tuple((Box(-1, 1, shape=(1,)), Discrete(2)))
144+
output = deserialize_space(serialize_space(space))
145+
self.assertIsInstance(output, Tuple)
146+
self.assertEqual(len(output), 2)
147+
self.assertIsInstance(output[0], Box)
148+
self.assertIsInstance(output[1], Discrete)
149+
# Dict
150+
space = {"box": [1, 2, 3, 4, 5], "discrete": {2}, "multi_discrete": [{1}, {2}, {3}]}
151+
output = deserialize_space(serialize_space(space))
152+
self.assertEqual(space, output)
153+
space = Dict({"box": Box(-1, 1, shape=(1,)), "discrete": Discrete(2)})
154+
output = deserialize_space(serialize_space(space))
155+
self.assertIsInstance(output, Dict)
156+
self.assertEqual(len(output), 2)
157+
self.assertIsInstance(output["box"], Box)
158+
self.assertIsInstance(output["discrete"], Discrete)
159+
107160
"""
108161
Helper functions.
109162
"""

source/extensions/omni.isaac.lab_tasks/config/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "0.10.9"
4+
version = "0.10.10"
55

66
# Description
77
title = "Isaac Lab Environments"

source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Changelog
22
---------
33

4+
0.10.10 (2024-10-25)
5+
~~~~~~~~~~~~~~~~~~~~
6+
7+
Fixed
8+
^^^^^
9+
10+
* Fixed issues with defining Gymnasium spaces in Direct workflows due to Hydra/OmegaConf limitations with non-primitive types.
11+
12+
413
0.10.9 (2024-10-22)
514
~~~~~~~~~~~~~~~~~~~
615

source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/hydra.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
raise ImportError("Hydra is not installed. Please install it by running 'pip install hydra-core'.")
1818

1919
from omni.isaac.lab.envs import DirectRLEnvCfg, ManagerBasedRLEnvCfg
20+
from omni.isaac.lab.envs.utils.spaces import replace_env_cfg_spaces_with_strings, replace_strings_with_env_cfg_spaces
2021
from omni.isaac.lab.utils import replace_slices_with_strings, replace_strings_with_slices
2122

2223
from omni.isaac.lab_tasks.utils.parse_cfg import load_cfg_from_registry
@@ -40,6 +41,9 @@ def register_task_to_hydra(
4041
# load the configurations
4142
env_cfg = load_cfg_from_registry(task_name, "env_cfg_entry_point")
4243
agent_cfg = load_cfg_from_registry(task_name, agent_cfg_entry_point)
44+
# replace gymnasium spaces with strings because OmegaConf does not support them.
45+
# this must be done before converting the env configs to dictionary to avoid internal reinterpretations
46+
replace_env_cfg_spaces_with_strings(env_cfg)
4347
# convert the configs to dictionary
4448
env_cfg_dict = env_cfg.to_dict()
4549
if isinstance(agent_cfg, dict):
@@ -83,6 +87,10 @@ def hydra_main(hydra_env_cfg: DictConfig, env_cfg=env_cfg, agent_cfg=agent_cfg):
8387
hydra_env_cfg = replace_strings_with_slices(hydra_env_cfg)
8488
# update the configs with the Hydra command line arguments
8589
env_cfg.from_dict(hydra_env_cfg["env"])
90+
# replace strings that represent gymnasium spaces because OmegaConf does not support them.
91+
# this must be done after converting the env configs from dictionary to avoid internal reinterpretations
92+
replace_strings_with_env_cfg_spaces(env_cfg)
93+
# get agent configs
8694
if isinstance(agent_cfg, dict):
8795
agent_cfg = hydra_env_cfg["agent"]
8896
else:

0 commit comments

Comments
 (0)