Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds recorder manager in manager-based environments #1336

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

nvcyc
Copy link

@nvcyc nvcyc commented Oct 29, 2024

Description

This PR adds a recorder manager (RecorderManager) and relevant utility classes for recording data produced in various reset and step stages in manager-based environments.

Wither the built-in recorder manager, users can create custom recorder terms in their environment configurations with callback functions returning tensors to be recorded as environments advance. It is particularly useful for implementing an app that collects human-operated demos and for those who want to record robot actions for post-validation/replay in Isaac Lab environments.

The recorder manager works in both single- and multi-environment use cases. An episode for an environment instance is exported to a dataset file, via a dataset file handler, upon completion (a termination term is signaled a reset to the environment instance is triggered).

By default, the recorder manager is inactive (by assigning no recorder terms in the default configuration), which should have minimal performance impact for existing apps that do not require data recording.

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update -- to be updated in later PRs

Checklist

  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the changelog and the corresponding version in the extension's config/extension.toml file
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

@nvcyc nvcyc force-pushed the cy/recorder_manager branch 2 times, most recently from 37b6d9c to 62087f0 Compare November 2, 2024 19:08
@nvcyc nvcyc force-pushed the cy/recorder_manager branch 6 times, most recently from 0e16d99 to 18f8f71 Compare November 7, 2024 23:58
@@ -303,8 +377,13 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
if "interval" in self.event_manager.available_modes:
self.event_manager.apply(mode="interval", dt=self.step_dt)

# -- compute observations
self.prev_obs_buf = self.obs_buf
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would obs_buf need to be cloned?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does prev_obs_buf get used? I only see it set.

# -- update command
self.command_manager.compute(dt=self.step_dt)
# -- step interval events
if "interval" in self.event_manager.available_modes:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not 100% sure if it's ok to do randomization before reset. could there be cases where the events could trigger resets?

if "interval" in self.event_manager.available_modes:
self.event_manager.apply(mode="interval", dt=self.step_dt)

self.prev_obs_buf = self.obs_buf
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could introduce additional observation computation if recording is not needed

* Pre-reset recording: This callback is invoked at the beginning of `env.reset()` before the reset is effective.
* Post-reset recording: This callback is invoked at the end of `env.reset()`.
* Pre-step recording: This callback is invoked at the beginning of `env.step()`, after the step action is processed
and before the action is applied the action manager.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied "to" the action manager?

for env_id in range(env.num_envs):
self._episodes[env_id] = EpisodeData()

if hasattr(env.cfg, "env_name"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use getattr with default None

env_ids = slice(None)
# -- assets
for asset_name, articulation in self._articulations.items():
joint_position = torch.Tensor(state["articulation"][asset_name])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this need to be wrapped in torch.Tensor? and if so, we would also need to specify the device?


# set up the environment
self.num_envs = 20
self.device = "cpu"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can device only be cpu? should we test for GPU case as well?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, ideally with subTest, but this will need to be set in each test_ method

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second this, ideally use subtest within each test_ method below.

episode = EpisodeData()
self.assertTrue(episode.is_empty())

episode.add("key", torch.Tensor([1, 2, 3], device="cpu"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar comment about device


# set the state
self.scene.reset_to(state, env_ids, is_relative=is_relative)
self.scene.write_data_to_sim()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reset_to seems to already call write_data_to_sim, is it necessary to call this again?

return torch.concat([delta_pose, gripper_vel], dim=1)


def main():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be similar to the collect_demonstrations.py script, are there key differences for us to keep both scripts?

@kellyguo11
Copy link
Contributor

test_episode_data.py test seems to be failing

@@ -303,8 +377,13 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
if "interval" in self.event_manager.available_modes:
self.event_manager.apply(mode="interval", dt=self.step_dt)

# -- compute observations
self.prev_obs_buf = self.obs_buf
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does prev_obs_buf get used? I only see it set.

state = dict()
# -- assets
state["articulation"] = dict()
for asset_name, articulation in self._articulations.items():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also velocity, and applied torque


# set up the environment
self.num_envs = 20
self.device = "cpu"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, ideally with subTest, but this will need to be set in each test_ method

Comment on lines +21 to +22
def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv) -> None:
super().__init__(cfg, env)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: This is unnecessary if you are not adding to the init of the parent class. Can be removed.

Comment on lines 78 to 121
def record_pre_reset(self, env_ids: Sequence[int] | None) -> tuple[str | None, torch.Tensor | None]:
"""Record data at the beginning of env.reset() before reset is effective."""
return None, None

def record_post_reset(self, env_ids: Sequence[int] | None) -> tuple[str | None, torch.Tensor | None]:
"""Record data at the end of env.reset()."""
return None, None

def record_pre_step(self) -> tuple[str | None, torch.Tensor | None]:
"""Record data in the beginning of env.step() after action is cached/processed in the ActionManager."""
return None, None

def record_post_step(self) -> tuple[str | None, torch.Tensor | None]:
"""Record data at the end of env.step() when all the managers are processed."""
return None, None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a description of the returns in the docstrings of these functions.

Comment on lines +345 to +358
def get_state(self, is_relative: bool = False) -> dict[str, dict[str, torch.Tensor]]:
"""Returns the state of the scene entities.
Returns:
A dictionary of the state of the scene entities.
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you describe the is_relative in the docstring? It could be interpreted as relative joint position and it looks like its just relative to environment origins.

Comment on lines +406 to +418
articulation.write_joint_state_to_sim(joint_position, torch.zeros_like(joint_position), env_ids=env_ids)
articulation.set_joint_position_target(joint_position, env_ids=env_ids)
articulation.set_joint_velocity_target(torch.zeros_like(joint_position), env_ids=env_ids)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as with the comments in scene.get_state we will need to set velocity and effort here too.,

state = dict()
# -- assets
state["articulation"] = dict()
for asset_name, articulation in self._articulations.items():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be good to get velocity and applied_torque too for more general use cases.


# set up the environment
self.num_envs = 20
self.device = "cpu"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second this, ideally use subtest within each test_ method below.

self.export_episodes(env_ids)

def record_post_reset(self, env_ids: Sequence[int] | None) -> None:
"""Trigger recorder terms for post-reset functions."""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add env_ids to docstring

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants