Skip to content

Commit 62087f0

Browse files
committed
Adds recorder manager in manager-based environments
1 parent f7350c7 commit 62087f0

23 files changed

+1706
-17
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
*.mp4 filter=lfs diff=lfs merge=lfs -text
1010
*.pt filter=lfs diff=lfs merge=lfs -text
1111
*.jit filter=lfs diff=lfs merge=lfs -text
12+
*.hdf5 filter=lfs diff=lfs merge=lfs -text

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Guidelines for modifications:
4141
* Brayden Zhang
4242
* Calvin Yu
4343
* Chenyu Yang
44+
* CY (Chien-Ying) Chen
4445
* David Yang
4546
* Dorsa Rohani
4647
* Felix Yu

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.11"
4+
version = "0.27.12"
55

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

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

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

4+
0.27.12 (2024-11-02)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Added
8+
^^^^^
9+
10+
* Added :class:`~omni.isaac.lab.managers.RecorderManager` and its utility classes to record data from the simulation.
11+
* Added :class:`~omni.isaac.lab.utils.datasets.EpisodeData` to store data for an episode.
12+
* Added :class:`~omni.isaac.lab.utils.datasets.DatasetFileHandlerBase` as a base class for handling dataset files.
13+
* Added :class:`~omni.isaac.lab.utils.datasets.HDF5DatasetFileHandler` as a dataset file handler implementation to
14+
export and load episodes from HDF5 files.
15+
* Added ``record_demos.py`` script to record human-teleoperated demos for a specified task and export to an HDF5 file.
16+
17+
418
0.27.11 (2024-10-31)
519
~~~~~~~~~~~~~~~~~~~~
620

source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env.py

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import omni.isaac.core.utils.torch as torch_utils
1212
import omni.log
1313

14-
from omni.isaac.lab.managers import ActionManager, EventManager, ObservationManager
14+
from omni.isaac.lab.managers import ActionManager, EventManager, ObservationManager, RecorderManager
1515
from omni.isaac.lab.scene import InteractiveScene
1616
from omni.isaac.lab.sim import SimulationContext
1717
from omni.isaac.lab.utils.timer import Timer
@@ -45,6 +45,9 @@ class ManagerBasedEnv:
4545
This includes resetting the scene to a default state, applying random pushes to the robot at different intervals
4646
of time, or randomizing properties such as mass and friction coefficients. This is useful for training
4747
and evaluating the robot in a variety of scenarios.
48+
* **Recorder Manager**: The recorder manager that handles recording data produced during different steps
49+
in the simulation. This includes recording in the beginning and end of a reset and a step. The recorded data
50+
is distinguished per episode, per environment and can be exported through a dataset file handler to a file.
4851
4952
The environment provides a unified interface for interacting with the simulation. However, it does not
5053
include task-specific quantities such as the reward function, or the termination conditions. These
@@ -153,6 +156,9 @@ def __init__(self, cfg: ManagerBasedEnvCfg):
153156
# allocate dictionary to store metrics
154157
self.extras = {}
155158

159+
# initialize observation buffers
160+
self.obs_buf = {}
161+
156162
def __del__(self):
157163
"""Cleanup for the environment."""
158164
self.close()
@@ -208,6 +214,9 @@ def load_managers(self):
208214
209215
"""
210216
# prepare the managers
217+
# -- recorder manager
218+
self.recorder_manager = RecorderManager(self.cfg.recorders, self)
219+
print("[INFO] Recorder Manager: ", self.recorder_manager)
211220
# -- action manager
212221
self.action_manager = ActionManager(self.cfg.actions, self)
213222
print("[INFO] Action Manager: ", self.action_manager)
@@ -228,15 +237,18 @@ def load_managers(self):
228237
Operations - MDP.
229238
"""
230239

