Commit 464631fa authored by CY Chen's avatar CY Chen Committed by Kelly Guo

Updates mimic to support multi-eef (DexMimicGen) data generation (#287)

This PR updates mimic to support multi-eef (DexMimicgen) data
generation.
It consists of the following major changes:
- Updated mimic code to support environments with multiple end effectors
- Added support for setting subtask constraints based on DexMimicGen
- Updated annotate_demos.py to support annotating subtask term signals
for multiple end effectors
- Updated mimic API target_eef_pose_to_action() to take noise as
dictionary of eef noise values instead of a single value

- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- This change requires a documentation update

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) 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
- [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 78a70bb5
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
Main data generation script. Main data generation script.
""" """
# Launching Isaac Sim Simulator first. """Launch Isaac Sim Simulator first."""
import argparse import argparse
...@@ -45,10 +45,15 @@ simulation_app = app_launcher.app ...@@ -45,10 +45,15 @@ simulation_app = app_launcher.app
import asyncio import asyncio
import gymnasium as gym import gymnasium as gym
import inspect
import numpy as np import numpy as np
import random import random
import torch import torch
import omni
from isaaclab.envs import ManagerBasedRLMimicEnv
import isaaclab_mimic.envs # noqa: F401 import isaaclab_mimic.envs # noqa: F401
from isaaclab_mimic.datagen.generation import env_loop, setup_async_generation, setup_env_config from isaaclab_mimic.datagen.generation import env_loop, setup_async_generation, setup_env_config
from isaaclab_mimic.datagen.utils import get_env_name_from_dataset, setup_output_paths from isaaclab_mimic.datagen.utils import get_env_name_from_dataset, setup_output_paths
...@@ -74,12 +79,22 @@ def main(): ...@@ -74,12 +79,22 @@ def main():
) )
# create environment # create environment
env = gym.make(env_name, cfg=env_cfg) env = gym.make(env_name, cfg=env_cfg).unwrapped
if not isinstance(env, ManagerBasedRLMimicEnv):
raise ValueError("The environment should be derived from ManagerBasedRLMimicEnv")
# check if the mimic API from this environment contains decprecated signatures
if "action_noise_dict" not in inspect.signature(env.target_eef_pose_to_action).parameters:
omni.log.warn(
f'The "noise" parameter in the "{env_name}" environment\'s mimic API "target_eef_pose_to_action", '
"is deprecated. Please update the API to take action_noise_dict instead."
)
# set seed for generation # set seed for generation
random.seed(env.unwrapped.cfg.datagen_config.seed) random.seed(env.cfg.datagen_config.seed)
np.random.seed(env.unwrapped.cfg.datagen_config.seed) np.random.seed(env.cfg.datagen_config.seed)
torch.manual_seed(env.unwrapped.cfg.datagen_config.seed) torch.manual_seed(env.cfg.datagen_config.seed)
# reset before starting # reset before starting
env.reset() env.reset()
...@@ -95,7 +110,13 @@ def main(): ...@@ -95,7 +110,13 @@ def main():
try: try:
asyncio.ensure_future(asyncio.gather(*async_components["tasks"])) asyncio.ensure_future(asyncio.gather(*async_components["tasks"]))
env_loop(env, async_components["action_queue"], async_components["info_pool"], async_components["event_loop"]) env_loop(
env,
async_components["reset_queue"],
async_components["action_queue"],
async_components["info_pool"],
async_components["event_loop"],
)
except asyncio.CancelledError: except asyncio.CancelledError:
print("Tasks were cancelled.") print("Tasks were cancelled.")
......
...@@ -118,6 +118,18 @@ Changed ...@@ -118,6 +118,18 @@ Changed
* ``set_fixed_tendon_limit`` → ``set_fixed_tendon_position_limit`` * ``set_fixed_tendon_limit`` → ``set_fixed_tendon_position_limit``
0.34.12 (2025-03-06)
~~~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Updated the mimic API :meth:`target_eef_pose_to_action` in :class:`isaaclab.envs.ManagerBasedRLMimicEnv` to take a dictionary of
eef noise values instead of a single noise value.
* Added support for optional subtask constraints based on DexMimicGen to the mimic configuration class :class:`isaaclab.envs.MimicEnvCfg`.
* Enabled data compression in HDF5 dataset file handler :class:`isaaclab.utils.datasets.hdf5_dataset_file_handler.HDF5DatasetFileHandler`.
0.34.11 (2025-03-04) 0.34.11 (2025-03-04)
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
......
...@@ -47,7 +47,11 @@ class ManagerBasedRLMimicEnv(ManagerBasedRLEnv): ...@@ -47,7 +47,11 @@ class ManagerBasedRLMimicEnv(ManagerBasedRLEnv):
raise NotImplementedError raise NotImplementedError
def target_eef_pose_to_action( def target_eef_pose_to_action(
self, target_eef_pose_dict: dict, gripper_action_dict: dict, noise: float | None = None, env_id: int = 0 self,
target_eef_pose_dict: dict,
gripper_action_dict: dict,
action_noise_dict: dict | None = None,
env_id: int = 0,
) -> torch.Tensor: ) -> torch.Tensor:
""" """
Takes a target pose and gripper action for the end effector controller and returns an action Takes a target pose and gripper action for the end effector controller and returns an action
...@@ -57,7 +61,7 @@ class ManagerBasedRLMimicEnv(ManagerBasedRLEnv): ...@@ -57,7 +61,7 @@ class ManagerBasedRLMimicEnv(ManagerBasedRLEnv):
Args: Args:
target_eef_pose_dict: Dictionary of 4x4 target eef pose for each end-effector. target_eef_pose_dict: Dictionary of 4x4 target eef pose for each end-effector.
gripper_action_dict: Dictionary of gripper actions for each end-effector. gripper_action_dict: Dictionary of gripper actions for each end-effector.
noise: Noise to add to the action. If None, no noise is added. action_noise_dict: Noise to add to the action. If None, no noise is added.
env_id: Environment index to compute the action for. env_id: Environment index to compute the action for.
Returns: Returns:
......
...@@ -163,7 +163,7 @@ class HDF5DatasetFileHandler(DatasetFileHandlerBase): ...@@ -163,7 +163,7 @@ class HDF5DatasetFileHandler(DatasetFileHandlerBase):
for sub_key, sub_value in value.items(): for sub_key, sub_value in value.items():
create_dataset_helper(key_group, sub_key, sub_value) create_dataset_helper(key_group, sub_key, sub_value)
else: else:
group.create_dataset(key, data=value.cpu().numpy()) group.create_dataset(key, data=value.cpu().numpy(), compression="gzip")
for key, value in episode.data.items(): for key, value in episode.data.items():
create_dataset_helper(h5_episode_group, key, value) create_dataset_helper(h5_episode_group, key, value)
......
[package] [package]
# Semantic Versioning is used: https://semver.org/ # Semantic Versioning is used: https://semver.org/
version = "1.0.4" version = "1.0.5"
# Description # Description
category = "isaaclab" category = "isaaclab"
......
Changelog Changelog
--------- ---------
1.0.4 (2025-03-10) 1.0.5 (2025-03-10)
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
Changed Changed
...@@ -15,6 +15,16 @@ Added ...@@ -15,6 +15,16 @@ Added
* Added ``Isaac-Stack-Cube-Franka-IK-Rel-Blueprint-Mimic-v0`` environment for blueprint vision stacking. * Added ``Isaac-Stack-Cube-Franka-IK-Rel-Blueprint-Mimic-v0`` environment for blueprint vision stacking.
1.0.4 (2025-03-07)
~~~~~~~~~~~~~~~~~~
Changed
^^^^^^^
* Updated data generator to support environments with multiple end effectors.
* Updated data generator to support subtask constraints based on DexMimicGen.
1.0.3 (2025-03-06) 1.0.3 (2025-03-06)
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
""" """
Defines structure of information that is needed from an environment for data generation. Defines structure of information that is needed from an environment for data generation.
""" """
import torch
from copy import deepcopy from copy import deepcopy
...@@ -46,40 +45,6 @@ class DatagenInfo: ...@@ -46,40 +45,6 @@ class DatagenInfo:
gripper_action (torch.Tensor or None): gripper actions of shape [..., D] where D gripper_action (torch.Tensor or None): gripper actions of shape [..., D] where D
is the dimension of the gripper actuation action for the robot arm is the dimension of the gripper actuation action for the robot arm
""" """
# Type checks using assert
if eef_pose is not None:
assert isinstance(
eef_pose, torch.Tensor
), f"Expected 'eef_pose' to be of type torch.Tensor, but got {type(eef_pose)}"
if object_poses is not None:
assert isinstance(
object_poses, dict
), f"Expected 'object_poses' to be a dictionary, but got {type(object_poses)}"
for k, v in object_poses.items():
assert isinstance(
v, torch.Tensor
), f"Expected 'object_poses[{k}]' to be of type torch.Tensor, but got {type(v)}"
if subtask_term_signals is not None:
assert isinstance(
subtask_term_signals, dict
), f"Expected 'subtask_term_signals' to be a dictionary, but got {type(subtask_term_signals)}"
for k, v in subtask_term_signals.items():
assert isinstance(
v, (torch.Tensor, int, float)
), f"Expected 'subtask_term_signals[{k}]' to be of type torch.Tensor, int, or float, but got {type(v)}"
if target_eef_pose is not None:
assert isinstance(
target_eef_pose, torch.Tensor
), f"Expected 'target_eef_pose' to be of type torch.Tensor, but got {type(target_eef_pose)}"
if gripper_action is not None:
assert isinstance(
gripper_action, torch.Tensor
), f"Expected 'gripper_action' to be of type torch.Tensor, but got {type(gripper_action)}"
self.eef_pose = None self.eef_pose = None
if eef_pose is not None: if eef_pose is not None:
self.eef_pose = eef_pose self.eef_pose = eef_pose
......
...@@ -8,9 +8,9 @@ import contextlib ...@@ -8,9 +8,9 @@ import contextlib
import torch import torch
from typing import Any from typing import Any
from isaaclab.envs import ManagerBasedEnv from isaaclab.envs import ManagerBasedRLMimicEnv
from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg
from isaaclab.managers import DatasetExportMode from isaaclab.managers import DatasetExportMode, TerminationTermCfg
from isaaclab_mimic.datagen.data_generator import DataGenerator from isaaclab_mimic.datagen.data_generator import DataGenerator
from isaaclab_mimic.datagen.datagen_info_pool import DataGenInfoPool from isaaclab_mimic.datagen.datagen_info_pool import DataGenInfoPool
...@@ -24,23 +24,32 @@ num_attempts = 0 ...@@ -24,23 +24,32 @@ num_attempts = 0
async def run_data_generator( async def run_data_generator(
env: ManagerBasedEnv, env: ManagerBasedRLMimicEnv,
env_id: int, env_id: int,
env_reset_queue: asyncio.Queue,
env_action_queue: asyncio.Queue, env_action_queue: asyncio.Queue,
data_generator: DataGenerator, data_generator: DataGenerator,
success_term: Any, success_term: TerminationTermCfg,
pause_subtask: bool = False, pause_subtask: bool = False,
): ):
"""Run data generator.""" """Run mimic data generation from the given data generator in the specified environment index.
Args:
env: The environment to run the data generator on.
env_id: The environment index to run the data generation on.
env_reset_queue: The asyncio queue to send environment (for this particular env_id) reset requests to.
env_action_queue: The asyncio queue to send actions to for executing actions.
data_generator: The data generator instance to use.
success_term: The success termination term to use.
pause_subtask: Whether to pause the subtask during generation.
"""
global num_success, num_failures, num_attempts global num_success, num_failures, num_attempts
while True: while True:
results = await data_generator.generate( results = await data_generator.generate(
env_id=env_id, env_id=env_id,
success_term=success_term, success_term=success_term,
env_reset_queue=env_reset_queue,
env_action_queue=env_action_queue, env_action_queue=env_action_queue,
select_src_per_subtask=env.unwrapped.cfg.datagen_config.generation_select_src_per_subtask,
transform_first_robot_pose=env.unwrapped.cfg.datagen_config.generation_transform_first_robot_pose,
interpolate_from_last_target_pose=env.unwrapped.cfg.datagen_config.generation_interpolate_from_last_target_pose,
pause_subtask=pause_subtask, pause_subtask=pause_subtask,
) )
if bool(results["success"]): if bool(results["success"]):
...@@ -51,22 +60,40 @@ async def run_data_generator( ...@@ -51,22 +60,40 @@ async def run_data_generator(
def env_loop( def env_loop(
env: ManagerBasedEnv, env: ManagerBasedRLMimicEnv,
env_reset_queue: asyncio.Queue,
env_action_queue: asyncio.Queue, env_action_queue: asyncio.Queue,
shared_datagen_info_pool: DataGenInfoPool, shared_datagen_info_pool: DataGenInfoPool,
asyncio_event_loop: asyncio.AbstractEventLoop, asyncio_event_loop: asyncio.AbstractEventLoop,
) -> None: ):
"""Main loop for the environment.""" """Main asyncio loop for the environment.
Args:
env: The environment to run the main step loop on.
env_reset_queue: The asyncio queue to handle reset request the environment.
env_action_queue: The asyncio queue to handle actions to for executing actions.
shared_datagen_info_pool: The shared datagen info pool that stores source demo info.
asyncio_event_loop: The main asyncio event loop.
"""
global num_success, num_failures, num_attempts global num_success, num_failures, num_attempts
env_id_tensor = torch.tensor([0], dtype=torch.int64, device=env.device)
prev_num_attempts = 0 prev_num_attempts = 0
# simulate environment -- run everything in inference mode # simulate environment -- run everything in inference mode
with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode():
while True: while True:
actions = torch.zeros(env.unwrapped.action_space.shape) # check if any environment needs to be reset while waiting for actions
while env_action_queue.qsize() != env.num_envs:
asyncio_event_loop.run_until_complete(asyncio.sleep(0))
while not env_reset_queue.empty():
env_id_tensor[0] = env_reset_queue.get_nowait()
env.reset(env_ids=env_id_tensor)
env_reset_queue.task_done()
actions = torch.zeros(env.action_space.shape)
# get actions from all the data generators # get actions from all the data generators
for i in range(env.unwrapped.num_envs): for i in range(env.num_envs):
# an async-blocking call to get an action from a data generator # an async-blocking call to get an action from a data generator
env_id, action = asyncio_event_loop.run_until_complete(env_action_queue.get()) env_id, action = asyncio_event_loop.run_until_complete(env_action_queue.get())
actions[env_id] = action actions[env_id] = action
...@@ -75,27 +102,30 @@ def env_loop( ...@@ -75,27 +102,30 @@ def env_loop(
env.step(actions) env.step(actions)
# mark done so the data generators can continue with the step results # mark done so the data generators can continue with the step results
for i in range(env.unwrapped.num_envs): for i in range(env.num_envs):
env_action_queue.task_done() env_action_queue.task_done()
if prev_num_attempts != num_attempts: if prev_num_attempts != num_attempts:
prev_num_attempts = num_attempts prev_num_attempts = num_attempts
generated_sucess_rate = 100 * num_success / num_attempts if num_attempts > 0 else 0.0
print("") print("")
print("*" * 50) print("*" * 50, "\033[K")
print(f"have {num_success} successes out of {num_attempts} trials so far") print(
print(f"have {num_failures} failures out of {num_attempts} trials so far") f"{num_success}/{num_attempts} ({generated_sucess_rate:.1f}%) successful demos generated by"
print("*" * 50) " mimic\033[K"
)
print("*" * 50, "\033[K")
# termination condition is on enough successes if @guarantee_success or enough attempts otherwise # termination condition is on enough successes if @guarantee_success or enough attempts otherwise
generation_guarantee = env.unwrapped.cfg.datagen_config.generation_guarantee generation_guarantee = env.cfg.datagen_config.generation_guarantee
generation_num_trials = env.unwrapped.cfg.datagen_config.generation_num_trials generation_num_trials = env.cfg.datagen_config.generation_num_trials
check_val = num_success if generation_guarantee else num_attempts check_val = num_success if generation_guarantee else num_attempts
if check_val >= generation_num_trials: if check_val >= generation_num_trials:
print(f"Reached {generation_num_trials} successes/attempts. Exiting.") print(f"Reached {generation_num_trials} successes/attempts. Exiting.")
break break
# check that simulation is stopped or not # check that simulation is stopped or not
if env.unwrapped.sim.is_stopped(): if env.sim.is_stopped():
break break
env.close() env.close()
...@@ -175,26 +205,28 @@ def setup_async_generation( ...@@ -175,26 +205,28 @@ def setup_async_generation(
List of asyncio tasks for data generation List of asyncio tasks for data generation
""" """
asyncio_event_loop = asyncio.get_event_loop() asyncio_event_loop = asyncio.get_event_loop()
env_reset_queue = asyncio.Queue()
env_action_queue = asyncio.Queue() env_action_queue = asyncio.Queue()
shared_datagen_info_pool_lock = asyncio.Lock() shared_datagen_info_pool_lock = asyncio.Lock()
shared_datagen_info_pool = DataGenInfoPool( shared_datagen_info_pool = DataGenInfoPool(env, env.cfg, env.device, asyncio_lock=shared_datagen_info_pool_lock)
env.unwrapped, env.unwrapped.cfg, env.unwrapped.device, asyncio_lock=shared_datagen_info_pool_lock
)
shared_datagen_info_pool.load_from_dataset_file(input_file) shared_datagen_info_pool.load_from_dataset_file(input_file)
print(f"Loaded {shared_datagen_info_pool.num_datagen_infos} to datagen info pool") print(f"Loaded {shared_datagen_info_pool.num_datagen_infos} to datagen info pool")
# Create and schedule data generator tasks # Create and schedule data generator tasks
data_generator = DataGenerator(env=env.unwrapped, src_demo_datagen_info_pool=shared_datagen_info_pool) data_generator = DataGenerator(env=env, src_demo_datagen_info_pool=shared_datagen_info_pool)
data_generator_asyncio_tasks = [] data_generator_asyncio_tasks = []
for i in range(num_envs): for i in range(num_envs):
task = asyncio_event_loop.create_task( task = asyncio_event_loop.create_task(
run_data_generator(env, i, env_action_queue, data_generator, success_term, pause_subtask=pause_subtask) run_data_generator(
env, i, env_reset_queue, env_action_queue, data_generator, success_term, pause_subtask=pause_subtask
)
) )
data_generator_asyncio_tasks.append(task) data_generator_asyncio_tasks.append(task)
return { return {
"tasks": data_generator_asyncio_tasks, "tasks": data_generator_asyncio_tasks,
"event_loop": asyncio_event_loop, "event_loop": asyncio_event_loop,
"reset_queue": env_reset_queue,
"action_queue": env_action_queue, "action_queue": env_action_queue,
"info_pool": shared_datagen_info_pool, "info_pool": shared_datagen_info_pool,
} }
...@@ -59,10 +59,6 @@ class TestGenerateDataset(unittest.TestCase): ...@@ -59,10 +59,6 @@ class TestGenerateDataset(unittest.TestCase):
DATASETS_DOWNLOAD_DIR + "/dataset.hdf5", DATASETS_DOWNLOAD_DIR + "/dataset.hdf5",
"--output_file", "--output_file",
DATASETS_DOWNLOAD_DIR + "/annotated_dataset.hdf5", DATASETS_DOWNLOAD_DIR + "/annotated_dataset.hdf5",
"--signals",
"grasp_1",
"stack_1",
"grasp_2",
"--auto", "--auto",
"--headless", "--headless",
] ]
......
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