diff --git a/README.md b/README.md index 35b28da87..57fec2e5f 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,11 @@ lerobot-train \ --dataset.repo_id=lerobot/aloha_mobile_cabinet ``` -| Category | Models | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Imitation Learning** | [ACT](./docs/source/policy_act_README.md), [Diffusion](./docs/source/policy_diffusion_README.md), [VQ-BeT](./docs/source/policy_vqbet_README.md) | -| **Reinforcement Learning** | [HIL-SERL](./docs/source/hilserl.mdx), [TDMPC](./docs/source/policy_tdmpc_README.md) & QC-FQL (coming soon) | -| **VLAs Models** | [Pi0.5](./docs/source/pi05.mdx), [GR00T N1.5](./docs/source/policy_groot_README.md), [SmolVLA](./docs/source/policy_smolvla_README.md), [XVLA](./docs/source/xvla.mdx) | +| Category | Models | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Imitation Learning** | [ACT](./docs/source/policy_act_README.md), [Diffusion](./docs/source/policy_diffusion_README.md), [VQ-BeT](./docs/source/policy_vqbet_README.md) | +| **Reinforcement Learning** | [HIL-SERL](./docs/source/hilserl.mdx), [TDMPC](./docs/source/policy_tdmpc_README.md) & QC-FQL (coming soon) | +| **VLAs Models** | [Pi0Fast](./docs/source/pi0fast.mdx), [Pi0.5](./docs/source/pi05.mdx), [GR00T N1.5](./docs/source/policy_groot_README.md), [SmolVLA](./docs/source/policy_smolvla_README.md), [XVLA](./docs/source/xvla.mdx) | Similarly to the hardware, you can easily implement your own policy & leverage LeRobot's data collection, training, and visualization tools, and share your model to the HF Hub diff --git a/docs/source/envhub_leisaac.mdx b/docs/source/envhub_leisaac.mdx index 1fc74c0fa..2537700a5 100644 --- a/docs/source/envhub_leisaac.mdx +++ b/docs/source/envhub_leisaac.mdx @@ -138,6 +138,7 @@ from lerobot.teleoperators import ( # noqa: F401 TeleoperatorConfig, make_teleoperator_from_config, so_leader, + bi_so_leader, ) from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.utils import init_logging diff --git a/docs/source/groot.mdx b/docs/source/groot.mdx index 729a64656..8bfc22996 100644 --- a/docs/source/groot.mdx +++ b/docs/source/groot.mdx @@ -12,6 +12,12 @@ Developers and researchers can post-train GR00T N1.5 with their own real or synt GR00T N1.5 (specifically the GR00T-N1.5-3B model) is built using pre-trained vision and language encoders. It utilizes a flow matching action transformer to model a chunk of actions, conditioned on vision, language, and proprioception. +An overview of GR00T + Its strong performance comes from being trained on an expansive and diverse humanoid dataset, which includes: - Real captured data from robots. @@ -103,7 +109,7 @@ Once you have trained your model using your parameters you can run inference in ```bash lerobot-record \ - --robot.type=bi_so100_follower \ + --robot.type=bi_so_follower \ --robot.left_arm_port=/dev/ttyACM1 \ --robot.right_arm_port=/dev/ttyACM0 \ --robot.id=bimanual_follower \ diff --git a/docs/source/pi0.mdx b/docs/source/pi0.mdx index 89604b6aa..93e0b4c88 100644 --- a/docs/source/pi0.mdx +++ b/docs/source/pi0.mdx @@ -6,6 +6,12 @@ π₀ represents a breakthrough in robotics as the first general-purpose robot foundation model developed by [Physical Intelligence](https://www.physicalintelligence.company/blog/pi0). Unlike traditional robot programs that are narrow specialists programmed for repetitive motions, π₀ is designed to be a generalist policy that can understand visual inputs, interpret natural language instructions, and control a variety of different robots across diverse tasks. +An overview of Pi0 + ### The Vision for Physical Intelligence As described by Physical Intelligence, while AI has achieved remarkable success in digital domains, from chess-playing to drug discovery, human intelligence still dramatically outpaces AI in the physical world. To paraphrase Moravec's paradox, winning a game of chess represents an "easy" problem for AI, but folding a shirt or cleaning up a table requires solving some of the most difficult engineering problems ever conceived. π₀ represents a first step toward developing artificial physical intelligence that enables users to simply ask robots to perform any task they want, just like they can with large language models. diff --git a/docs/source/pi0fast.mdx b/docs/source/pi0fast.mdx index e64355765..c4230fa79 100644 --- a/docs/source/pi0fast.mdx +++ b/docs/source/pi0fast.mdx @@ -6,6 +6,12 @@ π₀-FAST combines the power of Vision-Language Models with a novel action tokenization approach called **FAST (Frequency-space Action Sequence Tokenization)**. This enables training autoregressive VLAs on highly dexterous tasks that are impossible with standard binning-based discretization, while training **up to 5x faster** than diffusion-based approaches like π₀. +An overview of Pi0-FAST + ### Why FAST? Standard approaches for robot action tokenization use simple per-dimension, per-timestep binning schemes. While passable for simple behaviors, this rapidly breaks down for complex and dexterous skills that require precision and high-frequency control. @@ -53,7 +59,7 @@ You have two options for the FAST tokenizer: ### Training Your Own Tokenizer ```bash -python src/lerobot/policies/pi0_fast/train_fast_tokenizer.py \ +lerobot-train-tokenizer \ --repo_id "user/my-lerobot-dataset" \ --action_horizon 10 \ --encoded_dims "0:6" \ @@ -90,7 +96,7 @@ policy.type=pi0_fast For training π₀-FAST, you can use the LeRobot training script: ```bash -python src/lerobot/scripts/lerobot_train.py \ +lerobot-train \ --dataset.repo_id=your_dataset \ --policy.type=pi0_fast \ --output_dir=./outputs/pi0fast_training \ @@ -171,6 +177,64 @@ The model takes images, text instructions, and robot state as input, and outputs | Inference Method | Iterative Denoising | Autoregressive Decoding | | KV-Caching | N/A | Supported | +## Reproducing π₀Fast results + +We reproduce the results of π₀Fast on the LIBERO benchmark using the LeRobot implementation. We take the LeRobot PiFast base model [lerobot/pi0fast-base](https://huggingface.co/lerobot/pi0fast-base) and finetune for an additional 40kk steps in bfloat16, with batch size of 256 on 8 H100 GPUs using the [HuggingFace LIBERO dataset](https://huggingface.co/datasets/HuggingFaceVLA/libero). + +The finetuned model can be found here: + +- **π₀Fast LIBERO**: [lerobot/pi0fast-libero](https://huggingface.co/lerobot/pi0fast-libero) + +With the following training command: + +```bash +lerobot-train \ + --dataset.repo_id=lerobot/libero \ + --output_dir=outputs/libero_pi0fast \ + --job_name=libero_pi0fast \ + --policy.path=lerobot/pi0fast_base \ + --policy.dtype=bfloat16 \ + --steps=100000 \ + --save_freq=20000 \ + --batch_size=4 \ + --policy.device=cuda \ + --policy.scheduler_warmup_steps=4000 \ + --policy.scheduler_decay_steps=100000 \ + --policy.scheduler_decay_lr=1e-5 \ + --policy.gradient_checkpointing=true \ + --policy.chunk_size=10 \ + --policy.n_action_steps=10 \ + --policy.max_action_tokens=256 \ + --policy.empty_cameras=1 \ +``` + +We then evaluate the finetuned model using the LeRobot LIBERO implementation, by running the following command: + +```bash +tasks="libero_object,libero_spatial,libero_goal,libero_10" +lerobot-eval \ + --policy.path=lerobot/pi0fast-libero \ + --policy.max_action_tokens=256 \ + --env.type=libero \ + --policy.gradient_checkpointing=false \ + --env.task=${tasks} \ + --eval.batch_size=1 \ + --eval.n_episodes=1 \ + --rename_map='{"observation.images.image":"observation.images.base_0_rgb","observation.images.image2":"observation.images.left_wrist_0_rgb"}' +``` + +**Note:** We set `n_action_steps=10`, similar to the original OpenPI implementation. + +### Results + +We obtain the following results on the LIBERO benchmark: + +| Model | LIBERO Spatial | LIBERO Object | LIBERO Goal | LIBERO 10 | Average | +| ----------- | -------------- | ------------- | ----------- | --------- | -------- | +| **π₀-fast** | 70.0 | 100.0 | 100.0 | 60.0 | **82.5** | + +The full evaluation output folder, including videos, is available [here](https://drive.google.com/drive/folders/1HXpwPTRm4hx6g1sF2P7OOqGG0TwPU7LQ?usp=sharing) + ## License This model follows the **Apache 2.0 License**, consistent with the original [OpenPI repository](https://github.com/Physical-Intelligence/openpi). diff --git a/docs/source/sarm.mdx b/docs/source/sarm.mdx index 321097692..65e49792b 100644 --- a/docs/source/sarm.mdx +++ b/docs/source/sarm.mdx @@ -4,6 +4,12 @@ SARM (Stage-Aware Reward Modeling) is a video-based reward modeling framework fo **Paper**: [SARM: Stage-Aware Reward Modeling for Long Horizon Robot Manipulation](https://arxiv.org/abs/2509.25358) +An overview of SARM + ## Why Reward Models? Standard behavior cloning treats all demonstration frames equally, but real-world robot datasets are messy. They contain hesitations, corrections, and variable-quality trajectories. Reward models solve this by learning a generalizable notion of **task progress** from demonstrations: given video frames and a task description, they predict how close the robot is to completing the task (0→1). This learned "progress signal" can be used in multiple ways, two promising applications are: (1) **weighted imitation learning** (RA-BC), where high-progress frames receive more weight during policy training, and (2) **reinforcement learning**, where the reward model provides dense rewards for online or offline policy improvement. diff --git a/docs/source/walloss.mdx b/docs/source/walloss.mdx index 12e9b1fc7..c0756c087 100644 --- a/docs/source/walloss.mdx +++ b/docs/source/walloss.mdx @@ -8,6 +8,12 @@ X Square Robot’s WALL-OSS is now integrated into Hugging Face’s LeRobot ecos The WALL-OSS team is building the embodied foundation model to capture and compress the world's most valuable data: the continuous, high-fidelity stream of physical interaction. By creating a direct feedback loop between the model's decisions and the body's lived experience, the emergence of a truly generalizable intelligence is enabled—one that understands not just how the world works, but how to act effectively within it. +An overview of WALL-OSS + Technically, WALL-OSS introduces a tightly coupled multimodal architecture (tightly-coupled MoE structure) that integrates both discrete and continuous action modeling strategies. Through a two-stage training pipeline (Inspiration → Integration), the model gradually unifies semantic reasoning and high-frequency action generation. Its core innovations include: - **Embodied perception–enhanced multimodal pretraining**: Large-scale training on unified vision–language–action data to strengthen spatial, causal, and manipulation understanding. diff --git a/examples/rtc/eval_with_real_robot.py b/examples/rtc/eval_with_real_robot.py index 991d2468d..1470899d9 100644 --- a/examples/rtc/eval_with_real_robot.py +++ b/examples/rtc/eval_with_real_robot.py @@ -94,6 +94,7 @@ from lerobot.rl.process import ProcessSignalHandler from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, + bi_so_follower, koch_follower, so_follower, ) diff --git a/examples/so100_to_so100_EE/record.py b/examples/so100_to_so100_EE/record.py index db24f4b93..eead7a9a8 100644 --- a/examples/so100_to_so100_EE/record.py +++ b/examples/so100_to_so100_EE/record.py @@ -34,8 +34,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import ( InverseKinematicsEEToJoints, ) from lerobot.scripts.lerobot_record import record_loop -from lerobot.teleoperators.so_leader import SO100LeaderConfig -from lerobot.teleoperators.so_leader.so100_leader import SO100Leader +from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig from lerobot.utils.control_utils import init_keyboard_listener from lerobot.utils.utils import log_say from lerobot.utils.visualization_utils import init_rerun diff --git a/examples/so100_to_so100_EE/teleoperate.py b/examples/so100_to_so100_EE/teleoperate.py index d520a6eaf..71d2899de 100644 --- a/examples/so100_to_so100_EE/teleoperate.py +++ b/examples/so100_to_so100_EE/teleoperate.py @@ -29,8 +29,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import ( ForwardKinematicsJointsToEE, InverseKinematicsEEToJoints, ) -from lerobot.teleoperators.so_leader import SO100LeaderConfig -from lerobot.teleoperators.so_leader.so100_leader import SO100Leader +from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.visualization_utils import init_rerun, log_rerun_data diff --git a/pyproject.toml b/pyproject.toml index 4058e5ec3..12637e302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,6 +198,7 @@ lerobot-setup-motors="lerobot.scripts.lerobot_setup_motors:main" lerobot-teleoperate="lerobot.scripts.lerobot_teleoperate:main" lerobot-eval="lerobot.scripts.lerobot_eval:main" lerobot-train="lerobot.scripts.lerobot_train:main" +lerobot-train-tokenizer="lerobot.scripts.lerobot_train_tokenizer:main" lerobot-dataset-viz="lerobot.scripts.lerobot_dataset_viz:main" lerobot-info="lerobot.scripts.lerobot_info:main" lerobot-find-joint-limits="lerobot.scripts.lerobot_find_joint_limits:main" diff --git a/src/lerobot/async_inference/constants.py b/src/lerobot/async_inference/constants.py index f8b6d7bb3..081db0504 100644 --- a/src/lerobot/async_inference/constants.py +++ b/src/lerobot/async_inference/constants.py @@ -26,4 +26,4 @@ DEFAULT_OBS_QUEUE_TIMEOUT = 2 SUPPORTED_POLICIES = ["act", "smolvla", "diffusion", "tdmpc", "vqbet", "pi0", "pi05"] # TODO: Add all other robots -SUPPORTED_ROBOTS = ["so100_follower", "so101_follower", "bi_so100_follower", "omx_follower"] +SUPPORTED_ROBOTS = ["so100_follower", "so101_follower", "bi_so_follower", "omx_follower"] diff --git a/src/lerobot/async_inference/robot_client.py b/src/lerobot/async_inference/robot_client.py index c3668d40b..eea5585b0 100644 --- a/src/lerobot/async_inference/robot_client.py +++ b/src/lerobot/async_inference/robot_client.py @@ -51,7 +51,7 @@ from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraCon from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so100_follower, + bi_so_follower, koch_follower, make_robot_from_config, omx_follower, diff --git a/src/lerobot/robots/bi_so100_follower/config_bi_so100_follower.py b/src/lerobot/robots/bi_so100_follower/config_bi_so100_follower.py deleted file mode 100644 index 5806d7415..000000000 --- a/src/lerobot/robots/bi_so100_follower/config_bi_so100_follower.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -from dataclasses import dataclass, field - -from lerobot.cameras import CameraConfig - -from ..config import RobotConfig - - -@RobotConfig.register_subclass("bi_so100_follower") -@dataclass -class BiSO100FollowerConfig(RobotConfig): - left_arm_port: str - right_arm_port: str - - # Optional - left_arm_disable_torque_on_disconnect: bool = True - left_arm_max_relative_target: float | dict[str, float] | None = None - left_arm_use_degrees: bool = False - right_arm_disable_torque_on_disconnect: bool = True - right_arm_max_relative_target: float | dict[str, float] | None = None - right_arm_use_degrees: bool = False - - # cameras (shared between both arms) - cameras: dict[str, CameraConfig] = field(default_factory=dict) diff --git a/src/lerobot/teleoperators/bi_so100_leader/__init__.py b/src/lerobot/robots/bi_so_follower/__init__.py similarity index 77% rename from src/lerobot/teleoperators/bi_so100_leader/__init__.py rename to src/lerobot/robots/bi_so_follower/__init__.py index 34313a61e..f631a14db 100644 --- a/src/lerobot/teleoperators/bi_so100_leader/__init__.py +++ b/src/lerobot/robots/bi_so_follower/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,5 +14,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .bi_so100_leader import BiSO100Leader -from .config_bi_so100_leader import BiSO100LeaderConfig +from .bi_so_follower import BiSOFollower +from .config_bi_so_follower import BiSOFollowerConfig diff --git a/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py b/src/lerobot/robots/bi_so_follower/bi_so_follower.py similarity index 53% rename from src/lerobot/robots/bi_so100_follower/bi_so100_follower.py rename to src/lerobot/robots/bi_so_follower/bi_so_follower.py index 87a7edcc5..fa81e7d09 100644 --- a/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py +++ b/src/lerobot/robots/bi_so_follower/bi_so_follower.py @@ -15,66 +15,73 @@ # limitations under the License. import logging -import time from functools import cached_property from typing import Any -from lerobot.cameras.utils import make_cameras_from_configs -from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower import SOFollower, SOFollowerRobotConfig from ..robot import Robot -from .config_bi_so100_follower import BiSO100FollowerConfig +from .config_bi_so_follower import BiSOFollowerConfig logger = logging.getLogger(__name__) -class BiSO100Follower(Robot): +class BiSOFollower(Robot): """ - [Bimanual SO-100 Follower Arms](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - This bimanual robot can also be easily adapted to use SO-101 follower arms, just replace the SO100Follower class with SO101Follower and SO100FollowerConfig with SO101FollowerConfig. + [Bimanual SO Follower Arms](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio """ - config_class = BiSO100FollowerConfig - name = "bi_so100_follower" + config_class = BiSOFollowerConfig + name = "bi_so_follower" - def __init__(self, config: BiSO100FollowerConfig): + def __init__(self, config: BiSOFollowerConfig): super().__init__(config) self.config = config - left_arm_config = SO100FollowerConfig( + left_arm_config = SOFollowerRobotConfig( id=f"{config.id}_left" if config.id else None, calibration_dir=config.calibration_dir, - port=config.left_arm_port, - disable_torque_on_disconnect=config.left_arm_disable_torque_on_disconnect, - max_relative_target=config.left_arm_max_relative_target, - use_degrees=config.left_arm_use_degrees, - cameras={}, + port=config.left_arm_config.port, + disable_torque_on_disconnect=config.left_arm_config.disable_torque_on_disconnect, + max_relative_target=config.left_arm_config.max_relative_target, + use_degrees=config.left_arm_config.use_degrees, + cameras=config.left_arm_config.cameras, ) - right_arm_config = SO100FollowerConfig( + right_arm_config = SOFollowerRobotConfig( id=f"{config.id}_right" if config.id else None, calibration_dir=config.calibration_dir, - port=config.right_arm_port, - disable_torque_on_disconnect=config.right_arm_disable_torque_on_disconnect, - max_relative_target=config.right_arm_max_relative_target, - use_degrees=config.right_arm_use_degrees, - cameras={}, + port=config.right_arm_config.port, + disable_torque_on_disconnect=config.right_arm_config.disable_torque_on_disconnect, + max_relative_target=config.right_arm_config.max_relative_target, + use_degrees=config.right_arm_config.use_degrees, + cameras=config.right_arm_config.cameras, ) - self.left_arm = SO100Follower(left_arm_config) - self.right_arm = SO100Follower(right_arm_config) - self.cameras = make_cameras_from_configs(config.cameras) + self.left_arm = SOFollower(left_arm_config) + self.right_arm = SOFollower(right_arm_config) + + # Only for compatibility with other parts of the codebase that expect a `robot.cameras` attribute + self.cameras = {**self.left_arm.cameras, **self.right_arm.cameras} @property def _motors_ft(self) -> dict[str, type]: - return {f"left_{motor}.pos": float for motor in self.left_arm.bus.motors} | { - f"right_{motor}.pos": float for motor in self.right_arm.bus.motors + left_arm_motors_ft = self.left_arm._motors_ft + right_arm_motors_ft = self.right_arm._motors_ft + + return { + **{f"left_{k}": v for k, v in left_arm_motors_ft.items()}, + **{f"right_{k}": v for k, v in right_arm_motors_ft.items()}, } @property def _cameras_ft(self) -> dict[str, tuple]: + left_arm_cameras_ft = self.left_arm._cameras_ft + right_arm_cameras_ft = self.right_arm._cameras_ft + return { - cam: (self.config.cameras[cam].height, self.config.cameras[cam].width, 3) for cam in self.cameras + **{f"left_{k}": v for k, v in left_arm_cameras_ft.items()}, + **{f"right_{k}": v for k, v in right_arm_cameras_ft.items()}, } @cached_property @@ -87,19 +94,12 @@ class BiSO100Follower(Robot): @property def is_connected(self) -> bool: - return ( - self.left_arm.bus.is_connected - and self.right_arm.bus.is_connected - and all(cam.is_connected for cam in self.cameras.values()) - ) + return self.left_arm.is_connected and self.right_arm.is_connected def connect(self, calibrate: bool = True) -> None: self.left_arm.connect(calibrate) self.right_arm.connect(calibrate) - for cam in self.cameras.values(): - cam.connect() - @property def is_calibrated(self) -> bool: return self.left_arm.is_calibrated and self.right_arm.is_calibrated @@ -127,12 +127,6 @@ class BiSO100Follower(Robot): right_obs = self.right_arm.get_observation() obs_dict.update({f"right_{key}": value for key, value in right_obs.items()}) - for cam_key, cam in self.cameras.items(): - start = time.perf_counter() - obs_dict[cam_key] = cam.async_read() - dt_ms = (time.perf_counter() - start) * 1e3 - logger.debug(f"{self} read {cam_key}: {dt_ms:.1f}ms") - return obs_dict def send_action(self, action: dict[str, Any]) -> dict[str, Any]: @@ -145,18 +139,15 @@ class BiSO100Follower(Robot): key.removeprefix("right_"): value for key, value in action.items() if key.startswith("right_") } - send_action_left = self.left_arm.send_action(left_action) - send_action_right = self.right_arm.send_action(right_action) + sent_action_left = self.left_arm.send_action(left_action) + sent_action_right = self.right_arm.send_action(right_action) # Add prefixes back - prefixed_send_action_left = {f"left_{key}": value for key, value in send_action_left.items()} - prefixed_send_action_right = {f"right_{key}": value for key, value in send_action_right.items()} + prefixed_sent_action_left = {f"left_{key}": value for key, value in sent_action_left.items()} + prefixed_sent_action_right = {f"right_{key}": value for key, value in sent_action_right.items()} - return {**prefixed_send_action_left, **prefixed_send_action_right} + return {**prefixed_sent_action_left, **prefixed_sent_action_right} def disconnect(self): self.left_arm.disconnect() self.right_arm.disconnect() - - for cam in self.cameras.values(): - cam.disconnect() diff --git a/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py b/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py similarity index 68% rename from src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py rename to src/lerobot/robots/bi_so_follower/config_bi_so_follower.py index 1a6bb0df4..dca74fa2d 100644 --- a/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py +++ b/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py @@ -16,11 +16,15 @@ from dataclasses import dataclass -from ...config import TeleoperatorConfig -from ..so_leader_config_base import SOLeaderConfigBase +from lerobot.robots.so_follower import SOFollowerConfig + +from ..config import RobotConfig -@TeleoperatorConfig.register_subclass("so101_leader") +@RobotConfig.register_subclass("bi_so_follower") @dataclass -class SO101LeaderConfig(SOLeaderConfigBase): - pass +class BiSOFollowerConfig(RobotConfig): + """Configuration class for Bi SO Follower robots.""" + + left_arm_config: SOFollowerConfig + right_arm_config: SOFollowerConfig diff --git a/src/lerobot/robots/so_follower/__init__.py b/src/lerobot/robots/so_follower/__init__.py index 82755250c..eea2fcbdf 100644 --- a/src/lerobot/robots/so_follower/__init__.py +++ b/src/lerobot/robots/so_follower/__init__.py @@ -14,10 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from .so100_follower.config_so100_follower import SO100FollowerConfig -from .so100_follower.so100_follower import SO100Follower -from .so101_follower.config_so101_follower import SO101FollowerConfig -from .so101_follower.so101_follower import SO101Follower -from .so_follower_base import SOFollowerBase -from .so_follower_config_base import SOFollowerConfigBase +from .config_so_follower import ( + SO100FollowerConfig, + SO101FollowerConfig, + SOFollowerConfig, + SOFollowerRobotConfig, +) +from .so_follower import SO100Follower, SO101Follower, SOFollower diff --git a/src/lerobot/robots/so_follower/so_follower_config_base.py b/src/lerobot/robots/so_follower/config_so_follower.py similarity index 80% rename from src/lerobot/robots/so_follower/so_follower_config_base.py rename to src/lerobot/robots/so_follower/config_so_follower.py index 6e42df278..e9ce27123 100644 --- a/src/lerobot/robots/so_follower/so_follower_config_base.py +++ b/src/lerobot/robots/so_follower/config_so_follower.py @@ -15,6 +15,7 @@ # limitations under the License. from dataclasses import dataclass, field +from typing import TypeAlias from lerobot.cameras import CameraConfig @@ -22,7 +23,7 @@ from ..config import RobotConfig @dataclass -class SOFollowerConfigBase(RobotConfig): +class SOFollowerConfig: """Base configuration class for SO Follower robots.""" # Port to connect to the arm @@ -40,3 +41,14 @@ class SOFollowerConfigBase(RobotConfig): # Set to `True` for backward compatibility with previous policies/dataset use_degrees: bool = False + + +@RobotConfig.register_subclass("so101_follower") +@RobotConfig.register_subclass("so100_follower") +@dataclass +class SOFollowerRobotConfig(RobotConfig, SOFollowerConfig): + pass + + +SO100FollowerConfig: TypeAlias = SOFollowerRobotConfig +SO101FollowerConfig: TypeAlias = SOFollowerRobotConfig diff --git a/src/lerobot/robots/so_follower/so100.md b/src/lerobot/robots/so_follower/so100.md new file mode 120000 index 000000000..ad1154e75 --- /dev/null +++ b/src/lerobot/robots/so_follower/so100.md @@ -0,0 +1 @@ +../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py b/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py deleted file mode 100644 index 3e8dd7e9f..000000000 --- a/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -from dataclasses import dataclass - -from ...config import RobotConfig -from ..so_follower_config_base import SOFollowerConfigBase - - -@RobotConfig.register_subclass("so100_follower") -@dataclass -class SO100FollowerConfig(SOFollowerConfigBase): - pass diff --git a/src/lerobot/robots/so_follower/so100_follower/so100.md b/src/lerobot/robots/so_follower/so100_follower/so100.md deleted file mode 120000 index f06f88ff6..000000000 --- a/src/lerobot/robots/so_follower/so100_follower/so100.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so_follower/so100_follower/so100_follower.py b/src/lerobot/robots/so_follower/so100_follower/so100_follower.py deleted file mode 100644 index ef61f5ce3..000000000 --- a/src/lerobot/robots/so_follower/so100_follower/so100_follower.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -from ..so_follower_base import SOFollowerBase -from .config_so100_follower import SO100FollowerConfig - - -class SO100Follower(SOFollowerBase): - """ - SO-101 follower robot class. [SO-101 Follower Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO100FollowerConfig - name = "so100_follower" diff --git a/src/lerobot/robots/so_follower/so101.md b/src/lerobot/robots/so_follower/so101.md new file mode 120000 index 000000000..27b892660 --- /dev/null +++ b/src/lerobot/robots/so_follower/so101.md @@ -0,0 +1 @@ +../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py b/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py deleted file mode 100644 index 950e8c839..000000000 --- a/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -from dataclasses import dataclass - -from ...config import RobotConfig -from ..so_follower_config_base import SOFollowerConfigBase - - -@RobotConfig.register_subclass("so101_follower") -@dataclass -class SO101FollowerConfig(SOFollowerConfigBase): - pass diff --git a/src/lerobot/robots/so_follower/so101_follower/so101.md b/src/lerobot/robots/so_follower/so101_follower/so101.md deleted file mode 120000 index 38f4deca7..000000000 --- a/src/lerobot/robots/so_follower/so101_follower/so101.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so_follower/so101_follower/so101_follower.py b/src/lerobot/robots/so_follower/so101_follower/so101_follower.py deleted file mode 100644 index b4cfb2711..000000000 --- a/src/lerobot/robots/so_follower/so101_follower/so101_follower.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -from ..so_follower_base import SOFollowerBase -from .config_so101_follower import SO101FollowerConfig - - -class SO101Follower(SOFollowerBase): - """ - SO-101 follower robot class. [SO-101 Follower Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO101FollowerConfig - name = "so101_follower" diff --git a/src/lerobot/robots/so_follower/so_follower_base.py b/src/lerobot/robots/so_follower/so_follower.py similarity index 96% rename from src/lerobot/robots/so_follower/so_follower_base.py rename to src/lerobot/robots/so_follower/so_follower.py index 5e6e32888..5e99b33a1 100644 --- a/src/lerobot/robots/so_follower/so_follower_base.py +++ b/src/lerobot/robots/so_follower/so_follower.py @@ -17,7 +17,7 @@ import logging import time from functools import cached_property -from typing import Any +from typing import Any, TypeAlias from lerobot.cameras.utils import make_cameras_from_configs from lerobot.motors import Motor, MotorCalibration, MotorNormMode @@ -29,20 +29,21 @@ from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnected from ..robot import Robot from ..utils import ensure_safe_goal_position -from .so_follower_config_base import SOFollowerConfigBase +from .config_so_follower import SOFollowerRobotConfig logger = logging.getLogger(__name__) -class SOFollowerBase(Robot): +class SOFollower(Robot): """ Generic SO follower base implementing common functionality for SO-100/101/10X. Designed to be subclassed with a per-hardware-model `config_class` and `name`. """ - # `config_class` and `name` should be set by subclasses + config_class = SOFollowerRobotConfig + name = "so_follower" - def __init__(self, config: SOFollowerConfigBase): + def __init__(self, config: SOFollowerRobotConfig): super().__init__(config) self.config = config # choose normalization mode depending on config if available @@ -232,3 +233,7 @@ class SOFollowerBase(Robot): cam.disconnect() logger.info(f"{self} disconnected.") + + +SO100Follower: TypeAlias = SOFollower +SO101Follower: TypeAlias = SOFollower diff --git a/src/lerobot/robots/utils.py b/src/lerobot/robots/utils.py index ad6cc3da1..27abaaa86 100644 --- a/src/lerobot/robots/utils.py +++ b/src/lerobot/robots/utils.py @@ -52,10 +52,10 @@ def make_robot_from_config(config: RobotConfig) -> Robot: from .hope_jr import HopeJrArm return HopeJrArm(config) - elif config.type == "bi_so100_follower": - from .bi_so100_follower import BiSO100Follower + elif config.type == "bi_so_follower": + from .bi_so_follower import BiSOFollower - return BiSO100Follower(config) + return BiSOFollower(config) elif config.type == "reachy2": from .reachy2 import Reachy2Robot diff --git a/src/lerobot/scripts/lerobot_calibrate.py b/src/lerobot/scripts/lerobot_calibrate.py index 1468c6e93..cbc7684d3 100644 --- a/src/lerobot/scripts/lerobot_calibrate.py +++ b/src/lerobot/scripts/lerobot_calibrate.py @@ -36,6 +36,7 @@ from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraCon from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, + bi_so_follower, hope_jr, koch_follower, lekiwi, @@ -46,6 +47,7 @@ from lerobot.robots import ( # noqa: F401 from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, + bi_so_leader, homunculus, koch_leader, make_teleoperator_from_config, diff --git a/src/lerobot/scripts/lerobot_find_joint_limits.py b/src/lerobot/scripts/lerobot_find_joint_limits.py index b36cdbc90..20bbc8615 100644 --- a/src/lerobot/scripts/lerobot_find_joint_limits.py +++ b/src/lerobot/scripts/lerobot_find_joint_limits.py @@ -44,6 +44,7 @@ import numpy as np from lerobot.model.kinematics import RobotKinematics from lerobot.robots import ( # noqa: F401 RobotConfig, + bi_so_follower, koch_follower, make_robot_from_config, omx_follower, @@ -51,6 +52,7 @@ from lerobot.robots import ( # noqa: F401 ) from lerobot.teleoperators import ( # noqa: F401 TeleoperatorConfig, + bi_so_leader, gamepad, koch_leader, make_teleoperator_from_config, diff --git a/src/lerobot/scripts/lerobot_record.py b/src/lerobot/scripts/lerobot_record.py index a81a5d54e..8eafa8e6d 100644 --- a/src/lerobot/scripts/lerobot_record.py +++ b/src/lerobot/scripts/lerobot_record.py @@ -40,21 +40,23 @@ lerobot-record \ Example recording with bimanual so100: ```shell lerobot-record \ - --robot.type=bi_so100_follower \ - --robot.left_arm_port=/dev/tty.usbmodem5A460851411 \ - --robot.right_arm_port=/dev/tty.usbmodem5A460812391 \ + --robot.type=bi_so_follower \ + --robot.left_arm_config.port=/dev/tty.usbmodem5A460822851 \ + --robot.right_arm_config.port=/dev/tty.usbmodem5A460814411 \ --robot.id=bimanual_follower \ - --robot.cameras='{ - left: {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30}, - top: {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, - right: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30} + --robot.left_arm_config.cameras='{ + wrist: {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, + top: {"type": "opencv", "index_or_path": 3, "width": 640, "height": 480, "fps": 30}, + }' --robot.right_arm_config.cameras='{ + wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}, + front: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}, }' \ - --teleop.type=bi_so100_leader \ - --teleop.left_arm_port=/dev/tty.usbmodem5A460828611 \ - --teleop.right_arm_port=/dev/tty.usbmodem5A460826981 \ + --teleop.type=bi_so_leader \ + --teleop.left_arm_config.port=/dev/tty.usbmodem5A460852721 \ + --teleop.right_arm_config.port=/dev/tty.usbmodem5A460819811 \ --teleop.id=bimanual_leader \ --display_data=true \ - --dataset.repo_id=${HF_USER}/bimanual-so100-handover-cube \ + --dataset.repo_id=${HF_USER}/bimanual-so-handover-cube \ --dataset.num_episodes=25 \ --dataset.single_task="Grab and handover the red cube to the other arm" ``` @@ -94,7 +96,7 @@ from lerobot.processor.rename_processor import rename_stats from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so100_follower, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, @@ -105,7 +107,7 @@ from lerobot.robots import ( # noqa: F401 from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, - bi_so100_leader, + bi_so_leader, homunculus, koch_leader, make_teleoperator_from_config, diff --git a/src/lerobot/scripts/lerobot_replay.py b/src/lerobot/scripts/lerobot_replay.py index af7c63365..8e0d9cf6d 100644 --- a/src/lerobot/scripts/lerobot_replay.py +++ b/src/lerobot/scripts/lerobot_replay.py @@ -29,7 +29,7 @@ lerobot-replay \ Example replay with bimanual so100: ```shell lerobot-replay \ - --robot.type=bi_so100_follower \ + --robot.type=bi_so_follower \ --robot.left_arm_port=/dev/tty.usbmodem5A460851411 \ --robot.right_arm_port=/dev/tty.usbmodem5A460812391 \ --robot.id=bimanual_follower \ @@ -53,7 +53,7 @@ from lerobot.processor import ( from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so100_follower, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, diff --git a/src/lerobot/scripts/lerobot_setup_motors.py b/src/lerobot/scripts/lerobot_setup_motors.py index ea5c821d1..01af95b61 100644 --- a/src/lerobot/scripts/lerobot_setup_motors.py +++ b/src/lerobot/scripts/lerobot_setup_motors.py @@ -30,6 +30,7 @@ import draccus from lerobot.robots import ( # noqa: F401 RobotConfig, + bi_so_follower, koch_follower, lekiwi, make_robot_from_config, @@ -38,6 +39,7 @@ from lerobot.robots import ( # noqa: F401 ) from lerobot.teleoperators import ( # noqa: F401 TeleoperatorConfig, + bi_so_leader, koch_leader, make_teleoperator_from_config, omx_leader, diff --git a/src/lerobot/scripts/lerobot_teleoperate.py b/src/lerobot/scripts/lerobot_teleoperate.py index 2e0724574..f2392bb51 100644 --- a/src/lerobot/scripts/lerobot_teleoperate.py +++ b/src/lerobot/scripts/lerobot_teleoperate.py @@ -33,18 +33,18 @@ Example teleoperation with bimanual so100: ```shell lerobot-teleoperate \ - --robot.type=bi_so100_follower \ - --robot.left_arm_port=/dev/tty.usbmodem5A460851411 \ - --robot.right_arm_port=/dev/tty.usbmodem5A460812391 \ + --robot.type=bi_so_follower \ + --robot.left_arm_config.port=/dev/tty.usbmodem5A460822851 \ + --robot.right_arm_config.port=/dev/tty.usbmodem5A460814411 \ --robot.id=bimanual_follower \ - --robot.cameras='{ - left: {"type": "opencv", "index_or_path": 0, "width": 1920, "height": 1080, "fps": 30}, - top: {"type": "opencv", "index_or_path": 1, "width": 1920, "height": 1080, "fps": 30}, - right: {"type": "opencv", "index_or_path": 2, "width": 1920, "height": 1080, "fps": 30} + --robot.left_arm_config.cameras='{ + wrist: {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, + }' --robot.right_arm_config.cameras='{ + wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}, }' \ - --teleop.type=bi_so100_leader \ - --teleop.left_arm_port=/dev/tty.usbmodem5A460828611 \ - --teleop.right_arm_port=/dev/tty.usbmodem5A460826981 \ + --teleop.type=bi_so_leader \ + --teleop.left_arm_config.port=/dev/tty.usbmodem5A460852721 \ + --teleop.right_arm_config.port=/dev/tty.usbmodem5A460819811 \ --teleop.id=bimanual_leader \ --display_data=true ``` @@ -70,7 +70,7 @@ from lerobot.processor import ( from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so100_follower, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, @@ -81,7 +81,7 @@ from lerobot.robots import ( # noqa: F401 from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, - bi_so100_leader, + bi_so_leader, gamepad, homunculus, keyboard, diff --git a/src/lerobot/scripts/lerobot_train.py b/src/lerobot/scripts/lerobot_train.py index 286c69906..927e9659a 100644 --- a/src/lerobot/scripts/lerobot_train.py +++ b/src/lerobot/scripts/lerobot_train.py @@ -259,7 +259,14 @@ def train(cfg: TrainPipelineConfig, accelerator: Accelerator | None = None): from accelerate.utils import DistributedDataParallelKwargs ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True) - accelerator = Accelerator(step_scheduler_with_optimizer=False, kwargs_handlers=[ddp_kwargs]) + # Accelerate auto-detects the device based on the available hardware and ignores the policy.device setting. + # Force the device to be CPU when policy.device is set to CPU. + force_cpu = cfg.policy.device == "cpu" + accelerator = Accelerator( + step_scheduler_with_optimizer=False, + kwargs_handlers=[ddp_kwargs], + cpu=force_cpu, + ) init_logging(accelerator=accelerator) diff --git a/src/lerobot/policies/pi0_fast/train_fast_tokenizer.py b/src/lerobot/scripts/lerobot_train_tokenizer.py similarity index 94% rename from src/lerobot/policies/pi0_fast/train_fast_tokenizer.py rename to src/lerobot/scripts/lerobot_train_tokenizer.py index 6a3a1fe69..03bfcaaf8 100644 --- a/src/lerobot/policies/pi0_fast/train_fast_tokenizer.py +++ b/src/lerobot/scripts/lerobot_train_tokenizer.py @@ -1,3 +1,16 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. """Train FAST tokenizer for action encoding. This script: @@ -6,6 +19,26 @@ This script: 3. Trains FAST tokenizer on specified action dimensions 4. Saves tokenizer to assets directory 5. Reports compression statistics + +Example: + +```shell +lerobot-train-tokenizer \ + --repo_id=user/dataset_name \ + --action_horizon=10 \ + --max_episodes=100 \ + --sample_fraction=0.1 \ + --encoded_dims="0:6" \ + --delta_dims="0,1,2,3,4,5" \ + --use_delta_transform=true \ + --state_key="observation.state" \ + --normalization_mode="QUANTILES" \ + --vocab_size=1024 \ + --scale=10.0 \ + --output_dir="./fast_tokenizer_dataset_name" \ + --push_to_hub=true \ + --hub_repo_id="user/fast_tokenizer_dataset_name" \ + --hub_private=false """ import json diff --git a/src/lerobot/robots/bi_so100_follower/__init__.py b/src/lerobot/teleoperators/bi_so_leader/__init__.py similarity index 76% rename from src/lerobot/robots/bi_so100_follower/__init__.py rename to src/lerobot/teleoperators/bi_so_leader/__init__.py index 90f56516b..b902270f9 100644 --- a/src/lerobot/robots/bi_so100_follower/__init__.py +++ b/src/lerobot/teleoperators/bi_so_leader/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,5 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .bi_so100_follower import BiSO100Follower -from .config_bi_so100_follower import BiSO100FollowerConfig +from .bi_so_leader import BiSOLeader, BiSOLeaderConfig diff --git a/src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py b/src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py similarity index 62% rename from src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py rename to src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py index 93f66eb2e..45c46c100 100644 --- a/src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py +++ b/src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py @@ -17,46 +17,50 @@ import logging from functools import cached_property -from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig +from lerobot.teleoperators.so_leader import SOLeaderTeleopConfig +from ..so_leader import SOLeader from ..teleoperator import Teleoperator -from .config_bi_so100_leader import BiSO100LeaderConfig +from .config_bi_so_leader import BiSOLeaderConfig logger = logging.getLogger(__name__) -class BiSO100Leader(Teleoperator): +class BiSOLeader(Teleoperator): """ - [Bimanual SO-100 Leader Arms](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - This bimanual leader arm can also be easily adapted to use SO-101 leader arms, just replace the SO100Leader class with SO101Leader and SO100LeaderConfig with SO101LeaderConfig. + [Bimanual SO Leader Arms](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio """ - config_class = BiSO100LeaderConfig - name = "bi_so100_leader" + config_class = BiSOLeaderConfig + name = "bi_so_leader" - def __init__(self, config: BiSO100LeaderConfig): + def __init__(self, config: BiSOLeaderConfig): super().__init__(config) self.config = config - left_arm_config = SO100LeaderConfig( + left_arm_config = SOLeaderTeleopConfig( id=f"{config.id}_left" if config.id else None, calibration_dir=config.calibration_dir, - port=config.left_arm_port, + port=config.left_arm_config.port, ) - right_arm_config = SO100LeaderConfig( + right_arm_config = SOLeaderTeleopConfig( id=f"{config.id}_right" if config.id else None, calibration_dir=config.calibration_dir, - port=config.right_arm_port, + port=config.right_arm_config.port, ) - self.left_arm = SO100Leader(left_arm_config) - self.right_arm = SO100Leader(right_arm_config) + self.left_arm = SOLeader(left_arm_config) + self.right_arm = SOLeader(right_arm_config) @cached_property def action_features(self) -> dict[str, type]: - return {f"left_{motor}.pos": float for motor in self.left_arm.bus.motors} | { - f"right_{motor}.pos": float for motor in self.right_arm.bus.motors + left_arm_features = self.left_arm.action_features + right_arm_features = self.right_arm.action_features + + return { + **{f"left_{k}": v for k, v in left_arm_features.items()}, + **{f"right_{k}": v for k, v in right_arm_features.items()}, } @cached_property @@ -101,19 +105,8 @@ class BiSO100Leader(Teleoperator): return action_dict def send_feedback(self, feedback: dict[str, float]) -> None: - # Remove "left_" prefix - left_feedback = { - key.removeprefix("left_"): value for key, value in feedback.items() if key.startswith("left_") - } - # Remove "right_" prefix - right_feedback = { - key.removeprefix("right_"): value for key, value in feedback.items() if key.startswith("right_") - } - - if left_feedback: - self.left_arm.send_feedback(left_feedback) - if right_feedback: - self.right_arm.send_feedback(right_feedback) + # TODO: Implement force feedback + raise NotImplementedError def disconnect(self) -> None: self.left_arm.disconnect() diff --git a/src/lerobot/teleoperators/bi_so100_leader/config_bi_so100_leader.py b/src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py similarity index 71% rename from src/lerobot/teleoperators/bi_so100_leader/config_bi_so100_leader.py rename to src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py index 117e09913..c2f23c617 100644 --- a/src/lerobot/teleoperators/bi_so100_leader/config_bi_so100_leader.py +++ b/src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py @@ -16,11 +16,15 @@ from dataclasses import dataclass +from lerobot.teleoperators.so_leader import SOLeaderConfig + from ..config import TeleoperatorConfig -@TeleoperatorConfig.register_subclass("bi_so100_leader") +@TeleoperatorConfig.register_subclass("bi_so_leader") @dataclass -class BiSO100LeaderConfig(TeleoperatorConfig): - left_arm_port: str - right_arm_port: str +class BiSOLeaderConfig(TeleoperatorConfig): + """Configuration class for Bi SO Leader teleoperators.""" + + left_arm_config: SOLeaderConfig + right_arm_config: SOLeaderConfig diff --git a/src/lerobot/teleoperators/so_leader/__init__.py b/src/lerobot/teleoperators/so_leader/__init__.py index a1017f3b9..e5aaa31b6 100644 --- a/src/lerobot/teleoperators/so_leader/__init__.py +++ b/src/lerobot/teleoperators/so_leader/__init__.py @@ -14,9 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .so100_leader.config_so100_leader import SO100LeaderConfig -from .so100_leader.so100_leader import SO100Leader -from .so101_leader.config_so101_leader import SO101LeaderConfig -from .so101_leader.so101_leader import SO101Leader -from .so_leader_base import SOLeaderBase -from .so_leader_config_base import SOLeaderConfigBase +from .config_so_leader import ( + SO100LeaderConfig, + SO101LeaderConfig, + SOLeaderConfig, + SOLeaderTeleopConfig, +) +from .so_leader import SO100Leader, SO101Leader, SOLeader diff --git a/src/lerobot/teleoperators/so_leader/so_leader_config_base.py b/src/lerobot/teleoperators/so_leader/config_so_leader.py similarity index 72% rename from src/lerobot/teleoperators/so_leader/so_leader_config_base.py rename to src/lerobot/teleoperators/so_leader/config_so_leader.py index b2f2dfcfa..dd55196d7 100644 --- a/src/lerobot/teleoperators/so_leader/so_leader_config_base.py +++ b/src/lerobot/teleoperators/so_leader/config_so_leader.py @@ -15,12 +15,13 @@ # limitations under the License. from dataclasses import dataclass +from typing import TypeAlias from ..config import TeleoperatorConfig @dataclass -class SOLeaderConfigBase(TeleoperatorConfig): +class SOLeaderConfig: """Base configuration class for SO Leader teleoperators.""" # Port to connect to the arm @@ -28,3 +29,14 @@ class SOLeaderConfigBase(TeleoperatorConfig): # Whether to use degrees for angles use_degrees: bool = False + + +@TeleoperatorConfig.register_subclass("so101_leader") +@TeleoperatorConfig.register_subclass("so100_leader") +@dataclass +class SOLeaderTeleopConfig(TeleoperatorConfig, SOLeaderConfig): + pass + + +SO100LeaderConfig: TypeAlias = SOLeaderTeleopConfig +SO101LeaderConfig: TypeAlias = SOLeaderTeleopConfig diff --git a/src/lerobot/teleoperators/so_leader/so100.md b/src/lerobot/teleoperators/so_leader/so100.md new file mode 120000 index 000000000..ad1154e75 --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/so100.md @@ -0,0 +1 @@ +../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py b/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py deleted file mode 100644 index 092eb0cfc..000000000 --- a/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -from dataclasses import dataclass - -from ...config import TeleoperatorConfig -from ..so_leader_config_base import SOLeaderConfigBase - - -@TeleoperatorConfig.register_subclass("so100_leader") -@dataclass -class SO100LeaderConfig(SOLeaderConfigBase): - pass diff --git a/src/lerobot/teleoperators/so_leader/so100_leader/so100.md b/src/lerobot/teleoperators/so_leader/so100_leader/so100.md deleted file mode 120000 index f06f88ff6..000000000 --- a/src/lerobot/teleoperators/so_leader/so100_leader/so100.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py b/src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py deleted file mode 100644 index 530e4f723..000000000 --- a/src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py +++ /dev/null @@ -1,27 +0,0 @@ -# !/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -from ..so_leader_base import SOLeaderBase -from .config_so100_leader import SO100LeaderConfig - - -class SO100Leader(SOLeaderBase): - """ - SO-101 leader robot class. [SO-101 Leader Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO100LeaderConfig - name = "so100_leader" diff --git a/src/lerobot/teleoperators/so_leader/so101.md b/src/lerobot/teleoperators/so_leader/so101.md new file mode 120000 index 000000000..27b892660 --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/so101.md @@ -0,0 +1 @@ +../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/teleoperators/so_leader/so101_leader/so101.md b/src/lerobot/teleoperators/so_leader/so101_leader/so101.md deleted file mode 120000 index 38f4deca7..000000000 --- a/src/lerobot/teleoperators/so_leader/so101_leader/so101.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py b/src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py deleted file mode 100644 index 3aed1f6f9..000000000 --- a/src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py +++ /dev/null @@ -1,27 +0,0 @@ -# !/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -from ..so_leader_base import SOLeaderBase -from .config_so101_leader import SO101LeaderConfig - - -class SO101Leader(SOLeaderBase): - """ - SO-101 leader robot class. [SO-101 Leader Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO101LeaderConfig - name = "so101_leader" diff --git a/src/lerobot/teleoperators/so_leader/so_leader_base.py b/src/lerobot/teleoperators/so_leader/so_leader.py similarity index 95% rename from src/lerobot/teleoperators/so_leader/so_leader_base.py rename to src/lerobot/teleoperators/so_leader/so_leader.py index 6cdaca2e7..760ef2eb1 100644 --- a/src/lerobot/teleoperators/so_leader/so_leader_base.py +++ b/src/lerobot/teleoperators/so_leader/so_leader.py @@ -16,6 +16,7 @@ import logging import time +from typing import TypeAlias from lerobot.motors import Motor, MotorCalibration, MotorNormMode from lerobot.motors.feetech import ( @@ -25,15 +26,18 @@ from lerobot.motors.feetech import ( from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError from ..teleoperator import Teleoperator -from .so_leader_config_base import SOLeaderConfigBase +from .config_so_leader import SOLeaderTeleopConfig logger = logging.getLogger(__name__) -class SOLeaderBase(Teleoperator): +class SOLeader(Teleoperator): """Generic SO leader base for SO-100/101/10X teleoperators.""" - def __init__(self, config: SOLeaderConfigBase): + config_class = SOLeaderTeleopConfig + name = "so_leader" + + def __init__(self, config: SOLeaderTeleopConfig): super().__init__(config) self.config = config norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100 @@ -153,3 +157,7 @@ class SOLeaderBase(Teleoperator): self.bus.disconnect() logger.info(f"{self} disconnected.") + + +SO100Leader: TypeAlias = SOLeader +SO101Leader: TypeAlias = SOLeader diff --git a/src/lerobot/teleoperators/utils.py b/src/lerobot/teleoperators/utils.py index 74e43ec95..eec2f119c 100644 --- a/src/lerobot/teleoperators/utils.py +++ b/src/lerobot/teleoperators/utils.py @@ -73,10 +73,10 @@ def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator: from .homunculus import HomunculusArm return HomunculusArm(config) - elif config.type == "bi_so100_leader": - from .bi_so100_leader import BiSO100Leader + elif config.type == "bi_so_leader": + from .bi_so_leader import BiSOLeader - return BiSO100Leader(config) + return BiSOLeader(config) elif config.type == "reachy2_teleoperator": from .reachy2_teleoperator import Reachy2Teleoperator diff --git a/tests/robots/test_so100_follower.py b/tests/robots/test_so100_follower.py index fc300b820..b61d0ca01 100644 --- a/tests/robots/test_so100_follower.py +++ b/tests/robots/test_so100_follower.py @@ -66,7 +66,7 @@ def follower(): with ( patch( - "lerobot.robots.so_follower.so_follower_base.FeetechMotorsBus", + "lerobot.robots.so_follower.so_follower.FeetechMotorsBus", side_effect=_bus_side_effect, ), patch.object(SO100Follower, "configure", lambda self: None),