231-
def reset(self, seed: int | None = None, options: dict[str, Any] | None = None) -> tuple[VecEnvObs, dict]:
232-
"""Resets all the environments and returns observations.
240+
def reset(
241+
self, seed: int | None = None, env_ids: Sequence[int] | None = None, options: dict[str, Any] | None = None
242+
) -> tuple[VecEnvObs, dict]:
243+
"""Resets the specified environments and returns observations.
233244
234-
This function calls the :meth:`_reset_idx` function to reset all the environments.
245+
This function calls the :meth:`_reset_idx` function to reset the specified environments.
235246
However, certain operations, such as procedural terrain generation, that happened during initialization
236247
are not repeated.
237248
238249
Args:
239250
seed: The seed to use for randomization. Defaults to None, in which case the seed is not set.
251+
env_ids: The environment ids to reset. Defaults to None, in which case all environments are reset.
240252
options: Additional information to specify how the environment is reset. Defaults to None.
241253
242254
Note:
@@ -245,20 +257,80 @@ def reset(self, seed: int | None = None, options: dict[str, Any] | None = None)
245257
Returns:
246258
A tuple containing the observations and extras.
247259
"""
260+
if env_ids is None:
261+
env_ids = torch.arange(self.num_envs, dtype=torch.int64, device=self.device)
262+
263+
# trigger recorder terms for pre-reset calls
264+
self.recorder_manager.record_pre_reset(env_ids)
265+
248266
# set the seed
249267
if seed is not None:
250268
self.seed(seed)
251269

252270
# reset state of scene
253-
indices = torch.arange(self.num_envs, dtype=torch.int64, device=self.device)
254-
self._reset_idx(indices)
271+
self._reset_idx(env_ids)
272+
273+
self.scene.write_data_to_sim()
274+
275+
# trigger recorder terms for post-reset calls
276+
self.recorder_manager.record_post_reset(env_ids)
277+
278+
# if sensors are added to the scene, make sure we render to reflect changes in reset
279+
if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset:
280+
self.sim.render()
281+
282+
# compute observations
283+
self.obs_buf = self.observation_manager.compute()
284+
285+
# return observations
286+
return self.obs_buf, self.extras
287+
288+
def reset_to(
289+
self,
290+
state: dict[str, dict[str, torch.Tensor]],
291+
env_ids: Sequence[int] | None,
292+
seed: int | None = None,
293+
is_relative: bool = False,
294+
) -> None:
295+
"""Resets specified environments to known states.
296+
297+
Note that this is different from reset() function as it resets the environments to specific states
298+
299+
Args:
300+
state: The state to reset the specified environments to.
301+
env_ids: The environment ids to reset. Defaults to None, in which case all environments are reset.
302+
seed: The seed to use for randomization. Defaults to None, in which case the seed is not set.
303+
is_relative: If set to True, the state is considered relative to the environment origins. Defaults to False.
304+
"""
305+
# reset all envs in the scene if env_ids is None
306+
if env_ids is None:
307+
env_ids = torch.arange(self.num_envs, dtype=torch.int64, device=self.device)
308+
309+
# trigger recorder terms for pre-reset calls
310+
self.recorder_manager.record_pre_reset(env_ids)
311+
312+
# set the seed
313+
if seed is not None:
314+
self.seed(seed)
315+
316+
self._reset_idx(env_ids)
317+
318+
# set the state
319+
self.scene.reset_to(state, env_ids, is_relative=is_relative)
320+
self.scene.write_data_to_sim()
321+
322+
# trigger recorder terms for post-reset calls
323+
self.recorder_manager.record_post_reset(env_ids)
255324

256325
# if sensors are added to the scene, make sure we render to reflect changes in reset
257326
if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset:
258327
self.sim.render()
259328

329+
# compute observations
330+
self.obs_buf = self.observation_manager.compute()
331+
260332
# return observations
261-
return self.observation_manager.compute(), self.extras
333+
return self.obs_buf, self.extras
262334

263335
def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
264336
"""Execute one time-step of the environment's dynamics.
@@ -278,6 +350,8 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
278350
# process actions
279351
self.action_manager.process_action(action.to(self.device))
280352

