From ec40ccde0d892d6ada167f371e93eef997b2e669 Mon Sep 17 00:00:00 2001 From: Michel Aractingi Date: Fri, 26 Sep 2025 14:28:58 +0200 Subject: [PATCH 1/9] Bug in conversion from v2.1 script (#2057) * False logic in setting the dataset to index in the meta data when converting from v2.1' * Improved logging --- .../datasets/v30/convert_dataset_v21_to_v30.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/lerobot/datasets/v30/convert_dataset_v21_to_v30.py b/src/lerobot/datasets/v30/convert_dataset_v21_to_v30.py index e5a6e3c9a..ac9d41cf7 100644 --- a/src/lerobot/datasets/v30/convert_dataset_v21_to_v30.py +++ b/src/lerobot/datasets/v30/convert_dataset_v21_to_v30.py @@ -34,6 +34,7 @@ python src/lerobot/datasets/v30/convert_dataset_v21_to_v30.py \ """ import argparse +import logging import shutil from pathlib import Path from typing import Any @@ -71,6 +72,7 @@ from lerobot.datasets.utils import ( ) from lerobot.datasets.video_utils import concatenate_video_files, get_video_duration_in_s from lerobot.utils.constants import HF_LEROBOT_HOME +from lerobot.utils.utils import init_logging V21 = "v2.1" @@ -144,6 +146,7 @@ def legacy_load_tasks(local_dir: Path) -> tuple[dict, dict]: def convert_tasks(root, new_root): + logging.info(f"Converting tasks from {root} to {new_root}") tasks, _ = legacy_load_tasks(root) task_indices = tasks.keys() task_strings = tasks.values() @@ -185,7 +188,10 @@ def convert_data(root: Path, new_root: Path, data_file_size_in_mb: int): num_frames = 0 paths_to_cat = [] episodes_metadata = [] - for ep_path in ep_paths: + + logging.info(f"Converting data files from {len(ep_paths)} episodes") + + for ep_path in tqdm.tqdm(ep_paths, desc="convert data files"): ep_size_in_mb = get_parquet_file_size_in_mb(ep_path) ep_num_frames = get_parquet_num_frames(ep_path) ep_metadata = { @@ -209,7 +215,6 @@ def convert_data(root: Path, new_root: Path, data_file_size_in_mb: int): # Reset for the next file size_in_mb = ep_size_in_mb - num_frames = ep_num_frames paths_to_cat = [ep_path] chunk_idx, file_idx = update_chunk_file_indices(chunk_idx, file_idx, DEFAULT_CHUNK_SIZE) @@ -236,6 +241,8 @@ def get_image_keys(root): def convert_videos(root: Path, new_root: Path, video_file_size_in_mb: int): + logging.info(f"Converting videos from {root} to {new_root}") + video_keys = get_video_keys(root) if len(video_keys) == 0: return None @@ -254,7 +261,7 @@ def convert_videos(root: Path, new_root: Path, video_file_size_in_mb: int): episods_metadata = [] num_cameras = len(video_keys) num_episodes = num_eps_per_cam[0] - for ep_idx in range(num_episodes): + for ep_idx in tqdm.tqdm(range(num_episodes), desc="convert videos"): # Sanity check ep_ids = [eps_metadata_per_cam[cam_idx][ep_idx]["episode_index"] for cam_idx in range(num_cameras)] ep_ids += [ep_idx] @@ -281,6 +288,7 @@ def convert_videos_of_camera(root: Path, new_root: Path, video_key: str, video_f duration_in_s = 0.0 paths_to_cat = [] episodes_metadata = [] + for ep_path in tqdm.tqdm(ep_paths, desc=f"convert videos of {video_key}"): ep_size_in_mb = get_video_size_in_mb(ep_path) ep_duration_in_s = get_video_duration_in_s(ep_path) @@ -374,6 +382,8 @@ def generate_episode_metadata_dict( def convert_episodes_metadata(root, new_root, episodes_metadata, episodes_video_metadata=None): + logging.info(f"Converting episodes metadata from {root} to {new_root}") + episodes_legacy_metadata = legacy_load_episodes(root) episodes_stats = legacy_load_episodes_stats(root) @@ -405,6 +415,7 @@ def convert_info(root, new_root, data_file_size_in_mb, video_file_size_in_mb): info["data_path"] = DEFAULT_DATA_PATH info["video_path"] = DEFAULT_VIDEO_PATH info["fps"] = int(info["fps"]) + logging.info(f"Converting info from {root} to {new_root}") for key in info["features"]: if info["features"][key]["dtype"] == "video": # already has fps in video_info @@ -469,6 +480,7 @@ def convert_dataset( if __name__ == "__main__": + init_logging() parser = argparse.ArgumentParser() parser.add_argument( "--repo-id", From c5b5955c5acf15e1b3f0ace6ae72612f98c1fe06 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Fri, 26 Sep 2025 14:30:07 +0200 Subject: [PATCH 2/9] chore: replace hard-coded next values with constants throughout all the source code (#2056) --- src/lerobot/datasets/factory.py | 4 +- src/lerobot/processor/converters.py | 14 ++-- src/lerobot/rl/buffer.py | 16 ++--- src/lerobot/rl/crop_dataset_roi.py | 3 +- src/lerobot/rl/gym_manipulator.py | 10 +-- src/lerobot/scripts/lerobot_dataset_viz.py | 10 +-- src/lerobot/scripts/lerobot_eval.py | 6 +- tests/datasets/test_datasets.py | 6 +- .../hilserl/test_modeling_classifier.py | 10 +-- tests/processor/test_batch_conversion.py | 68 +++++++++---------- tests/processor/test_converters.py | 6 +- tests/processor/test_pipeline.py | 14 ++-- tests/utils/test_replay_buffer.py | 6 +- 13 files changed, 87 insertions(+), 86 deletions(-) diff --git a/src/lerobot/datasets/factory.py b/src/lerobot/datasets/factory.py index f74b6ac4f..f3ceb2b0c 100644 --- a/src/lerobot/datasets/factory.py +++ b/src/lerobot/datasets/factory.py @@ -27,7 +27,7 @@ from lerobot.datasets.lerobot_dataset import ( ) from lerobot.datasets.streaming_dataset import StreamingLeRobotDataset from lerobot.datasets.transforms import ImageTransforms -from lerobot.utils.constants import ACTION, OBS_PREFIX +from lerobot.utils.constants import ACTION, OBS_PREFIX, REWARD IMAGENET_STATS = { "mean": [[[0.485]], [[0.456]], [[0.406]]], # (c,1,1) @@ -55,7 +55,7 @@ def resolve_delta_timestamps( """ delta_timestamps = {} for key in ds_meta.features: - if key == "next.reward" and cfg.reward_delta_indices is not None: + if key == REWARD and cfg.reward_delta_indices is not None: delta_timestamps[key] = [i / ds_meta.fps for i in cfg.reward_delta_indices] if key == ACTION and cfg.action_delta_indices is not None: delta_timestamps[key] = [i / ds_meta.fps for i in cfg.action_delta_indices] diff --git a/src/lerobot/processor/converters.py b/src/lerobot/processor/converters.py index 68f9dd6fa..6b0b67598 100644 --- a/src/lerobot/processor/converters.py +++ b/src/lerobot/processor/converters.py @@ -23,7 +23,7 @@ from typing import Any import numpy as np import torch -from lerobot.utils.constants import ACTION, OBS_PREFIX +from lerobot.utils.constants import ACTION, DONE, OBS_PREFIX, REWARD, TRUNCATED from .core import EnvTransition, PolicyAction, RobotAction, RobotObservation, TransitionKey @@ -355,9 +355,9 @@ def batch_to_transition(batch: dict[str, Any]) -> EnvTransition: return create_transition( observation=observation_keys if observation_keys else None, action=batch.get(ACTION), - reward=batch.get("next.reward", 0.0), - done=batch.get("next.done", False), - truncated=batch.get("next.truncated", False), + reward=batch.get(REWARD, 0.0), + done=batch.get(DONE, False), + truncated=batch.get(TRUNCATED, False), info=batch.get("info", {}), complementary_data=complementary_data if complementary_data else None, ) @@ -380,9 +380,9 @@ def transition_to_batch(transition: EnvTransition) -> dict[str, Any]: batch = { ACTION: transition.get(TransitionKey.ACTION), - "next.reward": transition.get(TransitionKey.REWARD, 0.0), - "next.done": transition.get(TransitionKey.DONE, False), - "next.truncated": transition.get(TransitionKey.TRUNCATED, False), + REWARD: transition.get(TransitionKey.REWARD, 0.0), + DONE: transition.get(TransitionKey.DONE, False), + TRUNCATED: transition.get(TransitionKey.TRUNCATED, False), "info": transition.get(TransitionKey.INFO, {}), } diff --git a/src/lerobot/rl/buffer.py b/src/lerobot/rl/buffer.py index b572bbce5..d30b65082 100644 --- a/src/lerobot/rl/buffer.py +++ b/src/lerobot/rl/buffer.py @@ -24,7 +24,7 @@ import torch.nn.functional as F # noqa: N812 from tqdm import tqdm from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.utils.constants import ACTION, OBS_IMAGE +from lerobot.utils.constants import ACTION, DONE, OBS_IMAGE, REWARD from lerobot.utils.transition import Transition @@ -534,8 +534,8 @@ class ReplayBuffer: features[ACTION] = act_info # Add "reward" and "done" - features["next.reward"] = {"dtype": "float32", "shape": (1,)} - features["next.done"] = {"dtype": "bool", "shape": (1,)} + features[REWARD] = {"dtype": "float32", "shape": (1,)} + features[DONE] = {"dtype": "bool", "shape": (1,)} # Add state keys for key in self.states: @@ -578,8 +578,8 @@ class ReplayBuffer: # Fill action, reward, done frame_dict[ACTION] = self.actions[actual_idx].cpu() - frame_dict["next.reward"] = torch.tensor([self.rewards[actual_idx]], dtype=torch.float32).cpu() - frame_dict["next.done"] = torch.tensor([self.dones[actual_idx]], dtype=torch.bool).cpu() + frame_dict[REWARD] = torch.tensor([self.rewards[actual_idx]], dtype=torch.float32).cpu() + frame_dict[DONE] = torch.tensor([self.dones[actual_idx]], dtype=torch.bool).cpu() frame_dict["task"] = task_name # Add complementary_info if available @@ -648,7 +648,7 @@ class ReplayBuffer: # Check if the dataset has "next.done" key sample = dataset[0] - has_done_key = "next.done" in sample + has_done_key = DONE in sample # Check for complementary_info keys complementary_info_keys = [key for key in sample if key.startswith("complementary_info.")] @@ -671,11 +671,11 @@ class ReplayBuffer: action = current_sample[ACTION].unsqueeze(0) # Add batch dimension # ----- 3) Reward and done ----- - reward = float(current_sample["next.reward"].item()) # ensure float + reward = float(current_sample[REWARD].item()) # ensure float # Determine done flag - use next.done if available, otherwise infer from episode boundaries if has_done_key: - done = bool(current_sample["next.done"].item()) # ensure bool + done = bool(current_sample[DONE].item()) # ensure bool else: # If this is the last frame or if next frame is in a different episode, mark as done done = False diff --git a/src/lerobot/rl/crop_dataset_roi.py b/src/lerobot/rl/crop_dataset_roi.py index c4318c415..281069e14 100644 --- a/src/lerobot/rl/crop_dataset_roi.py +++ b/src/lerobot/rl/crop_dataset_roi.py @@ -25,6 +25,7 @@ import torchvision.transforms.functional as F # type: ignore # noqa: N812 from tqdm import tqdm # type: ignore from lerobot.datasets.lerobot_dataset import LeRobotDataset +from lerobot.utils.constants import DONE, REWARD def select_rect_roi(img): @@ -212,7 +213,7 @@ def convert_lerobot_dataset_to_cropper_lerobot_dataset( for key, value in frame.items(): if key in ("task_index", "timestamp", "episode_index", "frame_index", "index", "task"): continue - if key in ("next.done", "next.reward"): + if key in (DONE, REWARD): # if not isinstance(value, str) and len(value.shape) == 0: value = value.unsqueeze(0) diff --git a/src/lerobot/rl/gym_manipulator.py b/src/lerobot/rl/gym_manipulator.py index fa9f4e3e1..ad36f1b36 100644 --- a/src/lerobot/rl/gym_manipulator.py +++ b/src/lerobot/rl/gym_manipulator.py @@ -73,7 +73,7 @@ from lerobot.teleoperators import ( ) from lerobot.teleoperators.teleoperator import Teleoperator from lerobot.teleoperators.utils import TeleopEvents -from lerobot.utils.constants import ACTION, OBS_IMAGES, OBS_STATE +from lerobot.utils.constants import ACTION, DONE, OBS_IMAGES, OBS_STATE, REWARD from lerobot.utils.robot_utils import busy_wait from lerobot.utils.utils import log_say @@ -602,8 +602,8 @@ def control_loop( action_features = teleop_device.action_features features = { ACTION: action_features, - "next.reward": {"dtype": "float32", "shape": (1,), "names": None}, - "next.done": {"dtype": "bool", "shape": (1,), "names": None}, + REWARD: {"dtype": "float32", "shape": (1,), "names": None}, + DONE: {"dtype": "bool", "shape": (1,), "names": None}, } if use_gripper: features["complementary_info.discrete_penalty"] = { @@ -673,8 +673,8 @@ def control_loop( frame = { **observations, ACTION: action_to_record.cpu(), - "next.reward": np.array([transition[TransitionKey.REWARD]], dtype=np.float32), - "next.done": np.array([terminated or truncated], dtype=bool), + REWARD: np.array([transition[TransitionKey.REWARD]], dtype=np.float32), + DONE: np.array([terminated or truncated], dtype=bool), } if use_gripper: discrete_penalty = transition[TransitionKey.COMPLEMENTARY_DATA].get("discrete_penalty", 0.0) diff --git a/src/lerobot/scripts/lerobot_dataset_viz.py b/src/lerobot/scripts/lerobot_dataset_viz.py index adff5c085..55708d9a9 100644 --- a/src/lerobot/scripts/lerobot_dataset_viz.py +++ b/src/lerobot/scripts/lerobot_dataset_viz.py @@ -75,7 +75,7 @@ import torch.utils.data import tqdm from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.utils.constants import ACTION, OBS_STATE +from lerobot.utils.constants import ACTION, DONE, OBS_STATE, REWARD class EpisodeSampler(torch.utils.data.Sampler): @@ -166,11 +166,11 @@ def visualize_dataset( for dim_idx, val in enumerate(batch[OBS_STATE][i]): rr.log(f"state/{dim_idx}", rr.Scalar(val.item())) - if "next.done" in batch: - rr.log("next.done", rr.Scalar(batch["next.done"][i].item())) + if DONE in batch: + rr.log(DONE, rr.Scalar(batch[DONE][i].item())) - if "next.reward" in batch: - rr.log("next.reward", rr.Scalar(batch["next.reward"][i].item())) + if REWARD in batch: + rr.log(REWARD, rr.Scalar(batch[REWARD][i].item())) if "next.success" in batch: rr.log("next.success", rr.Scalar(batch["next.success"][i].item())) diff --git a/src/lerobot/scripts/lerobot_eval.py b/src/lerobot/scripts/lerobot_eval.py index 882aeacc3..d45be5c42 100644 --- a/src/lerobot/scripts/lerobot_eval.py +++ b/src/lerobot/scripts/lerobot_eval.py @@ -81,7 +81,7 @@ from lerobot.envs.utils import ( from lerobot.policies.factory import make_policy, make_pre_post_processors from lerobot.policies.pretrained import PreTrainedPolicy from lerobot.processor import PolicyAction, PolicyProcessorPipeline -from lerobot.utils.constants import ACTION, OBS_STR +from lerobot.utils.constants import ACTION, DONE, OBS_STR, REWARD from lerobot.utils.io_utils import write_video from lerobot.utils.random_utils import set_seed from lerobot.utils.utils import ( @@ -451,9 +451,9 @@ def _compile_episode_data( "episode_index": torch.tensor([start_episode_index + ep_ix] * (num_frames - 1)), "frame_index": torch.arange(0, num_frames - 1, 1), "timestamp": torch.arange(0, num_frames - 1, 1) / fps, - "next.done": rollout_data["done"][ep_ix, : num_frames - 1], + DONE: rollout_data["done"][ep_ix, : num_frames - 1], "next.success": rollout_data["success"][ep_ix, : num_frames - 1], - "next.reward": rollout_data["reward"][ep_ix, : num_frames - 1].type(torch.float32), + REWARD: rollout_data["reward"][ep_ix, : num_frames - 1].type(torch.float32), } # For the last observation frame, all other keys will just be copy padded. diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index fcfef677b..b9e966fe6 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -46,7 +46,7 @@ from lerobot.datasets.utils import ( from lerobot.envs.factory import make_env_config from lerobot.policies.factory import make_policy_config from lerobot.robots import make_robot_from_config -from lerobot.utils.constants import ACTION, OBS_IMAGES, OBS_STATE, OBS_STR +from lerobot.utils.constants import ACTION, DONE, OBS_IMAGES, OBS_STATE, OBS_STR, REWARD from tests.fixtures.constants import DUMMY_CHW, DUMMY_HWC, DUMMY_REPO_ID from tests.mocks.mock_robot import MockRobotConfig from tests.utils import require_x86_64_kernel @@ -399,8 +399,8 @@ def test_factory(env_name, repo_id, policy_name): ("timestamp", 0, True), # TODO(rcadene): should we rename it agent_pos? (OBS_STATE, 1, True), - ("next.reward", 0, False), - ("next.done", 0, False), + (REWARD, 0, False), + (DONE, 0, False), ] # test number of dimensions diff --git a/tests/policies/hilserl/test_modeling_classifier.py b/tests/policies/hilserl/test_modeling_classifier.py index 7a8782230..a572ea9e1 100644 --- a/tests/policies/hilserl/test_modeling_classifier.py +++ b/tests/policies/hilserl/test_modeling_classifier.py @@ -19,7 +19,7 @@ import torch from lerobot.configs.types import FeatureType, NormalizationMode, PolicyFeature from lerobot.policies.sac.reward_model.configuration_classifier import RewardClassifierConfig from lerobot.policies.sac.reward_model.modeling_classifier import ClassifierOutput -from lerobot.utils.constants import OBS_IMAGE +from lerobot.utils.constants import OBS_IMAGE, REWARD from tests.utils import require_package @@ -45,7 +45,7 @@ def test_binary_classifier_with_default_params(): OBS_IMAGE: PolicyFeature(type=FeatureType.VISUAL, shape=(3, 224, 224)), } config.output_features = { - "next.reward": PolicyFeature(type=FeatureType.REWARD, shape=(1,)), + REWARD: PolicyFeature(type=FeatureType.REWARD, shape=(1,)), } config.normalization_mapping = { "VISUAL": NormalizationMode.IDENTITY, @@ -58,7 +58,7 @@ def test_binary_classifier_with_default_params(): input = { OBS_IMAGE: torch.rand((batch_size, 3, 128, 128)), - "next.reward": torch.randint(low=0, high=2, size=(batch_size,)).float(), + REWARD: torch.randint(low=0, high=2, size=(batch_size,)).float(), } images, labels = classifier.extract_images_and_labels(input) @@ -87,7 +87,7 @@ def test_multiclass_classifier(): OBS_IMAGE: PolicyFeature(type=FeatureType.VISUAL, shape=(3, 224, 224)), } config.output_features = { - "next.reward": PolicyFeature(type=FeatureType.REWARD, shape=(num_classes,)), + REWARD: PolicyFeature(type=FeatureType.REWARD, shape=(num_classes,)), } config.num_cameras = 1 config.num_classes = num_classes @@ -97,7 +97,7 @@ def test_multiclass_classifier(): input = { OBS_IMAGE: torch.rand((batch_size, 3, 128, 128)), - "next.reward": torch.rand((batch_size, num_classes)), + REWARD: torch.rand((batch_size, num_classes)), } images, labels = classifier.extract_images_and_labels(input) diff --git a/tests/processor/test_batch_conversion.py b/tests/processor/test_batch_conversion.py index 0f7018972..88b873128 100644 --- a/tests/processor/test_batch_conversion.py +++ b/tests/processor/test_batch_conversion.py @@ -2,7 +2,7 @@ import torch from lerobot.processor import DataProcessorPipeline, TransitionKey from lerobot.processor.converters import batch_to_transition, transition_to_batch -from lerobot.utils.constants import ACTION, OBS_IMAGE, OBS_PREFIX, OBS_STATE +from lerobot.utils.constants import ACTION, DONE, OBS_IMAGE, OBS_PREFIX, OBS_STATE, REWARD, TRUNCATED def _dummy_batch(): @@ -12,9 +12,9 @@ def _dummy_batch(): f"{OBS_IMAGE}.right": torch.randn(1, 3, 128, 128), OBS_STATE: torch.tensor([[0.1, 0.2, 0.3, 0.4]]), ACTION: torch.tensor([[0.5]]), - "next.reward": 1.0, - "next.done": False, - "next.truncated": False, + REWARD: 1.0, + DONE: False, + TRUNCATED: False, "info": {"key": "value"}, } @@ -38,9 +38,9 @@ def test_observation_grouping_roundtrip(): # Check other fields assert torch.allclose(batch_out[ACTION], batch_in[ACTION]) - assert batch_out["next.reward"] == batch_in["next.reward"] - assert batch_out["next.done"] == batch_in["next.done"] - assert batch_out["next.truncated"] == batch_in["next.truncated"] + assert batch_out[REWARD] == batch_in[REWARD] + assert batch_out[DONE] == batch_in[DONE] + assert batch_out[TRUNCATED] == batch_in[TRUNCATED] assert batch_out["info"] == batch_in["info"] @@ -51,9 +51,9 @@ def test_batch_to_transition_observation_grouping(): f"{OBS_IMAGE}.left": torch.randn(1, 3, 128, 128), OBS_STATE: [1, 2, 3, 4], ACTION: torch.tensor([0.1, 0.2, 0.3, 0.4]), - "next.reward": 1.5, - "next.done": True, - "next.truncated": False, + REWARD: 1.5, + DONE: True, + TRUNCATED: False, "info": {"episode": 42}, } @@ -115,9 +115,9 @@ def test_transition_to_batch_observation_flattening(): # Check other fields are mapped to next.* format assert batch[ACTION] == "action_data" - assert batch["next.reward"] == 1.5 - assert batch["next.done"] - assert not batch["next.truncated"] + assert batch[REWARD] == 1.5 + assert batch[DONE] + assert not batch[TRUNCATED] assert batch["info"] == {"episode": 42} @@ -125,9 +125,9 @@ def test_no_observation_keys(): """Test behavior when there are no observation.* keys.""" batch = { ACTION: torch.tensor([1.0, 2.0]), - "next.reward": 2.0, - "next.done": False, - "next.truncated": True, + REWARD: 2.0, + DONE: False, + TRUNCATED: True, "info": {"test": "no_obs"}, } @@ -146,9 +146,9 @@ def test_no_observation_keys(): # Round trip should work reconstructed_batch = transition_to_batch(transition) assert torch.allclose(reconstructed_batch[ACTION], torch.tensor([1.0, 2.0])) - assert reconstructed_batch["next.reward"] == 2.0 - assert not reconstructed_batch["next.done"] - assert reconstructed_batch["next.truncated"] + assert reconstructed_batch[REWARD] == 2.0 + assert not reconstructed_batch[DONE] + assert reconstructed_batch[TRUNCATED] assert reconstructed_batch["info"] == {"test": "no_obs"} @@ -173,9 +173,9 @@ def test_minimal_batch(): reconstructed_batch = transition_to_batch(transition) assert reconstructed_batch[OBS_STATE] == "minimal_state" assert torch.allclose(reconstructed_batch[ACTION], torch.tensor([0.5])) - assert reconstructed_batch["next.reward"] == 0.0 - assert not reconstructed_batch["next.done"] - assert not reconstructed_batch["next.truncated"] + assert reconstructed_batch[REWARD] == 0.0 + assert not reconstructed_batch[DONE] + assert not reconstructed_batch[TRUNCATED] assert reconstructed_batch["info"] == {} @@ -197,9 +197,9 @@ def test_empty_batch(): # Round trip reconstructed_batch = transition_to_batch(transition) assert reconstructed_batch[ACTION] is None - assert reconstructed_batch["next.reward"] == 0.0 - assert not reconstructed_batch["next.done"] - assert not reconstructed_batch["next.truncated"] + assert reconstructed_batch[REWARD] == 0.0 + assert not reconstructed_batch[DONE] + assert not reconstructed_batch[TRUNCATED] assert reconstructed_batch["info"] == {} @@ -210,9 +210,9 @@ def test_complex_nested_observation(): f"{OBS_IMAGE}.left": {"image": torch.randn(1, 3, 128, 128), "timestamp": 1234567891}, OBS_STATE: torch.randn(7), ACTION: torch.randn(8), - "next.reward": 3.14, - "next.done": False, - "next.truncated": True, + REWARD: 3.14, + DONE: False, + TRUNCATED: True, "info": {"episode_length": 200, "success": True}, } @@ -240,9 +240,9 @@ def test_complex_nested_observation(): assert torch.allclose(batch[ACTION], reconstructed_batch[ACTION]) # Check other fields - assert batch["next.reward"] == reconstructed_batch["next.reward"] - assert batch["next.done"] == reconstructed_batch["next.done"] - assert batch["next.truncated"] == reconstructed_batch["next.truncated"] + assert batch[REWARD] == reconstructed_batch[REWARD] + assert batch[DONE] == reconstructed_batch[DONE] + assert batch[TRUNCATED] == reconstructed_batch[TRUNCATED] assert batch["info"] == reconstructed_batch["info"] @@ -267,13 +267,13 @@ def test_custom_converter(): batch = { OBS_STATE: torch.randn(1, 4), ACTION: torch.randn(1, 2), - "next.reward": 1.0, - "next.done": False, + REWARD: 1.0, + DONE: False, } result = processor(batch) # Check the reward was doubled by our custom converter - assert result["next.reward"] == 2.0 + assert result[REWARD] == 2.0 assert torch.allclose(result[OBS_STATE], batch[OBS_STATE]) assert torch.allclose(result[ACTION], batch[ACTION]) diff --git a/tests/processor/test_converters.py b/tests/processor/test_converters.py index d347858dc..bc58f7a61 100644 --- a/tests/processor/test_converters.py +++ b/tests/processor/test_converters.py @@ -9,7 +9,7 @@ from lerobot.processor.converters import ( to_tensor, transition_to_batch, ) -from lerobot.utils.constants import ACTION, OBS_STATE, OBS_STR +from lerobot.utils.constants import ACTION, DONE, OBS_STATE, OBS_STR, REWARD # Tests for the unified to_tensor function @@ -201,8 +201,8 @@ def test_batch_to_transition_with_index_fields(): batch = { OBS_STATE: torch.randn(1, 7), ACTION: torch.randn(1, 4), - "next.reward": 1.5, - "next.done": False, + REWARD: 1.5, + DONE: False, "task": ["pick_cube"], "index": torch.tensor([42], dtype=torch.int64), "task_index": torch.tensor([3], dtype=torch.int64), diff --git a/tests/processor/test_pipeline.py b/tests/processor/test_pipeline.py index 6dbf37450..904fd6fc1 100644 --- a/tests/processor/test_pipeline.py +++ b/tests/processor/test_pipeline.py @@ -35,7 +35,7 @@ from lerobot.processor import ( TransitionKey, ) from lerobot.processor.converters import create_transition, identity_transition -from lerobot.utils.constants import ACTION, OBS_IMAGE, OBS_IMAGES, OBS_STATE +from lerobot.utils.constants import ACTION, DONE, OBS_IMAGE, OBS_IMAGES, OBS_STATE, REWARD, TRUNCATED from tests.conftest import assert_contract_is_typed @@ -258,9 +258,9 @@ def test_step_through_with_dict(): batch = { OBS_IMAGE: None, ACTION: None, - "next.reward": 0.0, - "next.done": False, - "next.truncated": False, + REWARD: 0.0, + DONE: False, + TRUNCATED: False, "info": {}, } @@ -1843,9 +1843,9 @@ def test_save_load_with_custom_converter_functions(): batch = { OBS_IMAGE: torch.randn(1, 3, 32, 32), ACTION: torch.randn(1, 7), - "next.reward": torch.tensor([1.0]), - "next.done": torch.tensor([False]), - "next.truncated": torch.tensor([False]), + REWARD: torch.tensor([1.0]), + DONE: torch.tensor([False]), + TRUNCATED: torch.tensor([False]), "info": {}, } diff --git a/tests/utils/test_replay_buffer.py b/tests/utils/test_replay_buffer.py index 1e6c0df95..ddf0771f1 100644 --- a/tests/utils/test_replay_buffer.py +++ b/tests/utils/test_replay_buffer.py @@ -22,7 +22,7 @@ import torch from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.rl.buffer import BatchTransition, ReplayBuffer, random_crop_vectorized -from lerobot.utils.constants import ACTION, OBS_IMAGE, OBS_STATE, OBS_STR +from lerobot.utils.constants import ACTION, DONE, OBS_IMAGE, OBS_STATE, OBS_STR, REWARD from tests.fixtures.constants import DUMMY_REPO_ID @@ -380,9 +380,9 @@ def test_to_lerobot_dataset(tmp_path): for feature, value in ds[i].items(): if feature == ACTION: assert torch.equal(value, buffer.actions[i]) - elif feature == "next.reward": + elif feature == REWARD: assert torch.equal(value, buffer.rewards[i]) - elif feature == "next.done": + elif feature == DONE: assert torch.equal(value, buffer.dones[i]) elif feature == OBS_IMAGE: # Tensor -> numpy is not precise, so we have some diff there From 49918efbc1946da822f743902ba36da8e1e398b4 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Fri, 26 Sep 2025 14:30:17 +0200 Subject: [PATCH 3/9] chore(utils): remove unused code (#2059) --- src/lerobot/utils/control_utils.py | 59 ------------------------------ src/lerobot/utils/import_utils.py | 4 -- 2 files changed, 63 deletions(-) diff --git a/src/lerobot/utils/control_utils.py b/src/lerobot/utils/control_utils.py index 47beb5746..17371921c 100644 --- a/src/lerobot/utils/control_utils.py +++ b/src/lerobot/utils/control_utils.py @@ -27,7 +27,6 @@ from typing import Any import numpy as np import torch from deepdiff import DeepDiff -from termcolor import colored from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.utils import DEFAULT_FEATURES @@ -36,64 +35,6 @@ from lerobot.processor import PolicyAction, PolicyProcessorPipeline from lerobot.robots import Robot -def log_control_info(robot: Robot, dt_s, episode_index=None, frame_index=None, fps=None): - """ - Logs performance metrics for a single step of the robot control loop. - - This function formats and prints a single line of log information, including episode/frame counters, - total loop time (dt), and detailed timings for various robot and camera operations. It can also - highlight performance drops in yellow if the actual FPS is lower than the target FPS. - - Args: - robot: The `Robot` instance, used to access its internal logs for detailed timings. - dt_s: The total duration of the control loop step in seconds. - episode_index: The index of the current episode. - frame_index: The index of the current frame within the episode. - fps: The target frames per second, used to check for performance degradation. - """ - log_items = [] - if episode_index is not None: - log_items.append(f"ep:{episode_index}") - if frame_index is not None: - log_items.append(f"frame:{frame_index}") - - def log_dt(shortname, dt_val_s): - nonlocal log_items, fps - info_str = f"{shortname}:{dt_val_s * 1000:5.2f} ({1 / dt_val_s:3.1f}hz)" - if fps is not None: - actual_fps = 1 / dt_val_s - if actual_fps < fps - 1: - info_str = colored(info_str, "yellow") - log_items.append(info_str) - - # total step time displayed in milliseconds and its frequency - log_dt("dt", dt_s) - - # TODO(aliberts): move robot-specific logs logic in robot.print_logs() - if not robot.robot_type.startswith("stretch"): - for name in robot.leader_arms: - key = f"read_leader_{name}_pos_dt_s" - if key in robot.logs: - log_dt("dtRlead", robot.logs[key]) - - for name in robot.follower_arms: - key = f"write_follower_{name}_goal_pos_dt_s" - if key in robot.logs: - log_dt("dtWfoll", robot.logs[key]) - - key = f"read_follower_{name}_pos_dt_s" - if key in robot.logs: - log_dt("dtRfoll", robot.logs[key]) - - for name in robot.cameras: - key = f"read_camera_{name}_dt_s" - if key in robot.logs: - log_dt(f"dtR{name}", robot.logs[key]) - - info_str = " ".join(log_items) - logging.info(info_str) - - @cache def is_headless(): """ diff --git a/src/lerobot/utils/import_utils.py b/src/lerobot/utils/import_utils.py index 09e649372..5f41ea3a3 100644 --- a/src/lerobot/utils/import_utils.py +++ b/src/lerobot/utils/import_utils.py @@ -57,8 +57,4 @@ def is_package_available(pkg_name: str, return_version: bool = False) -> tuple[b return package_exists -_torch_available, _torch_version = is_package_available("torch", return_version=True) _transformers_available = is_package_available("transformers") -_gym_xarm_available = is_package_available("gym_xarm") -_gym_aloha_available = is_package_available("gym_aloha") -_gym_pusht_available = is_package_available("gym_pusht") From ddfff054bc80438e70a64088d73564aba0aec67b Mon Sep 17 00:00:00 2001 From: Adil Zouitine Date: Fri, 26 Sep 2025 14:32:29 +0200 Subject: [PATCH 4/9] feat(train): enhance processor overrides with normalizer and unnormalizer stats (#2038) --- src/lerobot/scripts/lerobot_train.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lerobot/scripts/lerobot_train.py b/src/lerobot/scripts/lerobot_train.py index 5ef8c7263..86b2bbae5 100644 --- a/src/lerobot/scripts/lerobot_train.py +++ b/src/lerobot/scripts/lerobot_train.py @@ -185,7 +185,13 @@ def train(cfg: TrainPipelineConfig): processor_kwargs["dataset_stats"] = dataset.meta.stats if cfg.policy.pretrained_path is not None: - processor_kwargs["preprocessor_overrides"] = {"device_processor": {"device": device.type}} + processor_kwargs["preprocessor_overrides"] = { + "device_processor": {"device": device.type}, + "normalizer_processor": {"stats": dataset.meta.stats}, + } + processor_kwargs["postprocessor_overrides"] = { + "unnormalizer_processor": {"stats": dataset.meta.stats}, + } preprocessor, postprocessor = make_pre_post_processors( policy_cfg=cfg.policy, pretrained_path=cfg.policy.pretrained_path, **processor_kwargs From 5b647e3bcbedc2a734714a1f7f219a861ec843ab Mon Sep 17 00:00:00 2001 From: Jade Choghari Date: Fri, 26 Sep 2025 15:09:42 +0200 Subject: [PATCH 5/9] docs(fix): libero example command (#2060) Signed-off-by: Jade Choghari --- docs/source/libero.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/libero.mdx b/docs/source/libero.mdx index 17e12d45e..eafe3e78b 100644 --- a/docs/source/libero.mdx +++ b/docs/source/libero.mdx @@ -106,6 +106,7 @@ For reference, here is the **original dataset** published by Physical Intelligen lerobot-train \ --policy.type=smolvla \ --policy.repo_id=${HF_USER}/libero-test \ + --policy.load_vlm_weights=true \ --dataset.repo_id=HuggingFaceVLA/libero \ --env.type=libero \ --env.task=libero_10 \ From e3b572992e2d66d44e0e6e9f02fdf40d6f484866 Mon Sep 17 00:00:00 2001 From: Francesco Capuano <74058581+fracapuano@users.noreply.github.com> Date: Sat, 27 Sep 2025 16:07:53 +0200 Subject: [PATCH 6/9] Save Cropped Dataset to Hub (#2071) * fix: cast fps argument from dataset to int * fix: typo * fix: specify repo-id --- src/lerobot/rl/crop_dataset_roi.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/lerobot/rl/crop_dataset_roi.py b/src/lerobot/rl/crop_dataset_roi.py index 281069e14..4345fed3c 100644 --- a/src/lerobot/rl/crop_dataset_roi.py +++ b/src/lerobot/rl/crop_dataset_roi.py @@ -160,7 +160,7 @@ def get_image_from_lerobot_dataset(dataset: LeRobotDataset): return image_dict -def convert_lerobot_dataset_to_cropper_lerobot_dataset( +def convert_lerobot_dataset_to_cropped_lerobot_dataset( original_dataset: LeRobotDataset, crop_params_dict: dict[str, tuple[int, int, int, int]], new_repo_id: str, @@ -190,7 +190,7 @@ def convert_lerobot_dataset_to_cropper_lerobot_dataset( # 1. Create a new (empty) LeRobotDataset for writing. new_dataset = LeRobotDataset.create( repo_id=new_repo_id, - fps=original_dataset.fps, + fps=int(original_dataset.fps), root=new_dataset_root, robot_type=original_dataset.meta.robot_type, features=original_dataset.meta.info["features"], @@ -275,6 +275,12 @@ if __name__ == "__main__": default="", help="The natural language task to describe the dataset.", ) + parser.add_argument( + "--new-repo-id", + type=str, + default=None, + help="The repository id for the new cropped and resized dataset. If not provided, it defaults to `repo_id` + '_cropped_resized'.", + ) args = parser.parse_args() dataset = LeRobotDataset(repo_id=args.repo_id, root=args.root) @@ -294,10 +300,16 @@ if __name__ == "__main__": for key, roi in rois.items(): print(f"{key}: {roi}") - new_repo_id = args.repo_id + "_cropped_resized" - new_dataset_root = Path(str(dataset.root) + "_cropped_resized") + new_repo_id = args.new_repo_id if args.new_repo_id else args.repo_id + "_cropped_resized" - cropped_resized_dataset = convert_lerobot_dataset_to_cropper_lerobot_dataset( + if args.new_repo_id: + new_dataset_name = args.new_repo_id.split("/")[-1] + # Parent 1: HF user, Parent 2: HF LeRobot Home + new_dataset_root = dataset.root.parent.parent / new_dataset_name + else: + new_dataset_root = Path(str(dataset.root) + "_cropped_resized") + + cropped_resized_dataset = convert_lerobot_dataset_to_cropped_lerobot_dataset( original_dataset=dataset, crop_params_dict=rois, new_repo_id=new_repo_id, From 62e9849ffd4a600ca48dbc0f14d3f860e633c568 Mon Sep 17 00:00:00 2001 From: Qizhi Chen Date: Sun, 28 Sep 2025 20:18:22 +0800 Subject: [PATCH 7/9] use abs path when concatenating (#2076) --- src/lerobot/datasets/video_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lerobot/datasets/video_utils.py b/src/lerobot/datasets/video_utils.py index 9da89022b..b4c036e6c 100644 --- a/src/lerobot/datasets/video_utils.py +++ b/src/lerobot/datasets/video_utils.py @@ -428,7 +428,7 @@ def concatenate_video_files( with tempfile.NamedTemporaryFile(mode="w", suffix=".ffconcat", delete=False) as tmp_concatenate_file: tmp_concatenate_file.write("ffconcat version 1.0\n") for input_path in input_video_paths: - tmp_concatenate_file.write(f"file '{str(input_path)}'\n") + tmp_concatenate_file.write(f"file '{str(input_path.resolve())}'\n") tmp_concatenate_file.flush() tmp_concatenate_path = tmp_concatenate_file.name From f59eb54f5c0e3f46286e15305402bed2830a7848 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Mon, 29 Sep 2025 10:49:36 +0200 Subject: [PATCH 8/9] chore: remove unused code (#2062) --- docs/source/hilserl.mdx | 3 - docs/source/phone_teleop.mdx | 3 +- docs/source/processors_robots_teleop.mdx | 2 +- examples/phone_to_so100/record.py | 1 - examples/phone_to_so100/teleoperate.py | 1 - examples/so100_to_so100_EE/record.py | 1 - examples/so100_to_so100_EE/teleoperate.py | 1 - src/lerobot/async_inference/helpers.py | 3 +- src/lerobot/cameras/utils.py | 4 -- src/lerobot/configs/default.py | 3 - src/lerobot/configs/types.py | 5 -- src/lerobot/datasets/lerobot_dataset.py | 10 ---- .../datasets/push_dataset_to_hub/utils.py | 57 ------------------- src/lerobot/datasets/streaming_dataset.py | 4 +- src/lerobot/datasets/utils.py | 49 ---------------- src/lerobot/datasets/video_utils.py | 13 ----- src/lerobot/envs/configs.py | 2 - src/lerobot/motors/motors_bus.py | 6 -- src/lerobot/policies/sac/configuration_sac.py | 2 - src/lerobot/policies/sac/modeling_sac.py | 12 ---- .../policies/vqbet/configuration_vqbet.py | 2 - src/lerobot/policies/vqbet/vqbet_utils.py | 10 ---- .../processor/delta_action_processor.py | 2 - src/lerobot/rl/actor.py | 2 - src/lerobot/rl/learner.py | 2 - src/lerobot/robots/hope_jr/hope_jr_arm.py | 2 +- .../robot_kinematic_processor.py | 6 -- .../robots/stretch3/configuration_stretch3.py | 2 - src/lerobot/robots/stretch3/robot_stretch3.py | 4 -- .../teleoperators/gamepad/gamepad_utils.py | 22 ------- .../keyboard/configuration_keyboard.py | 3 +- .../configuration_stretch3.py | 2 +- .../stretch3_gamepad/stretch3_gamepad.py | 4 -- src/lerobot/utils/constants.py | 1 - src/lerobot/utils/errors.py | 11 ---- src/lerobot/utils/utils.py | 4 -- tests/policies/test_sac_config.py | 1 - 37 files changed, 8 insertions(+), 254 deletions(-) diff --git a/docs/source/hilserl.mdx b/docs/source/hilserl.mdx index bc38408e6..ad1c74f9a 100644 --- a/docs/source/hilserl.mdx +++ b/docs/source/hilserl.mdx @@ -95,7 +95,6 @@ class HILSerlProcessorConfig: class ObservationConfig: add_joint_velocity_to_observation: bool = False # Add joint velocities to state add_current_to_observation: bool = False # Add motor currents to state - add_ee_pose_to_observation: bool = False # Add end-effector pose to state display_cameras: bool = False # Display camera feeds during execution class ImagePreprocessingConfig: @@ -105,7 +104,6 @@ class ImagePreprocessingConfig: class GripperConfig: use_gripper: bool = True # Enable gripper control gripper_penalty: float = 0.0 # Penalty for inappropriate gripper usage - gripper_penalty_in_reward: bool = False # Include gripper penalty in reward class ResetConfig: fixed_reset_joint_positions: Any | None = None # Joint positions for reset @@ -288,7 +286,6 @@ You can enable multiple observation processing features simultaneously: "observation": { "add_joint_velocity_to_observation": true, "add_current_to_observation": true, - "add_ee_pose_to_observation": false, "display_cameras": false } } diff --git a/docs/source/phone_teleop.mdx b/docs/source/phone_teleop.mdx index bab0ac28e..22159193c 100644 --- a/docs/source/phone_teleop.mdx +++ b/docs/source/phone_teleop.mdx @@ -136,13 +136,12 @@ Additionally you can customize mapping or safety limits by editing the processor ), ``` -- The `EEBoundsAndSafety` step clamps EE motion to a workspace and checks for large ee step jumps to ensure safety. The `end_effector_bounds` are the bounds for the EE pose and can be modified to change the workspace. The `max_ee_step_m` and `max_ee_twist_step_rad` are the step limits for the EE pose and can be modified to change the safety limits. +- The `EEBoundsAndSafety` step clamps EE motion to a workspace and checks for large ee step jumps to ensure safety. The `end_effector_bounds` are the bounds for the EE pose and can be modified to change the workspace. The `max_ee_step_m` are the step limits for the EE pose and can be modified to change the safety limits. ```examples/phone_to_so100/teleoperate.py EEBoundsAndSafety( end_effector_bounds={"min": [-1.0, -1.0, -1.0], "max": [1.0, 1.0, 1.0]}, max_ee_step_m=0.10, - max_ee_twist_step_rad=0.50, ) ``` diff --git a/docs/source/processors_robots_teleop.mdx b/docs/source/processors_robots_teleop.mdx index c4fcbe03d..3d8dcb409 100644 --- a/docs/source/processors_robots_teleop.mdx +++ b/docs/source/processors_robots_teleop.mdx @@ -38,7 +38,7 @@ phone_to_robot_ee_pose_processor = RobotProcessorPipeline[RobotAction, RobotActi kinematics=kinematics_solver, end_effector_step_sizes={"x": 0.5, "y": 0.5, "z": 0.5}, motor_names=list(robot.bus.motors.keys()), ), EEBoundsAndSafety( - end_effector_bounds={"min": [-1.0, -1.0, -1.0], "max": [1.0, 1.0, 1.0]}, max_ee_step_m=0.20, max_ee_twist_step_rad=0.50, + end_effector_bounds={"min": [-1.0, -1.0, -1.0], "max": [1.0, 1.0, 1.0]}, max_ee_step_m=0.20, ), GripperVelocityToJoint(), ], diff --git a/examples/phone_to_so100/record.py b/examples/phone_to_so100/record.py index bb2e2f5f7..d3ef293a7 100644 --- a/examples/phone_to_so100/record.py +++ b/examples/phone_to_so100/record.py @@ -84,7 +84,6 @@ phone_to_robot_ee_pose_processor = RobotProcessorPipeline[tuple[RobotAction, Rob EEBoundsAndSafety( end_effector_bounds={"min": [-1.0, -1.0, -1.0], "max": [1.0, 1.0, 1.0]}, max_ee_step_m=0.20, - max_ee_twist_step_rad=0.50, ), GripperVelocityToJoint(speed_factor=20.0), ], diff --git a/examples/phone_to_so100/teleoperate.py b/examples/phone_to_so100/teleoperate.py index 6c49a8453..783dce242 100644 --- a/examples/phone_to_so100/teleoperate.py +++ b/examples/phone_to_so100/teleoperate.py @@ -67,7 +67,6 @@ phone_to_robot_joints_processor = RobotProcessorPipeline[tuple[RobotAction, Robo EEBoundsAndSafety( end_effector_bounds={"min": [-1.0, -1.0, -1.0], "max": [1.0, 1.0, 1.0]}, max_ee_step_m=0.10, - max_ee_twist_step_rad=0.50, ), GripperVelocityToJoint( speed_factor=20.0, diff --git a/examples/so100_to_so100_EE/record.py b/examples/so100_to_so100_EE/record.py index 6c38553e2..9ed6e51a9 100644 --- a/examples/so100_to_so100_EE/record.py +++ b/examples/so100_to_so100_EE/record.py @@ -101,7 +101,6 @@ ee_to_follower_joints = RobotProcessorPipeline[tuple[RobotAction, RobotObservati EEBoundsAndSafety( end_effector_bounds={"min": [-1.0, -1.0, -1.0], "max": [1.0, 1.0, 1.0]}, max_ee_step_m=0.10, - max_ee_twist_step_rad=0.50, ), InverseKinematicsEEToJoints( kinematics=follower_kinematics_solver, diff --git a/examples/so100_to_so100_EE/teleoperate.py b/examples/so100_to_so100_EE/teleoperate.py index aa9755788..b1a8c8c27 100644 --- a/examples/so100_to_so100_EE/teleoperate.py +++ b/examples/so100_to_so100_EE/teleoperate.py @@ -78,7 +78,6 @@ ee_to_follower_joints = RobotProcessorPipeline[tuple[RobotAction, RobotObservati EEBoundsAndSafety( end_effector_bounds={"min": [-1.0, -1.0, -1.0], "max": [1.0, 1.0, 1.0]}, max_ee_step_m=0.10, - max_ee_twist_step_rad=0.50, ), InverseKinematicsEEToJoints( kinematics=follower_kinematics_solver, diff --git a/src/lerobot/async_inference/helpers.py b/src/lerobot/async_inference/helpers.py index 75d81a0f3..a336d3a63 100644 --- a/src/lerobot/async_inference/helpers.py +++ b/src/lerobot/async_inference/helpers.py @@ -31,7 +31,6 @@ from lerobot.utils.constants import OBS_IMAGES, OBS_STATE, OBS_STR from lerobot.utils.utils import init_logging Action = torch.Tensor -ActionChunk = torch.Tensor # observation as received from the robot RawObservation = dict[str, torch.Tensor] @@ -46,7 +45,7 @@ Observation = dict[str, torch.Tensor] def visualize_action_queue_size(action_queue_size: list[int]) -> None: import matplotlib.pyplot as plt - fig, ax = plt.subplots() + _, ax = plt.subplots() ax.set_title("Action Queue Size Over Time") ax.set_xlabel("Environment steps") ax.set_ylabel("Action Queue Size") diff --git a/src/lerobot/cameras/utils.py b/src/lerobot/cameras/utils.py index dfac33e17..4a23843b2 100644 --- a/src/lerobot/cameras/utils.py +++ b/src/lerobot/cameras/utils.py @@ -15,14 +15,10 @@ # limitations under the License. import platform -from pathlib import Path -from typing import TypeAlias from .camera import Camera from .configs import CameraConfig, Cv2Rotation -IndexOrPath: TypeAlias = int | Path - def make_cameras_from_configs(camera_configs: dict[str, CameraConfig]) -> dict[str, Camera]: cameras = {} diff --git a/src/lerobot/configs/default.py b/src/lerobot/configs/default.py index 1bc2b8d16..afd644e1c 100644 --- a/src/lerobot/configs/default.py +++ b/src/lerobot/configs/default.py @@ -16,9 +16,6 @@ from dataclasses import dataclass, field -from lerobot import ( - policies, # noqa: F401 -) from lerobot.datasets.transforms import ImageTransformsConfig from lerobot.datasets.video_utils import get_safe_default_codec diff --git a/src/lerobot/configs/types.py b/src/lerobot/configs/types.py index e02527840..754aca1ab 100644 --- a/src/lerobot/configs/types.py +++ b/src/lerobot/configs/types.py @@ -15,7 +15,6 @@ # https://stackoverflow.com/questions/24481852/serialising-an-enum-member-to-json from dataclasses import dataclass from enum import Enum -from typing import Any, Protocol class FeatureType(str, Enum): @@ -38,10 +37,6 @@ class NormalizationMode(str, Enum): IDENTITY = "IDENTITY" -class DictLike(Protocol): - def __getitem__(self, key: Any) -> Any: ... - - @dataclass class PolicyFeature: type: FeatureType diff --git a/src/lerobot/datasets/lerobot_dataset.py b/src/lerobot/datasets/lerobot_dataset.py index 9eebcea4b..b8aa880da 100644 --- a/src/lerobot/datasets/lerobot_dataset.py +++ b/src/lerobot/datasets/lerobot_dataset.py @@ -848,11 +848,6 @@ class LeRobotDataset(torch.utils.data.Dataset): return item - def _add_padding_keys(self, item: dict, padding: dict[str, list[bool]]) -> dict: - for key, val in padding.items(): - item[key] = torch.BoolTensor(val) - return item - def __len__(self): return self.num_frames @@ -1396,11 +1391,6 @@ class MultiLeRobotDataset(torch.utils.data.Dataset): """ return {repo_id: i for i, repo_id in enumerate(self.repo_ids)} - @property - def repo_index_to_id(self): - """Return the inverse mapping if repo_id_to_index.""" - return {v: k for k, v in self.repo_id_to_index} - @property def fps(self) -> int: """Frames per second used during data collection. diff --git a/src/lerobot/datasets/push_dataset_to_hub/utils.py b/src/lerobot/datasets/push_dataset_to_hub/utils.py index 5f6363a77..48214e1bf 100644 --- a/src/lerobot/datasets/push_dataset_to_hub/utils.py +++ b/src/lerobot/datasets/push_dataset_to_hub/utils.py @@ -13,67 +13,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import inspect -from concurrent.futures import ThreadPoolExecutor -from pathlib import Path import datasets -import numpy -import PIL import torch -from lerobot.datasets.video_utils import encode_video_frames - - -def concatenate_episodes(ep_dicts): - data_dict = {} - - keys = ep_dicts[0].keys() - for key in keys: - if torch.is_tensor(ep_dicts[0][key][0]): - data_dict[key] = torch.cat([ep_dict[key] for ep_dict in ep_dicts]) - else: - if key not in data_dict: - data_dict[key] = [] - for ep_dict in ep_dicts: - for x in ep_dict[key]: - data_dict[key].append(x) - - total_frames = data_dict["frame_index"].shape[0] - data_dict["index"] = torch.arange(0, total_frames, 1) - return data_dict - - -def save_images_concurrently(imgs_array: numpy.array, out_dir: Path, max_workers: int = 4): - out_dir = Path(out_dir) - out_dir.mkdir(parents=True, exist_ok=True) - - def save_image(img_array, i, out_dir): - img = PIL.Image.fromarray(img_array) - img.save(str(out_dir / f"frame_{i:06d}.png"), quality=100) - - num_images = len(imgs_array) - with ThreadPoolExecutor(max_workers=max_workers) as executor: - [executor.submit(save_image, imgs_array[i], i, out_dir) for i in range(num_images)] - - -def get_default_encoding() -> dict: - """Returns the default ffmpeg encoding parameters used by `encode_video_frames`.""" - signature = inspect.signature(encode_video_frames) - return { - k: v.default - for k, v in signature.parameters.items() - if v.default is not inspect.Parameter.empty and k in ["vcodec", "pix_fmt", "g", "crf"] - } - - -def check_repo_id(repo_id: str) -> None: - if len(repo_id.split("/")) != 2: - raise ValueError( - f"""`repo_id` is expected to contain a community or user id `/` the name of the dataset - (e.g. 'lerobot/pusht'), but contains '{repo_id}'.""" - ) - # TODO(aliberts): remove def calculate_episode_data_index(hf_dataset: datasets.Dataset) -> dict[str, torch.Tensor]: diff --git a/src/lerobot/datasets/streaming_dataset.py b/src/lerobot/datasets/streaming_dataset.py index c3c48d90d..454389d46 100644 --- a/src/lerobot/datasets/streaming_dataset.py +++ b/src/lerobot/datasets/streaming_dataset.py @@ -298,9 +298,7 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset): return padding_mask - def make_frame( - self, dataset_iterator: Backtrackable, previous_dataset_iterator: Backtrackable | None = None - ) -> Generator: + def make_frame(self, dataset_iterator: Backtrackable) -> Generator: """Makes a frame starting from a dataset iterator""" item = next(dataset_iterator) item = item_to_torch(item) diff --git a/src/lerobot/datasets/utils.py b/src/lerobot/datasets/utils.py index 35313bde5..81b361ab6 100644 --- a/src/lerobot/datasets/utils.py +++ b/src/lerobot/datasets/utils.py @@ -67,18 +67,6 @@ DEFAULT_IMAGE_PATH = "images/{image_key}/episode-{episode_index:06d}/frame-{fram LEGACY_EPISODES_PATH = "meta/episodes.jsonl" LEGACY_EPISODES_STATS_PATH = "meta/episodes_stats.jsonl" LEGACY_TASKS_PATH = "meta/tasks.jsonl" -LEGACY_DEFAULT_VIDEO_PATH = "videos/chunk-{episode_chunk:03d}/{video_key}/episode_{episode_index:06d}.mp4" -LEGACY_DEFAULT_PARQUET_PATH = "data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet" - -DATASET_CARD_TEMPLATE = """ ---- -# Metadata will go there ---- -This dataset was created using [LeRobot](https://github.com/huggingface/lerobot). - -## {} - -""" DEFAULT_FEATURES = { "timestamp": {"dtype": "float32", "shape": (1,), "names": None}, @@ -383,12 +371,6 @@ def load_episodes(local_dir: Path) -> datasets.Dataset: return episodes -def backward_compatible_episodes_stats( - stats: dict[str, dict[str, np.ndarray]], episodes: list[int] -) -> dict[int, dict[str, dict[str, np.ndarray]]]: - return dict.fromkeys(episodes, stats) - - def load_image_as_numpy( fpath: str | Path, dtype: np.dtype = np.float32, channel_first: bool = True ) -> np.ndarray: @@ -1346,12 +1328,6 @@ class Backtrackable(Generic[T]): # When cursor<0, slice so the order remains chronological return list(self._back_buf)[: self._cursor or None] - def lookahead_buffer(self) -> list[T]: - """ - Return a copy of the current lookahead buffer. - """ - return list(self._ahead_buf) - def can_peek_back(self, steps: int = 1) -> bool: """ Check if we can go back `steps` items without raising an IndexError. @@ -1377,31 +1353,6 @@ class Backtrackable(Generic[T]): except StopIteration: return False - def reset_cursor(self) -> None: - """ - Reset cursor to the most recent position (equivalent to calling next() - until you're back to the latest item). - """ - self._cursor = 0 - - def clear_ahead_buffer(self) -> None: - """ - Clear the ahead buffer, discarding any pre-fetched items. - """ - self._ahead_buf.clear() - - def switch_source_iterable(self, new_source: Iterable[T]) -> None: - """ - Switch the source of the backtrackable to a new iterable, keeping the history. - - This is useful when iterating over a sequence of datasets. The history from the - previous source is kept, but the lookahead buffer is cleared. The cursor is reset - to the present. - """ - self._source = iter(new_source) - self.clear_ahead_buffer() - self.reset_cursor() - def safe_shard(dataset: datasets.IterableDataset, index: int, num_shards: int) -> datasets.Dataset: """ diff --git a/src/lerobot/datasets/video_utils.py b/src/lerobot/datasets/video_utils.py index b4c036e6c..5f8b207e0 100644 --- a/src/lerobot/datasets/video_utils.py +++ b/src/lerobot/datasets/video_utils.py @@ -585,19 +585,6 @@ def get_video_pixel_channels(pix_fmt: str) -> int: raise ValueError("Unknown format") -def get_image_pixel_channels(image: Image): - if image.mode == "L": - return 1 # Grayscale - elif image.mode == "LA": - return 2 # Grayscale + Alpha - elif image.mode == "RGB": - return 3 # RGB - elif image.mode == "RGBA": - return 4 # RGBA - else: - raise ValueError("Unknown format") - - def get_video_duration_in_s(video_path: Path | str) -> float: """ Get the duration of a video file in seconds using PyAV. diff --git a/src/lerobot/envs/configs.py b/src/lerobot/envs/configs.py index 8cbc597dc..8c0c8b3ab 100644 --- a/src/lerobot/envs/configs.py +++ b/src/lerobot/envs/configs.py @@ -193,7 +193,6 @@ class ObservationConfig: add_joint_velocity_to_observation: bool = False add_current_to_observation: bool = False - add_ee_pose_to_observation: bool = False display_cameras: bool = False @@ -203,7 +202,6 @@ class GripperConfig: use_gripper: bool = True gripper_penalty: float = 0.0 - gripper_penalty_in_reward: bool = False @dataclass diff --git a/src/lerobot/motors/motors_bus.py b/src/lerobot/motors/motors_bus.py index dca7650e0..8603d81a9 100644 --- a/src/lerobot/motors/motors_bus.py +++ b/src/lerobot/motors/motors_bus.py @@ -99,12 +99,6 @@ class Motor: norm_mode: MotorNormMode -class JointOutOfRangeError(Exception): - def __init__(self, message="Joint is out of range"): - self.message = message - super().__init__(self.message) - - class PortHandler(Protocol): def __init__(self, port_name): self.is_open: bool diff --git a/src/lerobot/policies/sac/configuration_sac.py b/src/lerobot/policies/sac/configuration_sac.py index 6b5ad5b59..ada12330c 100644 --- a/src/lerobot/policies/sac/configuration_sac.py +++ b/src/lerobot/policies/sac/configuration_sac.py @@ -139,8 +139,6 @@ class SACConfig(PreTrainedConfig): # Training parameter # Number of steps for online training online_steps: int = 1000000 - # Seed for the online environment - online_env_seed: int = 10000 # Capacity of the online replay buffer online_buffer_capacity: int = 100000 # Capacity of the offline replay buffer diff --git a/src/lerobot/policies/sac/modeling_sac.py b/src/lerobot/policies/sac/modeling_sac.py index c66044406..c7c6798ed 100644 --- a/src/lerobot/policies/sac/modeling_sac.py +++ b/src/lerobot/policies/sac/modeling_sac.py @@ -1061,15 +1061,3 @@ class TanhMultivariateNormalDiag(TransformedDistribution): x = transform(x) return x - - -def _convert_normalization_params_to_tensor(normalization_params: dict) -> dict: - converted_params = {} - for outer_key, inner_dict in normalization_params.items(): - converted_params[outer_key] = {} - for key, value in inner_dict.items(): - converted_params[outer_key][key] = torch.tensor(value) - if "image" in outer_key: - converted_params[outer_key][key] = converted_params[outer_key][key].view(3, 1, 1) - - return converted_params diff --git a/src/lerobot/policies/vqbet/configuration_vqbet.py b/src/lerobot/policies/vqbet/configuration_vqbet.py index d7a79f189..44ada9f17 100644 --- a/src/lerobot/policies/vqbet/configuration_vqbet.py +++ b/src/lerobot/policies/vqbet/configuration_vqbet.py @@ -82,7 +82,6 @@ class VQBeTConfig(PreTrainedConfig): gpt_n_head: Number of headers of GPT gpt_hidden_dim: Size of hidden dimensions of GPT dropout: Dropout rate for GPT - mlp_hidden_dim: Size of hidden dimensions of offset header / bin prediction headers parts of VQ-BeT offset_loss_weight: A constant that is multiplied to the offset loss primary_code_loss_weight: A constant that is multiplied to the primary code prediction loss secondary_code_loss_weight: A constant that is multiplied to the secondary code prediction loss @@ -125,7 +124,6 @@ class VQBeTConfig(PreTrainedConfig): gpt_n_head: int = 8 gpt_hidden_dim: int = 512 dropout: float = 0.1 - mlp_hidden_dim: int = 1024 offset_loss_weight: float = 10000.0 primary_code_loss_weight: float = 5.0 secondary_code_loss_weight: float = 0.5 diff --git a/src/lerobot/policies/vqbet/vqbet_utils.py b/src/lerobot/policies/vqbet/vqbet_utils.py index e0afe5585..44b7d5f0b 100644 --- a/src/lerobot/policies/vqbet/vqbet_utils.py +++ b/src/lerobot/policies/vqbet/vqbet_utils.py @@ -231,16 +231,6 @@ class GPT(nn.Module): torch.nn.init.zeros_(module.bias) torch.nn.init.ones_(module.weight) - def crop_block_size(self, gpt_block_size): - # model surgery to decrease the block size if necessary - # e.g. we may load the GPT2 pretrained model checkpoint (block size 1024) - # but want to use a smaller block size for some smaller, simpler model - assert gpt_block_size <= self.config.gpt_block_size - self.config.gpt_block_size = gpt_block_size - self.transformer.wpe.weight = nn.Parameter(self.transformer.wpe.weight[:gpt_block_size]) - for block in self.transformer.h: - block.attn.bias = block.attn.bias[:, :, :gpt_block_size, :gpt_block_size] - def configure_parameters(self): """ This long function is unfortunately doing something very simple and is being very defensive: diff --git a/src/lerobot/processor/delta_action_processor.py b/src/lerobot/processor/delta_action_processor.py index 949ae78d5..a8395637c 100644 --- a/src/lerobot/processor/delta_action_processor.py +++ b/src/lerobot/processor/delta_action_processor.py @@ -83,14 +83,12 @@ class MapDeltaActionToRobotActionStep(RobotActionProcessorStep): Attributes: position_scale: A factor to scale the delta position inputs. - rotation_scale: A factor to scale the delta rotation inputs (currently unused). noise_threshold: The magnitude below which delta inputs are considered noise and do not trigger an "enabled" state. """ # Scale factors for delta movements position_scale: float = 1.0 - rotation_scale: float = 0.0 # No rotation deltas for gamepad/keyboard noise_threshold: float = 1e-3 # 1 mm threshold to filter out noise def action(self, action: RobotAction) -> RobotAction: diff --git a/src/lerobot/rl/actor.py b/src/lerobot/rl/actor.py index 3c025a05d..54d0fba69 100644 --- a/src/lerobot/rl/actor.py +++ b/src/lerobot/rl/actor.py @@ -97,8 +97,6 @@ from .gym_manipulator import ( step_env_and_process_transition, ) -ACTOR_SHUTDOWN_TIMEOUT = 30 - # Main entry point diff --git a/src/lerobot/rl/learner.py b/src/lerobot/rl/learner.py index b7cfdb30c..d9758d3a3 100644 --- a/src/lerobot/rl/learner.py +++ b/src/lerobot/rl/learner.py @@ -102,8 +102,6 @@ from lerobot.utils.utils import ( from .learner_service import MAX_WORKERS, SHUTDOWN_TIMEOUT, LearnerService -LOG_PREFIX = "[LEARNER]" - @parser.wrap() def train_cli(cfg: TrainRLServerPipelineConfig): diff --git a/src/lerobot/robots/hope_jr/hope_jr_arm.py b/src/lerobot/robots/hope_jr/hope_jr_arm.py index baa36b560..220a29f8c 100644 --- a/src/lerobot/robots/hope_jr/hope_jr_arm.py +++ b/src/lerobot/robots/hope_jr/hope_jr_arm.py @@ -105,7 +105,7 @@ class HopeJrArm(Robot): def is_calibrated(self) -> bool: return self.bus.is_calibrated - def calibrate(self, limb_name: str = None) -> None: + def calibrate(self) -> None: groups = { "all": list(self.bus.motors.keys()), "shoulder": ["shoulder_pitch", "shoulder_yaw", "shoulder_roll"], diff --git a/src/lerobot/robots/so100_follower/robot_kinematic_processor.py b/src/lerobot/robots/so100_follower/robot_kinematic_processor.py index 56686d447..87e832db6 100644 --- a/src/lerobot/robots/so100_follower/robot_kinematic_processor.py +++ b/src/lerobot/robots/so100_follower/robot_kinematic_processor.py @@ -193,16 +193,12 @@ class EEBoundsAndSafety(RobotActionProcessorStep): Attributes: end_effector_bounds: A dictionary with "min" and "max" keys for position clipping. max_ee_step_m: The maximum allowed change in position (in meters) between steps. - max_ee_twist_step_rad: The maximum allowed change in orientation (in radians) between steps. _last_pos: Internal state storing the last commanded position. - _last_twist: Internal state storing the last commanded orientation. """ end_effector_bounds: dict max_ee_step_m: float = 0.05 - max_ee_twist_step_rad: float = 0.20 _last_pos: np.ndarray | None = field(default=None, init=False, repr=False) - _last_twist: np.ndarray | None = field(default=None, init=False, repr=False) def action(self, action: RobotAction) -> RobotAction: x = action["ee.x"] @@ -233,7 +229,6 @@ class EEBoundsAndSafety(RobotActionProcessorStep): raise ValueError(f"EE jump {n:.3f}m > {self.max_ee_step_m}m") self._last_pos = pos - self._last_twist = twist action["ee.x"] = float(pos[0]) action["ee.y"] = float(pos[1]) @@ -246,7 +241,6 @@ class EEBoundsAndSafety(RobotActionProcessorStep): def reset(self): """Resets the last known position and orientation.""" self._last_pos = None - self._last_twist = None def transform_features( self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]] diff --git a/src/lerobot/robots/stretch3/configuration_stretch3.py b/src/lerobot/robots/stretch3/configuration_stretch3.py index d4e217ca0..c1226bf90 100644 --- a/src/lerobot/robots/stretch3/configuration_stretch3.py +++ b/src/lerobot/robots/stretch3/configuration_stretch3.py @@ -49,5 +49,3 @@ class Stretch3RobotConfig(RobotConfig): ), } ) - - mock: bool = False diff --git a/src/lerobot/robots/stretch3/robot_stretch3.py b/src/lerobot/robots/stretch3/robot_stretch3.py index 8a0ff5c6a..73df360b2 100644 --- a/src/lerobot/robots/stretch3/robot_stretch3.py +++ b/src/lerobot/robots/stretch3/robot_stretch3.py @@ -164,10 +164,6 @@ class Stretch3Robot(Robot): # TODO(aliberts): return action_sent when motion is limited return action - def print_logs(self) -> None: - pass - # TODO(aliberts): move robot-specific logs logic here - def teleop_safety_stop(self) -> None: if self.teleop is not None: self.teleop._safety_stop(robot=self) diff --git a/src/lerobot/teleoperators/gamepad/gamepad_utils.py b/src/lerobot/teleoperators/gamepad/gamepad_utils.py index d994dadd1..9f94b6746 100644 --- a/src/lerobot/teleoperators/gamepad/gamepad_utils.py +++ b/src/lerobot/teleoperators/gamepad/gamepad_utils.py @@ -52,10 +52,6 @@ class InputController: """Get the current movement deltas (dx, dy, dz) in meters.""" return 0.0, 0.0, 0.0 - def should_quit(self): - """Return True if the user has requested to quit.""" - return not self.running - def update(self): """Update controller state - call this once per frame.""" pass @@ -198,14 +194,6 @@ class KeyboardController(InputController): return delta_x, delta_y, delta_z - def should_quit(self): - """Return True if ESC was pressed.""" - return self.key_states["quit"] - - def should_save(self): - """Return True if Enter was pressed (save episode).""" - return self.key_states["success"] or self.key_states["failure"] - class GamepadController(InputController): """Generate motion deltas from gamepad input.""" @@ -351,8 +339,6 @@ class GamepadControllerHID(InputController): # Button states self.buttons = {} - self.quit_requested = False - self.save_requested = False def find_device(self): """Look for the gamepad device by vendor and product ID.""" @@ -472,11 +458,3 @@ class GamepadControllerHID(InputController): delta_z = -self.right_y * self.z_step_size # Up/down return delta_x, delta_y, delta_z - - def should_quit(self): - """Return True if quit button was pressed.""" - return self.quit_requested - - def should_save(self): - """Return True if save button was pressed.""" - return self.save_requested diff --git a/src/lerobot/teleoperators/keyboard/configuration_keyboard.py b/src/lerobot/teleoperators/keyboard/configuration_keyboard.py index 5d5ef364f..6e070dedd 100644 --- a/src/lerobot/teleoperators/keyboard/configuration_keyboard.py +++ b/src/lerobot/teleoperators/keyboard/configuration_keyboard.py @@ -22,8 +22,9 @@ from ..config import TeleoperatorConfig @TeleoperatorConfig.register_subclass("keyboard") @dataclass class KeyboardTeleopConfig(TeleoperatorConfig): + """KeyboardTeleopConfig""" + # TODO(Steven): Consider setting in here the keys that we want to capture/listen - mock: bool = False @TeleoperatorConfig.register_subclass("keyboard_ee") diff --git a/src/lerobot/teleoperators/stretch3_gamepad/configuration_stretch3.py b/src/lerobot/teleoperators/stretch3_gamepad/configuration_stretch3.py index 507a21589..3af0b5be1 100644 --- a/src/lerobot/teleoperators/stretch3_gamepad/configuration_stretch3.py +++ b/src/lerobot/teleoperators/stretch3_gamepad/configuration_stretch3.py @@ -22,4 +22,4 @@ from ..config import TeleoperatorConfig @TeleoperatorConfig.register_subclass("stretch3") @dataclass class Stretch3GamePadConfig(TeleoperatorConfig): - mock: bool = False + """Stretch3GamePadConfig""" diff --git a/src/lerobot/teleoperators/stretch3_gamepad/stretch3_gamepad.py b/src/lerobot/teleoperators/stretch3_gamepad/stretch3_gamepad.py index 94e1ca7cc..09fdfadd7 100644 --- a/src/lerobot/teleoperators/stretch3_gamepad/stretch3_gamepad.py +++ b/src/lerobot/teleoperators/stretch3_gamepad/stretch3_gamepad.py @@ -112,10 +112,6 @@ class Stretch3GamePad(Teleoperator): def send_feedback(self, feedback: np.ndarray) -> None: pass - def print_logs(self) -> None: - pass - # TODO(aliberts): move robot-specific logs logic here - def disconnect(self) -> None: self.api.stop() self.is_connected = False diff --git a/src/lerobot/utils/constants.py b/src/lerobot/utils/constants.py index 337817908..824f74b30 100644 --- a/src/lerobot/utils/constants.py +++ b/src/lerobot/utils/constants.py @@ -33,7 +33,6 @@ TRUNCATED = "next.truncated" DONE = "next.done" ROBOTS = "robots" -ROBOT_TYPE = "robot_type" TELEOPERATORS = "teleoperators" # files & directories diff --git a/src/lerobot/utils/errors.py b/src/lerobot/utils/errors.py index c02d568d4..31b73eaca 100644 --- a/src/lerobot/utils/errors.py +++ b/src/lerobot/utils/errors.py @@ -30,14 +30,3 @@ class DeviceAlreadyConnectedError(ConnectionError): ): self.message = message super().__init__(self.message) - - -class InvalidActionError(ValueError): - """Exception raised when an action is already invalid.""" - - def __init__( - self, - message="The action is invalid. Check the value follows what it is expected from the action space.", - ): - self.message = message - super().__init__(self.message) diff --git a/src/lerobot/utils/utils.py b/src/lerobot/utils/utils.py index 523a5e4d2..8777d5a9d 100644 --- a/src/lerobot/utils/utils.py +++ b/src/lerobot/utils/utils.py @@ -330,10 +330,6 @@ class TimerManager: def history(self) -> list[float]: return deepcopy(self._history) - @property - def fps_history(self) -> list[float]: - return [1.0 / t for t in self._history] - @property def fps_last(self) -> float: return 0.0 if self.last == 0 else 1.0 / self.last diff --git a/tests/policies/test_sac_config.py b/tests/policies/test_sac_config.py index be6a8d26e..724c331ff 100644 --- a/tests/policies/test_sac_config.py +++ b/tests/policies/test_sac_config.py @@ -69,7 +69,6 @@ def test_sac_config_default_initialization(): # Training parameters assert config.online_steps == 1000000 - assert config.online_env_seed == 10000 assert config.online_buffer_capacity == 100000 assert config.offline_buffer_capacity == 100000 assert config.async_prefetch is False From 90684a9690c1a16e71dc64c7ee89847e5b82df1b Mon Sep 17 00:00:00 2001 From: Qizhi Chen Date: Mon, 29 Sep 2025 17:18:54 +0800 Subject: [PATCH 9/9] Improve V3 aggregate implementation (#2077) * fix return type * improve apply with vertorize op * Update src/lerobot/datasets/aggregate.py Co-authored-by: Michel Aractingi --- src/lerobot/datasets/aggregate.py | 45 +++++++++++-------------- src/lerobot/datasets/lerobot_dataset.py | 6 ++-- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/lerobot/datasets/aggregate.py b/src/lerobot/datasets/aggregate.py index 43d4ee233..803645f29 100644 --- a/src/lerobot/datasets/aggregate.py +++ b/src/lerobot/datasets/aggregate.py @@ -93,14 +93,13 @@ def update_data_df(df, src_meta, dst_meta): pd.DataFrame: Updated DataFrame with adjusted indices. """ - def _update(row): - row["episode_index"] = row["episode_index"] + dst_meta.info["total_episodes"] - row["index"] = row["index"] + dst_meta.info["total_frames"] - task = src_meta.tasks.iloc[row["task_index"]].name - row["task_index"] = dst_meta.tasks.loc[task].task_index.item() - return row + df["episode_index"] = df["episode_index"] + dst_meta.info["total_episodes"] + df["index"] = df["index"] + dst_meta.info["total_frames"] - return df.apply(_update, axis=1) + src_task_names = src_meta.tasks.index.take(df["task_index"].to_numpy()) + df["task_index"] = dst_meta.tasks.loc[src_task_names, "task_index"].to_numpy() + + return df def update_meta_data( @@ -126,27 +125,21 @@ def update_meta_data( pd.DataFrame: Updated DataFrame with adjusted indices and timestamps. """ - def _update(row): - row["meta/episodes/chunk_index"] = row["meta/episodes/chunk_index"] + meta_idx["chunk"] - row["meta/episodes/file_index"] = row["meta/episodes/file_index"] + meta_idx["file"] - row["data/chunk_index"] = row["data/chunk_index"] + data_idx["chunk"] - row["data/file_index"] = row["data/file_index"] + data_idx["file"] - for key, video_idx in videos_idx.items(): - row[f"videos/{key}/chunk_index"] = row[f"videos/{key}/chunk_index"] + video_idx["chunk"] - row[f"videos/{key}/file_index"] = row[f"videos/{key}/file_index"] + video_idx["file"] - row[f"videos/{key}/from_timestamp"] = ( - row[f"videos/{key}/from_timestamp"] + video_idx["latest_duration"] - ) - row[f"videos/{key}/to_timestamp"] = ( - row[f"videos/{key}/to_timestamp"] + video_idx["latest_duration"] - ) + df["meta/episodes/chunk_index"] = df["meta/episodes/chunk_index"] + meta_idx["chunk"] + df["meta/episodes/file_index"] = df["meta/episodes/file_index"] + meta_idx["file"] + df["data/chunk_index"] = df["data/chunk_index"] + data_idx["chunk"] + df["data/file_index"] = df["data/file_index"] + data_idx["file"] + for key, video_idx in videos_idx.items(): + df[f"videos/{key}/chunk_index"] = df[f"videos/{key}/chunk_index"] + video_idx["chunk"] + df[f"videos/{key}/file_index"] = df[f"videos/{key}/file_index"] + video_idx["file"] + df[f"videos/{key}/from_timestamp"] = df[f"videos/{key}/from_timestamp"] + video_idx["latest_duration"] + df[f"videos/{key}/to_timestamp"] = df[f"videos/{key}/to_timestamp"] + video_idx["latest_duration"] - row["dataset_from_index"] = row["dataset_from_index"] + dst_meta.info["total_frames"] - row["dataset_to_index"] = row["dataset_to_index"] + dst_meta.info["total_frames"] - row["episode_index"] = row["episode_index"] + dst_meta.info["total_episodes"] - return row + df["dataset_from_index"] = df["dataset_from_index"] + dst_meta.info["total_frames"] + df["dataset_to_index"] = df["dataset_to_index"] + dst_meta.info["total_frames"] + df["episode_index"] = df["episode_index"] + dst_meta.info["total_episodes"] - return df.apply(_update, axis=1) + return df def aggregate_datasets( diff --git a/src/lerobot/datasets/lerobot_dataset.py b/src/lerobot/datasets/lerobot_dataset.py index b8aa880da..691d86af7 100644 --- a/src/lerobot/datasets/lerobot_dataset.py +++ b/src/lerobot/datasets/lerobot_dataset.py @@ -1027,7 +1027,7 @@ class LeRobotDataset(torch.utils.data.Dataset): # Reset episode buffer and clean up temporary images (if not already deleted during video encoding) self.clear_episode_buffer(delete_images=len(self.meta.image_keys) > 0) - def _batch_save_episode_video(self, start_episode: int, end_episode: int | None = None): + def _batch_save_episode_video(self, start_episode: int, end_episode: int | None = None) -> None: """ Batch save videos for multiple episodes. @@ -1153,7 +1153,7 @@ class LeRobotDataset(torch.utils.data.Dataset): } return metadata - def _save_episode_video(self, video_key: str, episode_index: int): + def _save_episode_video(self, video_key: str, episode_index: int) -> dict: # Encode episode frames into a temporary video ep_path = self._encode_temporary_episode_video(video_key, episode_index) ep_size_in_mb = get_video_size_in_mb(ep_path) @@ -1258,7 +1258,7 @@ class LeRobotDataset(torch.utils.data.Dataset): if self.image_writer is not None: self.image_writer.wait_until_done() - def _encode_temporary_episode_video(self, video_key: str, episode_index: int) -> dict: + def _encode_temporary_episode_video(self, video_key: str, episode_index: int) -> Path: """ Use ffmpeg to convert frames stored as png into mp4 videos. Note: `encode_video_frames` is a blocking call. Making it asynchronous shouldn't speedup encoding,