353+
self.recorder_manager.record_pre_step()
354+
281355
# check if we need to do rendering within the physics loop
282356
# note: checked here once to avoid multiple checks within the loop
283357
is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors()
@@ -303,8 +377,13 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
303377
if "interval" in self.event_manager.available_modes:
304378
self.event_manager.apply(mode="interval", dt=self.step_dt)
305379

380+
# -- compute observations
381+
self.prev_obs_buf = self.obs_buf
382+
self.obs_buf = self.observation_manager.compute()
383+
self.recorder_manager.record_post_step()
384+
306385
# return observations and extras
307-
return self.observation_manager.compute(), self.extras
386+
return self.obs_buf, self.extras
308387

309388
@staticmethod
310389
def seed(seed: int = -1) -> int:
@@ -334,6 +413,7 @@ def close(self):
334413
del self.action_manager
335414
del self.observation_manager
336415
del self.event_manager
416+
del self.recorder_manager
337417
del self.scene
338418
# clear callbacks and instance
339419
self.sim.clear_all_callbacks()
@@ -375,3 +455,6 @@ def _reset_idx(self, env_ids: Sequence[int]):
375455
# -- event manager
376456
info = self.event_manager.reset(env_ids)
377457
self.extras["log"].update(info)
458+
# -- recroder manager
459+
info = self.recorder_manager.reset(env_ids)
460+
self.extras["log"].update(info)

source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env_cfg.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import omni.isaac.lab.envs.mdp as mdp
1515
from omni.isaac.lab.managers import EventTermCfg as EventTerm
16+
from omni.isaac.lab.managers import RecorderManagerBaseCfg as DefaultEmptyRecorderManagerCfg
1617
from omni.isaac.lab.scene import InteractiveSceneCfg
1718
from omni.isaac.lab.sim import SimulationCfg
1819
from omni.isaac.lab.utils import configclass
@@ -78,6 +79,12 @@ class ManagerBasedEnvCfg:
7879
Please refer to the :class:`omni.isaac.lab.scene.InteractiveSceneCfg` class for more details.
7980
"""
8081

82+
recorders: object = DefaultEmptyRecorderManagerCfg()
83+
"""Recorder settings. Defaults to recording nothing.
84+
85+
Please refer to the :class:`omni.isaac.lab.managers.RecorderManager` class for more details.
86+
"""
87+
8188
observations: object = MISSING
8289
"""Observation space settings.
8390

source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
158158
# process actions
159159
self.action_manager.process_action(action.to(self.device))
160160

161+
self.recorder_manager.record_pre_step()
162+
161163
# check if we need to do rendering within the physics loop
162164
# note: checked here once to avoid multiple checks within the loop
163165
is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors()
@@ -190,22 +192,37 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
190192
# -- reward computation
191193
self.reward_buf = self.reward_manager.compute(dt=self.step_dt)
192194

195+
# -- update command
196+
self.command_manager.compute(dt=self.step_dt)
197+
# -- step interval events
198+
if "interval" in self.event_manager.available_modes:
199+
self.event_manager.apply(mode="interval", dt=self.step_dt)
200+
201+
self.prev_obs_buf = self.obs_buf
202+
self.obs_buf = self.observation_manager.compute()
203+
self.recorder_manager.record_post_step()
204+
193205
# -- reset envs that terminated/timed-out and log the episode information
194206
reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1)
195207
if len(reset_env_ids) > 0:
208+
# trigger recorder terms for pre-reset calls
209+
self.recorder_manager.record_pre_reset(reset_env_ids)
210+
196211
self._reset_idx(reset_env_ids)
212+
213+
# this is needed to make joint positions set from reset events effective
214+
self.scene.write_data_to_sim()
215+
197216
# if sensors are added to the scene, make sure we render to reflect changes in reset
198217
if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset:
199218
self.sim.render()
200219

201-
# -- update command
202-
self.command_manager.compute(dt=self.step_dt)
203-
# -- step interval events
204-
if "interval" in self.event_manager.available_modes:
205-
self.event_manager.apply(mode="interval", dt=self.step_dt)
206-
# -- compute observations
207-
# note: done after reset to get the correct observations for reset envs
208-
self.obs_buf = self.observation_manager.compute()
220+
# compute observations
221+
# note: done after reset to get the correct observations for reset envs
222+
self.obs_buf = self.observation_manager.compute()
223+
224+
# trigger recorder terms for post-reset calls
225+
self.recorder_manager.record_post_reset(reset_env_ids)
209226

210227
# return observations, rewards, resets and extras
211228
return self.obs_buf, self.reward_buf, self.reset_terminated, self.reset_time_outs, self.extras
@@ -353,6 +370,9 @@ def _reset_idx(self, env_ids: Sequence[int]):
353370
# -- termination manager
354371
info = self.termination_manager.reset(env_ids)
355372
self.extras["log"].update(info)
373+
# -- recroder manager
374+
info = self.recorder_manager.reset(env_ids)
375+
self.extras["log"].update(info)
356376

357377
# reset the episode length buffer
358378
self.episode_length_buf[env_ids] = 0

source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
from .curriculums import * # noqa: F401, F403
2121
from .events import * # noqa: F401, F403
2222
from .observations import * # noqa: F401, F403
23+
from .recorders import * # noqa: F401, F403
2324
from .rewards import * # noqa: F401, F403
2425
from .terminations import * # noqa: F401, F403
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright (c) 2024, The Isaac Lab Project Developers.
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""Various recorder terms that can be used in the environment."""
7+
8+
from .recorders import *
9+
from .recorders_cfg import *
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright (c) 2024, The Isaac Lab Project Developers.
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
from __future__ import annotations
7+
8+
from collections.abc import Sequence
9+
from typing import TYPE_CHECKING
10+
11+
from omni.isaac.lab.managers.manager_term_cfg import RecorderTermCfg
12+
from omni.isaac.lab.managers.recorder_manager import RecorderTerm
13+
14+
if TYPE_CHECKING:
15+
from omni.isaac.lab.envs import ManagerBasedEnv
16+
17+
18+
class InitialStateRecorder(RecorderTerm):
19+
"""Recorder term that records the initial state of the environment after reset."""
20+
21+
def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv) -> None:
22+
super().__init__(cfg, env)
23+
24+
def record_post_reset(self, env_ids: Sequence[int] | None):
25+
return "initial_state", self._env.scene.get_state(is_relative=True)
26+
27+
28+
class PostStepStatesRecorder(RecorderTerm):
29+
"""Recorder term that records the state of the environment at the end of each step."""
30+
31+
def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv) -> None:
32+
super().__init__(cfg, env)
33+
34+
def record_post_step(self):
35+
return "states", self._env.scene.get_state(is_relative=True)
36+
37+
38+
class PreStepActionsRecorder(RecorderTerm):
39+
"""Recorder term that records the actions in the beginning of each step."""
40+
41+
def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv) -> None:
42+
super().__init__(cfg, env)
43+
44+
def record_pre_step(self):
45+
return "actions", self._env.action_manager.action
46+
47+
48+
class PreStepFlatPolicyObservationsRecorder(RecorderTerm):
49+
"""Recorder term that records the policy group observations in each step."""
50+
51+
def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv) -> None:
52+
super().__init__(cfg, env)
53+
54+
def record_pre_step(self):
55+
return "obs", self._env.obs_buf["policy"]
56+
57+
58+
class PreStepSubtaskTermsObservationsRecorder(RecorderTerm):
59+
"""Recorder term that records the subtask completion observations in each step."""
60+
61+
def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv) -> None:
62+
super().__init__(cfg, env)
63+
64+
def record_pre_step(self):
65+
return "obs/subtask_term_signals", self._env.obs_buf["subtask_terms"]

0 commit comments

Comments
 (0)