diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index df2e2db295..22f1ee3d7f 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,33 +1,40 @@
## What this does
+
Explain what this PR does. Feel free to tag your PR with the appropriate label(s).
Examples:
-| Title | Label |
+| Title | Label |
|----------------------|-----------------|
-| Fixes #[issue] | (🐛 Bug) |
-| Adds new dataset | (🗃️ Dataset) |
-| Optimizes something | (⚡️ Performance) |
+| Fixes #[issue] | (🐛 Bug) |
+| Adds new dataset | (🗃️ Dataset) |
+| Optimizes something | (⚡️ Performance) |
## How it was tested
+
Explain/show how you tested your changes.
Examples:
+
- Added `test_something` in `tests/test_stuff.py`.
- Added `new_feature` and checked that training converges with policy X on dataset/environment Y.
- Optimized `some_function`, it now runs X times faster than previously.
## How to checkout & try? (for the reviewer)
+
Provide a simple way for the reviewer to try out your changes.
Examples:
+
```bash
pytest -sx tests/test_stuff.py::test_something
```
+
```bash
python -m lerobot.scripts.train --some.option=true
```
## SECTION TO REMOVE BEFORE SUBMITTING YOUR PR
+
**Note**: Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR. Try to avoid tagging more than 3 people.
diff --git a/.gitignore b/.gitignore
index 4ab886933e..c4d1f769f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,164 +12,164 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# Dev scripts
-.dev
-
-# Logging
-logs
-tmp
-wandb
-
-# Data
-data
-outputs
-
-# Apple
-.DS_Store
-
-# VS Code
-.vscode
-.devcontainer
-
-# HPC
-nautilus/*.yaml
-*.key
-
-# Slurm
-sbatch*.sh
-
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
+### Environments & Dependencies ###
+.env
+.venv
+env/
+venv/
+env.bak/
+venv.bak/
+.python-version
+__pypackages__/
+node_modules/
-# C extensions
-*.so
+# Lock files
+poetry.lock
+uv.lock
+Pipfile.lock
-# Distribution / packaging
-.Python
+### Build & Distribution ###
build/
-develop-eggs/
dist/
+sdist/
+wheels/
downloads/
eggs/
.eggs/
-lib/
-lib64/
parts/
-sdist/
var/
-wheels/
pip-wheel-metadata/
share/python-wheels/
+develop-eggs/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
-
-# uv/poetry lock files
-poetry.lock
-uv.lock
+lib/
+lib64/
# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
+### Compiled & Cached Files ###
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+*.sage.py
+.cache/
+.ruff_cache/
+.mypy_cache/
+.pyre/
+.pytype/
+cython_debug/
-# Unit test / coverage reports
-!tests/artifacts
+### Testing & Coverage ###
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
+.pytest_cache/
+.hypothesis/
nosetests.xml
coverage.xml
*.cover
*.py,cover
-.hypothesis/
-.pytest_cache/
+!tests/artifacts
-# Ignore .cache
-.cache/*
+### Logs & Temporary Files ###
+logs/
+tmp/
+*.log
+pip-log.txt
+pip-delete-this-directory.txt
+celerybeat-schedule
+celerybeat.pid
-# Translations
-*.mo
-*.pot
+### IDE & Editor Config ###
+# VS Code
+.vscode/
+.devcontainer/
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
+# JetBrains / PyCharm
+.idea/
-# Flask stuff:
-instance/
-.webassets-cache
+# Spyder
+.spyderproject
+.spyproject
-# Scrapy stuff:
-.scrapy
+# Rope
+.ropeproject
-# Sphinx documentation
-docs/_build/
+# Vim
+*.swp
-# PyBuilder
-.pybuilder/
-target/
+# Other
+*~
-# Jupyter Notebook
-.ipynb_checkpoints
+### OS Specific ###
+# macOS
+.DS_Store
-# IPython
-profile_default/
-ipython_config.py
+# Windows
+Thumbs.db
-# pyenv
-.python-version
+### Framework & Tool Specific ###
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
+.Python
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
+# Django
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
-# SageMath parsed files
-*.sage.py
+# Flask
+instance/
+.webassets-cache
-# Environments
-.env
-.venv
-env/
-venv/
-env.bak/
-venv.bak/
+# Scrapy
+.scrapy
-# Spyder project settings
-.spyderproject
-.spyproject
+# Jupyter
+.ipynb_checkpoints/
+profile_default/
+ipython_config.py
-# Rope project settings
-.ropeproject
+# Sphinx
+docs/_build/
-# mkdocs documentation
+# MkDocs
/site
+# PyBuilder
+.pybuilder/
+target/
+
# mypy
-.mypy_cache/
.dmypy.json
dmypy.json
-# Pyre type checker
-.pyre/
+### HPC & Slurm ###
+nautilus/*.yaml
+*.key
+sbatch*.sh
-# pytype static type analyzer
-.pytype/
+### Miscellaneous ###
+# W&B
+wandb/
-# Cython debug symbols
-cython_debug/
+# Dev scripts
+.dev/
+
+# Data folders
+data/
+outputs/
+
+# Translations
+*.mo
+*.pot
+
+# Dev folders
+.cache/*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e25f33ee0b..e509d6d88e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -12,9 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-exclude: "tests/artifacts/.*\\.safetensors$"
default_language_version:
python: python3.10
+
+exclude: "tests/artifacts/.*\\.safetensors$"
+
repos:
##### Meta #####
- repo: meta
@@ -22,12 +24,12 @@ repos:
- id: check-useless-excludes
- id: check-hooks-apply
-
- ##### Style / Misc. #####
+ ##### General Code Quality & Formatting #####
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
+ args: ['--maxkb=1024']
- id: debug-statements
- id: check-merge-conflict
- id: check-case-conflict
@@ -36,7 +38,14 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- - repo: https://github.com/adhtruong/mirrors-typos
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.11.13
+ hooks:
+ - id: ruff-format
+ - id: ruff
+ args: [--fix, --exit-non-zero-on-fix]
+
+ - repo: https://github.com/crate-ci/typos
rev: v1.34.0
hooks:
- id: typos
@@ -46,14 +55,16 @@ repos:
rev: v3.20.0
hooks:
- id: pyupgrade
+ args: [--py310-plus]
- - repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.12.3
+ ##### Markdown Quality #####
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v4.0.0-alpha.8
hooks:
- - id: ruff
- args: [--fix]
- - id: ruff-format
-
+ - id: prettier
+ name: Format Markdown with Prettier
+ types_or: [markdown, mdx]
+ args: [--prose-wrap=preserve]
##### Security #####
- repo: https://github.com/gitleaks/gitleaks
@@ -72,3 +83,25 @@ repos:
- id: bandit
args: ["-c", "pyproject.toml"]
additional_dependencies: ["bandit[toml]"]
+
+ # TODO(Steven): Uncomment when ready to use
+ ##### Static Analysis & Typing #####
+ # - repo: https://github.com/pre-commit/mirrors-mypy
+ # rev: v1.16.0
+ # hooks:
+ # - id: mypy
+ # args: [--python-version=3.10]
+
+ ##### Docstring Checks #####
+ # - repo: https://github.com/akaihola/darglint2
+ # rev: v1.8.2
+ # hooks:
+ # - id: darglint2
+ # args: ["--docstring-style", "google", "-v", "2"]
+ # exclude: ^tests/.*$
+
+ # - repo: https://github.com/econchick/interrogate
+ # rev: 1.7.0
+ # hooks:
+ # - id: interrogate
+ # args: ["-vv", "--config=pyproject.toml"]
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 04a0527536..c0fdac843b 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,4 +1,3 @@
-
# Contributor Covenant Code of Conduct
## Our Pledge
@@ -18,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
-* Demonstrating empathy and kindness toward other people
-* Being respectful of differing opinions, viewpoints, and experiences
-* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes,
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the overall
+- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
-* The use of sexualized language or imagery, and sexual attention or advances of
+- The use of sexualized language or imagery, and sexual attention or advances of
any kind
-* Trolling, insulting or derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or email address,
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email address,
without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
+- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a354e13469..369af602bf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -15,10 +15,11 @@ Whichever way you choose to contribute, please be mindful to respect our
## You can contribute in so many ways!
Some of the ways you can contribute to 🤗 LeRobot:
-* Fixing outstanding issues with the existing code.
-* Implementing new models, datasets or simulation environments.
-* Contributing to the examples or to the documentation.
-* Submitting issues related to bugs or desired new features.
+
+- Fixing outstanding issues with the existing code.
+- Implementing new models, datasets or simulation environments.
+- Contributing to the examples or to the documentation.
+- Submitting issues related to bugs or desired new features.
Following the guides below, feel free to open issues and PRs and to coordinate your efforts with the community on our [Discord Channel](https://discord.gg/VjFz58wn3R). For specific inquiries, reach out to [Remi Cadene](mailto:remi.cadene@huggingface.co).
@@ -40,24 +41,26 @@ already reported** (use the search bar on Github under Issues).
Did not find it? :( So we can act quickly on it, please follow these steps:
-* Include your **OS type and version**, the versions of **Python** and **PyTorch**.
-* A short, self-contained, code snippet that allows us to reproduce the bug in
+- Include your **OS type and version**, the versions of **Python** and **PyTorch**.
+- A short, self-contained, code snippet that allows us to reproduce the bug in
less than 30s.
-* The full traceback if an exception is raised.
-* Attach any other additional information, like screenshots, you think may help.
+- The full traceback if an exception is raised.
+- Attach any other additional information, like screenshots, you think may help.
### Do you want a new feature?
A good feature request addresses the following points:
1. Motivation first:
-* Is it related to a problem/frustration with the library? If so, please explain
+
+- Is it related to a problem/frustration with the library? If so, please explain
why. Providing a code snippet that demonstrates the problem is best.
-* Is it related to something you would need for a project? We'd love to hear
+- Is it related to something you would need for a project? We'd love to hear
about it!
-* Is it something you worked on and think could benefit the community?
+- Is it something you worked on and think could benefit the community?
Awesome! Tell us what problem it solved for you.
-2. Write a *paragraph* describing the feature.
+
+2. Write a _paragraph_ describing the feature.
3. Provide a **code snippet** that demonstrates its future use.
4. In case this is related to a paper, please attach a link.
5. Attach any additional information (drawings, screenshots, etc.) you think may help.
@@ -74,12 +77,15 @@ environments ([aloha](https://github.com/huggingface/gym-aloha),
and follow the same api design.
When implementing a new dataset loadable with LeRobotDataset follow these steps:
+
- Update `available_datasets_per_env` in `lerobot/__init__.py`
When implementing a new environment (e.g. `gym_aloha`), follow these steps:
+
- Update `available_tasks_per_env` and `available_datasets_per_env` in `lerobot/__init__.py`
When implementing a new policy class (e.g. `DiffusionPolicy`) follow these steps:
+
- Update `available_policies` and `available_policies_per_env`, in `lerobot/__init__.py`
- Set the required `name` class attribute.
- Update variables in `tests/test_available.py` by importing your new Policy class
@@ -133,11 +139,13 @@ Follow these steps to start contributing:
Follow the instructions to [install poetry](https://python-poetry.org/docs/#installation) (use a version >=2.1.0) or to [install uv](https://docs.astral.sh/uv/getting-started/installation/#installation-methods) if you don't have one of them already.
Set up a development environment with conda or miniconda:
+
```bash
conda create -y -n lerobot-dev python=3.10 && conda activate lerobot-dev
```
If you're using `uv`, it can manage python versions so you can instead do:
+
```bash
uv venv --python 3.10 && source .venv/bin/activate
```
@@ -145,11 +153,13 @@ Follow these steps to start contributing:
To develop on 🤗 LeRobot, you will at least need to install the `dev` and `test` extras dependencies along with the core library:
using `poetry`
+
```bash
poetry sync --extras "dev test"
```
using `uv`
+
```bash
uv sync --extra dev --extra test
```
@@ -157,43 +167,48 @@ Follow these steps to start contributing:
You can also install the project with all its dependencies (including environments):
using `poetry`
+
```bash
poetry sync --all-extras
```
using `uv`
+
```bash
uv sync --all-extras
```
- > **Note:** If you don't install simulation environments with `--all-extras`, the tests that require them will be skipped when running the pytest suite locally. However, they *will* be tested in the CI. In general, we advise you to install everything and test locally before pushing.
+ > **Note:** If you don't install simulation environments with `--all-extras`, the tests that require them will be skipped when running the pytest suite locally. However, they _will_ be tested in the CI. In general, we advise you to install everything and test locally before pushing.
Whichever command you chose to install the project (e.g. `poetry sync --all-extras`), you should run it again when pulling code with an updated version of `pyproject.toml` and `poetry.lock` in order to synchronize your virtual environment with the new dependencies.
The equivalent of `pip install some-package`, would just be:
using `poetry`
+
```bash
poetry add some-package
```
using `uv`
+
```bash
uv add some-package
```
When making changes to the poetry sections of the `pyproject.toml`, you should run the following command to lock dependencies.
using `poetry`
+
```bash
poetry lock
```
using `uv`
+
```bash
uv lock
```
-
5. Develop the features on your branch.
As you work on the features, you should make sure that the test suite
@@ -211,11 +226,13 @@ Follow these steps to start contributing:
automatically as Git commit hooks.
Install `pre-commit` hooks:
+
```bash
pre-commit install
```
You can run these hooks whenever you need on staged files with:
+
```bash
pre-commit
```
@@ -229,6 +246,7 @@ Follow these steps to start contributing:
```
Note, if you already committed some changes that have a wrong formatting, you can use:
+
```bash
pre-commit run --all-files
```
@@ -249,16 +267,15 @@ Follow these steps to start contributing:
git push -u origin a-descriptive-name-for-my-changes
```
-6. Once you are satisfied (**and the checklist below is happy too**), go to the
+7. Once you are satisfied (**and the checklist below is happy too**), go to the
webpage of your fork on GitHub. Click on 'Pull request' to send your changes
to the project maintainers for review.
-7. It's ok if maintainers ask you for changes. It happens to core contributors
+8. It's ok if maintainers ask you for changes. It happens to core contributors
too! So everyone can see the changes in the Pull request, work in your local
branch and push the changes to your fork. They will automatically appear in
the pull request.
-
### Checklist
1. The title of your pull request should be a summary of its contribution;
@@ -277,18 +294,21 @@ An extensive test suite is included to test the library behavior and several exa
Install [git lfs](https://git-lfs.com/) to retrieve test artifacts (if you don't have it already).
On Mac:
+
```bash
brew install git-lfs
git lfs install
```
On Ubuntu:
+
```bash
sudo apt-get install git-lfs
git lfs install
```
Pull artifacts if they're not in [tests/artifacts](tests/artifacts)
+
```bash
git lfs pull
```
@@ -300,6 +320,5 @@ repository, here's how to run tests with `pytest` for the library:
python -m pytest -sv ./tests
```
-
You can specify a smaller set of tests in order to test only the feature
you're working on.
diff --git a/README.md b/README.md
index ff7a923844..1d7cbcad48 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,6 @@
/>
-
Meet the updated SO100, the SO-101 – Just €114 per arm!
Train it in minutes with a few simple moves on your laptop.
Then sit back and watch your creation act autonomously! 🤯
@@ -120,52 +119,61 @@
- Thanks to Antonio Loquercio and Ashish Kumar for their early support.
- Thanks to [Seungjae (Jay) Lee](https://sjlee.cc/), [Mahi Shafiullah](https://mahis.life/) and colleagues for open sourcing [VQ-BeT](https://sjlee.cc/vq-bet/) policy and helping us adapt the codebase to our repository. The policy is adapted from [VQ-BeT repo](https://github.com/jayLEE0301/vq_bet_official).
-
## Installation
Download our source code:
+
```bash
git clone https://github.com/huggingface/lerobot.git
cd lerobot
```
Create a virtual environment with Python 3.10 and activate it, e.g. with [`miniconda`](https://docs.anaconda.com/free/miniconda/index.html):
+
```bash
conda create -y -n lerobot python=3.10
conda activate lerobot
```
When using `miniconda`, install `ffmpeg` in your environment:
+
```bash
conda install ffmpeg -c conda-forge
```
> **NOTE:** This usually installs `ffmpeg 7.X` for your platform compiled with the `libsvtav1` encoder. If `libsvtav1` is not supported (check supported encoders with `ffmpeg -encoders`), you can:
-> - _[On any platform]_ Explicitly install `ffmpeg 7.X` using:
-> ```bash
-> conda install ffmpeg=7.1.1 -c conda-forge
-> ```
-> - _[On Linux only]_ Install [ffmpeg build dependencies](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#GettheDependencies) and [compile ffmpeg from source with libsvtav1](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#libsvtav1), and make sure you use the corresponding ffmpeg binary to your install with `which ffmpeg`.
+>
+> - _[On any platform]_ Explicitly install `ffmpeg 7.X` using:
+>
+> ```bash
+> conda install ffmpeg=7.1.1 -c conda-forge
+> ```
+>
+> - _[On Linux only]_ Install [ffmpeg build dependencies](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#GettheDependencies) and [compile ffmpeg from source with libsvtav1](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#libsvtav1), and make sure you use the corresponding ffmpeg binary to your install with `which ffmpeg`.
Install 🤗 LeRobot:
+
```bash
pip install -e .
```
> **NOTE:** If you encounter build errors, you may need to install additional dependencies (`cmake`, `build-essential`, and `ffmpeg libs`). On Linux, run:
-`sudo apt-get install cmake build-essential python3-dev pkg-config libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev`. For other systems, see: [Compiling PyAV](https://pyav.org/docs/develop/overview/installation.html#bring-your-own-ffmpeg)
+> `sudo apt-get install cmake build-essential python3-dev pkg-config libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev`. For other systems, see: [Compiling PyAV](https://pyav.org/docs/develop/overview/installation.html#bring-your-own-ffmpeg)
For simulations, 🤗 LeRobot comes with gymnasium environments that can be installed as extras:
+
- [aloha](https://github.com/huggingface/gym-aloha)
- [xarm](https://github.com/huggingface/gym-xarm)
- [pusht](https://github.com/huggingface/gym-pusht)
For instance, to install 🤗 LeRobot with aloha and pusht, use:
+
```bash
pip install -e ".[aloha, pusht]"
```
To use [Weights and Biases](https://docs.wandb.ai/quickstart) for experiment tracking, log in with
+
```bash
wandb login
```
@@ -177,6 +185,7 @@ wandb login
Check out [example 1](./examples/1_load_lerobot_dataset.py) that illustrates how to use our dataset class which automatically downloads data from the Hugging Face hub.
You can also locally visualize episodes from a dataset on the hub by executing our script from the command line:
+
```bash
python -m lerobot.scripts.visualize_dataset \
--repo-id lerobot/pusht \
@@ -184,6 +193,7 @@ python -m lerobot.scripts.visualize_dataset \
```
or from a dataset in a local folder with the `root` option and the `--local-files-only` (in the following case the dataset will be searched for in `./my_local_data_dir/lerobot/pusht`)
+
```bash
python -m lerobot.scripts.visualize_dataset \
--repo-id lerobot/pusht \
@@ -192,19 +202,17 @@ python -m lerobot.scripts.visualize_dataset \
--episode-index 0
```
-
It will open `rerun.io` and display the camera streams, robot states and actions, like this:
https://github-production-user-asset-6210df.s3.amazonaws.com/4681518/328035972-fd46b787-b532-47e2-bb6f-fd536a55a7ed.mov?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20240505%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240505T172924Z&X-Amz-Expires=300&X-Amz-Signature=d680b26c532eeaf80740f08af3320d22ad0b8a4e4da1bcc4f33142c15b509eda&X-Amz-SignedHeaders=host&actor_id=24889239&key_id=0&repo_id=748713144
-
Our script can also visualize datasets stored on a distant server. See `python -m lerobot.scripts.visualize_dataset --help` for more instructions.
### The `LeRobotDataset` format
A dataset in `LeRobotDataset` format is very simple to use. It can be loaded from a repository on the Hugging Face hub or a local folder simply with e.g. `dataset = LeRobotDataset("lerobot/aloha_static_coffee")` and can be indexed into like any Hugging Face and PyTorch dataset. For instance `dataset[0]` will retrieve a single temporal frame from the dataset containing observation(s) and an action as PyTorch tensors ready to be fed to a model.
-A specificity of `LeRobotDataset` is that, rather than retrieving a single frame by its index, we can retrieve several frames based on their temporal relationship with the indexed frame, by setting `delta_timestamps` to a list of relative times with respect to the indexed frame. For example, with `delta_timestamps = {"observation.image": [-1, -0.5, -0.2, 0]}` one can retrieve, for a given index, 4 frames: 3 "previous" frames 1 second, 0.5 seconds, and 0.2 seconds before the indexed frame, and the indexed frame itself (corresponding to the 0 entry). See example [1_load_lerobot_dataset.py](examples/1_load_lerobot_dataset.py) for more details on `delta_timestamps`.
+A specificity of `LeRobotDataset` is that, rather than retrieving a single frame by its index, we can retrieve several frames based on their temporal relationship with the indexed frame, by setting `delta_timestamps` to a list of relative times with respect to the indexed frame. For example, with `delta_timestamps = {"observation.image": [-1, -0.5, -0.2, 0]}` one can retrieve, for a given index, 4 frames: 3 "previous" frames 1 second, 0.5 seconds, and 0.2 seconds before the indexed frame, and the indexed frame itself (corresponding to the 0 entry). See example [1_load_lerobot_dataset.py](examples/1_load_lerobot_dataset.py) for more details on `delta_timestamps`.
Under the hood, the `LeRobotDataset` format makes use of several ways to serialize data which can be useful to understand if you plan to work more closely with this format. We tried to make a flexible yet simple dataset format that would cover most type of features and specificities present in reinforcement learning and robotics, in simulation and in real-world, with a focus on cameras and robot states but easily extended to other types of sensory inputs as long as they can be represented by a tensor.
@@ -239,6 +247,7 @@ dataset attributes:
```
A `LeRobotDataset` is serialised using several widespread file formats for each of its parts, namely:
+
- hf_dataset stored using Hugging Face datasets library serialization to parquet
- videos are stored in mp4 format to save space
- metadata are stored in plain json/jsonl files
@@ -250,6 +259,7 @@ Dataset can be uploaded/downloaded from the HuggingFace hub seamlessly. To work
Check out [example 2](./examples/2_evaluate_pretrained_policy.py) that illustrates how to download a pretrained policy from Hugging Face hub, and run an evaluation on its corresponding environment.
We also provide a more capable script to parallelize the evaluation over multiple environments during the same rollout. Here is an example with a pretrained model hosted on [lerobot/diffusion_pusht](https://huggingface.co/lerobot/diffusion_pusht):
+
```bash
python -m lerobot.scripts.eval \
--policy.path=lerobot/diffusion_pusht \
@@ -284,9 +294,11 @@ Note: For efficiency, during training every checkpoint is evaluated on a low num
We provide some pretrained policies on our [hub page](https://huggingface.co/lerobot) that can achieve state-of-the-art performances.
You can reproduce their training by loading the config from their run. Simply running:
+
```bash
python -m lerobot.scripts.train --config_path=lerobot/diffusion_pusht
```
+
reproduces SOTA results for Diffusion Policy on the PushT task.
## Contribute
@@ -313,27 +325,29 @@ See `python lerobot/scripts/push_dataset_to_hub.py --help` for more instructions
If your dataset format is not supported, implement your own in `lerobot/datasets/push_dataset_to_hub/${raw_format}_format.py` by copying examples like [pusht_zarr](https://github.com/huggingface/lerobot/blob/main/lerobot/datasets/push_dataset_to_hub/pusht_zarr_format.py), [umi_zarr](https://github.com/huggingface/lerobot/blob/main/lerobot/datasets/push_dataset_to_hub/umi_zarr_format.py), [aloha_hdf5](https://github.com/huggingface/lerobot/blob/main/lerobot/datasets/push_dataset_to_hub/aloha_hdf5_format.py), or [xarm_pkl](https://github.com/huggingface/lerobot/blob/main/lerobot/datasets/push_dataset_to_hub/xarm_pkl_format.py). -->
-
### Add a pretrained policy
Once you have trained a policy you may upload it to the Hugging Face hub using a hub id that looks like `${hf_user}/${repo_name}` (e.g. [lerobot/diffusion_pusht](https://huggingface.co/lerobot/diffusion_pusht)).
You first need to find the checkpoint folder located inside your experiment directory (e.g. `outputs/train/2024-05-05/20-21-12_aloha_act_default/checkpoints/002500`). Within that there is a `pretrained_model` directory which should contain:
+
- `config.json`: A serialized version of the policy configuration (following the policy's dataclass config).
- `model.safetensors`: A set of `torch.nn.Module` parameters, saved in [Hugging Face Safetensors](https://huggingface.co/docs/safetensors/index) format.
- `train_config.json`: A consolidated configuration containing all parameters used for training. The policy configuration should match `config.json` exactly. This is useful for anyone who wants to evaluate your policy or for reproducibility.
To upload these to the hub, run the following:
+
```bash
huggingface-cli upload ${hf_user}/${repo_name} path/to/pretrained_model
```
See [eval.py](https://github.com/huggingface/lerobot/blob/main/lerobot/scripts/eval.py) for an example of how other people may use your policy.
-
### Improve your code with profiling
An example of a code snippet to profile the evaluation of a policy:
+
+
```python
from torch.profiler import profile, record_function, ProfilerActivity
@@ -354,10 +368,12 @@ with profile(
prof.step()
# insert code to profile, potentially whole body of eval_policy function
```
+
## Citation
If you want, you can cite this work with:
+
```bibtex
@misc{cadene2024lerobot,
author = {Cadene, Remi and Alibert, Simon and Soare, Alexander and Gallouedec, Quentin and Zouitine, Adil and Palma, Steven and Kooijmans, Pepijn and Aractingi, Michel and Shukor, Mustafa and Aubakirova, Dana and Russi, Martino and Capuano, Francesco and Pascale, Caroline and Choghari, Jade and Moss, Jess and Wolf, Thomas},
@@ -368,7 +384,9 @@ If you want, you can cite this work with:
```
Additionally, if you are using any of the particular policy architecture, pretrained models, or datasets, it is recommended to cite the original authors of the work as they appear below:
+
- [SmolVLA](https://arxiv.org/abs/2506.01844)
+
```bibtex
@article{shukor2025smolvla,
title={SmolVLA: A Vision-Language-Action Model for Affordable and Efficient Robotics},
@@ -379,6 +397,7 @@ Additionally, if you are using any of the particular policy architecture, pretra
```
- [Diffusion Policy](https://diffusion-policy.cs.columbia.edu)
+
```bibtex
@article{chi2024diffusionpolicy,
author = {Cheng Chi and Zhenjia Xu and Siyuan Feng and Eric Cousineau and Yilun Du and Benjamin Burchfiel and Russ Tedrake and Shuran Song},
@@ -387,7 +406,9 @@ Additionally, if you are using any of the particular policy architecture, pretra
year = {2024},
}
```
+
- [ACT or ALOHA](https://tonyzhaozh.github.io/aloha)
+
```bibtex
@article{zhao2023learning,
title={Learning fine-grained bimanual manipulation with low-cost hardware},
@@ -409,6 +430,7 @@ Additionally, if you are using any of the particular policy architecture, pretra
```
- [VQ-BeT](https://sjlee.cc/vq-bet/)
+
```bibtex
@article{lee2024behavior,
title={Behavior generation with latent actions},
@@ -418,8 +440,8 @@ Additionally, if you are using any of the particular policy architecture, pretra
}
```
-
- [HIL-SERL](https://hil-serl.github.io/)
+
```bibtex
@Article{luo2024hilserl,
title={Precise and Dexterous Robotic Manipulation via Human-in-the-Loop Reinforcement Learning},
@@ -430,6 +452,7 @@ archivePrefix={arXiv},
primaryClass={cs.RO}
}
```
+
## Star History
[](https://star-history.com/#huggingface/lerobot&Timeline)
diff --git a/benchmarks/video/README.md b/benchmarks/video/README.md
index daa3e1f48d..490a4b4950 100644
--- a/benchmarks/video/README.md
+++ b/benchmarks/video/README.md
@@ -1,28 +1,32 @@
# Video benchmark
-
## Questions
+
What is the optimal trade-off between:
+
- maximizing loading time with random access,
- minimizing memory space on disk,
- maximizing success rate of policies,
- compatibility across devices/platforms for decoding videos (e.g. video players, web browsers).
How to encode videos?
+
- Which video codec (`-vcodec`) to use? h264, h265, AV1?
- What pixel format to use (`-pix_fmt`)? `yuv444p` or `yuv420p`?
- How much compression (`-crf`)? No compression with `0`, intermediate compression with `25` or extreme with `50+`?
- Which frequency to chose for key frames (`-g`)? A key frame every `10` frames?
How to decode videos?
+
- Which `decoder`? `torchvision`, `torchaudio`, `ffmpegio`, `decord`, or `nvc`?
- What scenarios to use for the requesting timestamps during benchmark? (`timestamps_mode`)
-
## Variables
+
**Image content & size**
We don't expect the same optimal settings for a dataset of images from a simulation, or from real-world in an apartment, or in a factory, or outdoor, or with lots of moving objects in the scene, etc. Similarly, loading times might not vary linearly with the image size (resolution).
For these reasons, we run this benchmark on four representative datasets:
+
- `lerobot/pusht_image`: (96 x 96 pixels) simulation with simple geometric shapes, fixed camera.
- `aliberts/aloha_mobile_shrimp_image`: (480 x 640 pixels) real-world indoor, moving camera.
- `aliberts/paris_street`: (720 x 1280 pixels) real-world outdoor, moving camera.
@@ -34,8 +38,9 @@ Note: The datasets used for this benchmark need to be image datasets, not video
We might revisit this benchmark and find better settings if we train our policies with various data augmentations to make them more robust (e.g. robust to color changes, compression, etc.).
### Encoding parameters
+
| parameter | values |
-|-------------|--------------------------------------------------------------|
+| ----------- | ------------------------------------------------------------ |
| **vcodec** | `libx264`, `libx265`, `libsvtav1` |
| **pix_fmt** | `yuv444p`, `yuv420p` |
| **g** | `1`, `2`, `3`, `4`, `5`, `6`, `10`, `15`, `20`, `40`, `None` |
@@ -44,19 +49,23 @@ We might revisit this benchmark and find better settings if we train our policie
Note that `crf` value might be interpreted differently by various video codecs. In other words, the same value used with one codec doesn't necessarily translate into the same compression level with another codec. In fact, the default value (`None`) isn't the same amongst the different video codecs. Importantly, it is also the case for many other ffmpeg arguments like `g` which specifies the frequency of the key frames.
For a comprehensive list and documentation of these parameters, see the ffmpeg documentation depending on the video codec used:
+
- h264: https://trac.ffmpeg.org/wiki/Encode/H.264
- h265: https://trac.ffmpeg.org/wiki/Encode/H.265
- AV1: https://trac.ffmpeg.org/wiki/Encode/AV1
### Decoding parameters
+
**Decoder**
We tested two video decoding backends from torchvision:
+
- `pyav`
- `video_reader` (requires to build torchvision from source)
**Requested timestamps**
Given the way video decoding works, once a keyframe has been loaded, the decoding of subsequent frames is fast.
This of course is affected by the `-g` parameter during encoding, which specifies the frequency of the keyframes. Given our typical use cases in robotics policies which might request a few timestamps in different random places, we want to replicate these use cases with the following scenarios:
+
- `1_frame`: 1 frame,
- `2_frames`: 2 consecutive frames (e.g. `[t, t + 1 / fps]`),
- `6_frames`: 6 consecutive frames (e.g. `[t + i / fps for i in range(6)]`)
@@ -64,12 +73,13 @@ This of course is affected by the `-g` parameter during encoding, which specifie
Note that this differs significantly from a typical use case like watching a movie, in which every frame is loaded sequentially from the beginning to the end and it's acceptable to have big values for `-g`.
Additionally, because some policies might request single timestamps that are a few frames apart, we also have the following scenario:
+
- `2_frames_4_space`: 2 frames with 4 consecutive frames of spacing in between (e.g `[t, t + 5 / fps]`),
However, due to how video decoding is implemented with `pyav`, we don't have access to an accurate seek so in practice this scenario is essentially the same as `6_frames` since all 6 frames between `t` and `t + 5 / fps` will be decoded.
-
## Metrics
+
**Data compression ratio (lower is better)**
`video_images_size_ratio` is the ratio of the memory space on disk taken by the encoded video over the memory space taken by the original images. For instance, `video_images_size_ratio=25%` means that the video takes 4 times less memory space on disk compared to the original images.
@@ -87,18 +97,18 @@ However, due to how video decoding is implemented with `pyav`, we don't have acc
One aspect that can't be measured here with those metrics is the compatibility of the encoding across platforms, in particular on web browser, for visualization purposes.
h264, h265 and AV1 are all commonly used codecs and should not pose an issue. However, the chroma subsampling (`pix_fmt`) format might affect compatibility:
+
- `yuv420p` is more widely supported across various platforms, including web browsers.
- `yuv444p` offers higher color fidelity but might not be supported as broadly.
-
-
## How the benchmark works
+
The benchmark evaluates both encoding and decoding of video frames on the first episode of each dataset.
**Encoding:** for each `vcodec` and `pix_fmt` pair, we use a default value for `g` and `crf` upon which we change a single value (either `g` or `crf`) to one of the specified values (we don't test every combination of those as this would be computationally too heavy).
@@ -110,15 +120,18 @@ Intermediate results saved for each `vcodec` and `pix_fmt` combination in csv ta
These are then all concatenated to a single table ready for analysis.
## Caveats
+
We tried to measure the most impactful parameters for both encoding and decoding. However, for computational reasons we can't test out every combination.
Additional encoding parameters exist that are not included in this benchmark. In particular:
+
- `-preset` which allows for selecting encoding presets. This represents a collection of options that will provide a certain encoding speed to compression ratio. By leaving this parameter unspecified, it is considered to be `medium` for libx264 and libx265 and `8` for libsvtav1.
- `-tune` which allows to optimize the encoding for certain aspects (e.g. film quality, fast decoding, etc.).
See the documentation mentioned above for more detailed info on these settings and for a more comprehensive list of other parameters.
Similarly on the decoding side, other decoders exist but are not implemented in our current benchmark. To name a few:
+
- `torchaudio`
- `ffmpegio`
- `decord`
@@ -127,16 +140,17 @@ Similarly on the decoding side, other decoders exist but are not implemented in
Note as well that since we are mostly interested in the performance at decoding time (also because encoding is done only once before uploading a dataset), we did not measure encoding times nor have any metrics regarding encoding.
However, besides the necessity to build ffmpeg from source, encoding did not pose any issue and it didn't take a significant amount of time during this benchmark.
-
## Install
+
Building ffmpeg from source is required to include libx265 and libaom/libsvtav1 (av1) video codecs ([compilation guide](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu)).
**Note:** While you still need to build torchvision with a conda-installed `ffmpeg<4.3` to use the `video_reader` decoder (as described in [#220](https://github.com/huggingface/lerobot/pull/220)), you also need another version which is custom-built with all the video codecs for encoding. For the script to then use that version, you can prepend the command above with `PATH="$HOME/bin:$PATH"`, which is where ffmpeg should be built.
-
## Adding a video decoder
+
Right now, we're only benchmarking the two video decoder available with torchvision: `pyav` and `video_reader`.
You can easily add a new decoder to benchmark by adding it to this function in the script:
+
```diff
def decode_video_frames(
video_path: str,
@@ -156,9 +170,10 @@ def decode_video_frames(
raise NotImplementedError(backend)
```
-
## Example
+
For a quick run, you can try these parameters:
+
```bash
python benchmark/video/run_video_benchmark.py \
--output-dir outputs/video_benchmark \
@@ -176,11 +191,12 @@ python benchmark/video/run_video_benchmark.py \
--save-frames 0
```
-
## Results
### Reproduce
+
We ran the benchmark with the following parameters:
+
```bash
# h264 and h265 encodings
python benchmark/video/run_video_benchmark.py \
@@ -221,9 +237,10 @@ python benchmark/video/run_video_benchmark.py \
The full results are available [here](https://docs.google.com/spreadsheets/d/1OYJB43Qu8fC26k_OyoMFgGBBKfQRCi4BIuYitQnq3sw/edit?usp=sharing)
-
### Parameters selected for LeRobotDataset
+
Considering these results, we chose what we think is the best set of encoding parameter:
+
- vcodec: `libsvtav1`
- pix-fmt: `yuv420p`
- g: `2`
@@ -236,7 +253,7 @@ Since we're using av1 encoding, we're choosing the `pyav` decoder as `video_read
These tables show the results for `g=2` and `crf=30`, using `timestamps-modes=6_frames` and `backend=pyav`
| video_images_size_ratio | vcodec | pix_fmt | | | |
-|------------------------------------|------------|---------|-----------|-----------|-----------|
+| ---------------------------------- | ---------- | ------- | --------- | --------- | --------- |
| | libx264 | | libx265 | | libsvtav1 |
| repo_id | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p |
| lerobot/pusht_image | **16.97%** | 17.58% | 18.57% | 18.86% | 22.06% |
@@ -245,7 +262,7 @@ These tables show the results for `g=2` and `crf=30`, using `timestamps-modes=6_
| aliberts/kitchen | 1.40% | 1.39% | **1.00%** | **1.00%** | 2.52% |
| video_images_load_time_ratio | vcodec | pix_fmt | | | |
-|------------------------------------|---------|---------|----------|---------|-----------|
+| ---------------------------------- | ------- | ------- | -------- | ------- | --------- |
| | libx264 | | libx265 | | libsvtav1 |
| repo_id | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p |
| lerobot/pusht_image | 6.45 | 5.19 | **1.90** | 2.12 | 2.47 |
@@ -254,7 +271,7 @@ These tables show the results for `g=2` and `crf=30`, using `timestamps-modes=6_
| aliberts/kitchen | 1.46 | 1.46 | 0.28 | 0.51 | **0.26** |
| | | vcodec | pix_fmt | | | |
-|------------------------------------|----------|----------|--------------|----------|-----------|--------------|
+| ---------------------------------- | -------- | -------- | ------------ | -------- | --------- | ------------ |
| | | libx264 | | libx265 | | libsvtav1 |
| repo_id | metric | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p |
| lerobot/pusht_image | avg_mse | 2.90E-04 | **2.03E-04** | 3.13E-04 | 2.29E-04 | 2.19E-04 |
diff --git a/docs/README.md b/docs/README.md
index 275fee46bb..967de7b841 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -26,6 +26,7 @@ pip install -e ".[docs]"
You will also need `nodejs`. Please refer to their [installation page](https://nodejs.org/en/download)
---
+
**NOTE**
You only need to generate the documentation to inspect it locally (if you're planning changes and want to
@@ -63,6 +64,7 @@ doc-builder preview lerobot docs/source/
The docs will be viewable at [http://localhost:3000](http://localhost:3000). You can also preview the docs once you have opened a PR. You will see a bot add a comment to a link where the documentation with your changes lives.
---
+
**NOTE**
The `preview` command only works with existing doc files. When you add a completely new file, you need to update `_toctree.yml` & restart `preview` command (`ctrl-c` to stop it & call `doc-builder preview ...` again).
@@ -89,6 +91,7 @@ Sections that were moved:
[ Section A ]
```
+
and of course, if you moved it to another file, then:
```
@@ -119,7 +122,6 @@ and objects like True, None or any strings should usually be put in `code`.
Multi-line code blocks can be useful for displaying examples. They are done between two lines of three backticks as usual in Markdown:
-
````
```
# first line of code
diff --git a/docs/source/async.mdx b/docs/source/async.mdx
index 6ff05a88a4..397c513cff 100644
--- a/docs/source/async.mdx
+++ b/docs/source/async.mdx
@@ -5,17 +5,18 @@ In this tutorial, we'll show how to use asynchronous inference (_async inference
**Try async inference with all the policies** supported by LeRobot!
**What you'll learn:**
+
1. Why asynchronous inference matters and how it compares to, more traditional, sequential inference.
2. How to spin-up a `PolicyServer` and connect a `RobotClient` from the same machine, and even over the network.
3. How to tune key parameters (`actions_per_chunk`, `chunk_size_threshold`) for your robot and policy.
If you get stuck, hop into our [Discord community](https://discord.gg/s3KuuzsPFb)!
-
-In a nutshell: with *async inference*, your robot keeps acting while the policy server is already busy computing the next chunk of actions---eliminating "wait-for-inference" lags and unlocking smoother, more reactive behaviours.
+In a nutshell: with _async inference_, your robot keeps acting while the policy server is already busy computing the next chunk of actions---eliminating "wait-for-inference" lags and unlocking smoother, more reactive behaviours.
This is fundamentally different from synchronous inference (sync), where the robot stays idle while the policy computes the next chunk of actions.
---
+
## Getting started with async inference
You can read more information on asynchronous inference in our [blogpost](https://huggingface.co/blog/async-robot-inference). This guide is designed to help you quickly set up and run asynchronous inference in your environment.
@@ -53,40 +54,53 @@ python src/lerobot/scripts/server/robot_client.py \
--aggregate_fn_name=weighted_average \ # CLIENT: the function to aggregate actions on overlapping portions
--debug_visualize_queue_size=True # CLIENT: whether to visualize the queue size at runtime
```
+
In summary, you need to specify instructions for:
+
- `SERVER`: the address and port of the policy server
- `ROBOT`: the type of robot to connect to, the port to connect to, and the local `id` of the robot
- `POLICY`: the type of policy to run, and the model name/path on server to the checkpoint to run. You also need to specify which device should the sever be using, and how many actions to output at once (capped at the policy max actions value).
- `CLIENT`: the threshold for the chunk size before sending a new observation to the server, and the function to aggregate actions on overlapping portions. Optionally, you can also visualize the queue size at runtime, to help you tune the `CLIENT` parameters.
Importantly,
+
- `actions_per_chunk` and `chunk_size_threshold` are key parameters to tune for your setup.
- `aggregate_fn_name` is the function to aggregate actions on overlapping portions. You can either add a new one to a registry of functions, or add your own in `robot_client.py` (see [here](NOTE:addlinktoLOC))
- `debug_visualize_queue_size` is a useful tool to tune the `CLIENT` parameters.
-Done! You should see your robot moving around by now 😉
----
+## Done! You should see your robot moving around by now 😉
## Async vs. synchronous inference
-Synchronous inference relies on interleaving action chunk prediction and action execution. This inherently results in *idle frames*, frames where the robot awaits idle the policy's output: a new action chunk.
+Synchronous inference relies on interleaving action chunk prediction and action execution. This inherently results in _idle frames_, frames where the robot awaits idle the policy's output: a new action chunk.
In turn, inference is plagued by evident real-time lags, where the robot simply stops acting due to the lack of available actions.
With robotics models increasing in size, this problem risks becoming only more severe.
-
+
+
+
+ Synchronous inference makes the robot idle while the policy is
+ computing the next chunk of actions.
-
Synchronous inference makes the robot idle while the policy is computing the next chunk of actions.
To overcome this, we design async inference, a paradigm where action planning and execution are decoupled, resulting in (1) higher adaptability and, most importantly, (2) no idle frames.
-Crucially, with async inference, the next action chunk is computed *before* the current one is exhausted, resulting in no idleness.
+Crucially, with async inference, the next action chunk is computed _before_ the current one is exhausted, resulting in no idleness.
Higher adaptability is ensured by aggregating the different action chunks on overlapping portions, obtaining an up-to-date plan and a tighter control loop.
-
+
+
+
+ Asynchronous inference results in no idleness because the next chunk is
+ computed before the current chunk is exhausted.
-
Asynchronous inference results in no idleness because the next chunk is computed before the current chunk is exhausted.
-
---
@@ -105,6 +119,8 @@ python -m lerobot.scripts.server.policy_server \
```
+
+
```python
from lerobot.scripts.server.configs import PolicyServerConfig
from lerobot.scripts.server.policy_server import serve
@@ -115,6 +131,8 @@ config = PolicyServerConfig(
)
serve(config)
```
+
+
@@ -147,6 +165,8 @@ python src/lerobot/scripts/server/robot_client.py \
```
+
+
```python
import threading
from lerobot.robots.so100_follower import SO100FollowerConfig
@@ -201,6 +221,8 @@ if client.start():
# (Optionally) plot the action queue size
visualize_action_queue_size(client.action_queue_size)
```
+
+
@@ -216,20 +238,30 @@ The following two parameters are key in every setup:
-
actions_per_chunk
+
+ actions_per_chunk
+
50
-
How many actions the policy outputs at once. Typical values: 10-50.
+
+ How many actions the policy outputs at once. Typical values: 10-50.
+
-
chunk_size_threshold
+
+ chunk_size_threshold
+
0.7
-
When the queue is ≤ 50% full, the client sends a fresh observation. Value in [0, 1].
+
+ When the queue is ≤ 50% full, the client sends a fresh observation.
+ Value in [0, 1].
+
-Different values of `actions_per_chunk` and `chunk_size_threshold` do result in different behaviours.
+ Different values of `actions_per_chunk` and `chunk_size_threshold` do result
+ in different behaviours.
On the one hand, increasing the value of `actions_per_chunk` will result in reducing the likelihood of ending up with no actions to execute, as more actions will be available when the new chunk is computed.
@@ -249,10 +281,18 @@ We found the default values of `actions_per_chunk` and `chunk_size_threshold` to
- We found values around 0.5-0.6 to work well. If you want to tweak this, spin up a `RobotClient` setting the `--debug-visualize-queue-size` to `True`. This will plot the action queue size evolution at runtime, and you can use it to find the value of `chunk_size_threshold` that works best for your setup.
-
+
+
+
+
+ The action queue size is plotted at runtime when the
+ `--debug-visualize-queue-size` flag is passed, for various levels of
+ `chunk_size_threshold` (`g` in the SmolVLA paper).
+
-
The action queue size is plotted at runtime when the `--debug-visualize-queue-size` flag is passed, for various levels of `chunk_size_threshold` (`g` in the SmolVLA paper).
-
---
diff --git a/docs/source/backwardcomp.mdx b/docs/source/backwardcomp.mdx
index 5552391700..0e1d016365 100644
--- a/docs/source/backwardcomp.mdx
+++ b/docs/source/backwardcomp.mdx
@@ -6,21 +6,22 @@ PR [#777](https://github.com/huggingface/lerobot/pull/777) improves the LeRobot
### What changed?
-| | Before PR #777 | After PR #777 |
-| --------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
-| **Joint range** | Degrees `-180...180°` | **Normalised range** Joints: `–100...100` Gripper: `0...100` |
-| **Zero position (SO100 / SO101)** | Arm fully extended horizontally | **In middle of the range for each joint** |
-| **Boundary handling** | Software safeguards to detect ±180 ° wrap-arounds | No wrap-around logic needed due to mid-range zero |
+| | Before PR #777 | After PR #777 |
+| --------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
+| **Joint range** | Degrees `-180...180°` | **Normalised range** Joints: `–100...100` Gripper: `0...100` |
+| **Zero position (SO100 / SO101)** | Arm fully extended horizontally | **In middle of the range for each joint** |
+| **Boundary handling** | Software safeguards to detect ±180 ° wrap-arounds | No wrap-around logic needed due to mid-range zero |
---
### Impact on existing datasets
-* Recorded trajectories created **before** PR #777 will replay incorrectly if loaded directly:
- * Joint angles are offset and incorrectly normalized.
-* Any models directly finetuned or trained on the old data will need their inputs and outputs converted.
+- Recorded trajectories created **before** PR #777 will replay incorrectly if loaded directly:
+ - Joint angles are offset and incorrectly normalized.
+- Any models directly finetuned or trained on the old data will need their inputs and outputs converted.
### Using datasets made with the previous calibration system
+
We provide a migration example script for replaying an episode recorded with the previous calibration here: `examples/backward_compatibility/replay.py`.
Below we take you through the modifications that are done in the example script to make the previous calibration datasets work.
@@ -33,20 +34,31 @@ Below we take you through the modifications that are done in the example script
Let's break this down.
New codebase uses `.pos` suffix for the position observations and we have removed `main_` prefix:
+
+
```python
key = f"{name.removeprefix('main_')}.pos"
```
+
For `"shoulder_lift"` (id = 2), the 0 position is changed by -90 degrees and the direction is reversed compared to old calibration/code.
+
+
```python
action["shoulder_lift.pos"] = -(action["shoulder_lift.pos"] - 90)
```
+
+
For `"elbow_flex"` (id = 3), the 0 position is changed by -90 degrees compared to old calibration/code.
+
+
```python
action["elbow_flex.pos"] -= 90
```
+
To use degrees normalization we then set the `--robot.use_degrees` option to `true`.
+
```diff
python examples/backward_compatibility/replay.py \
--robot.type=so101_follower \
@@ -63,6 +75,7 @@ Policies output actions in the same format as the datasets (`torch.Tensors`). Th
To find these transformations, we recommend to first try and and replay an episode of the dataset your policy was trained on using the section above.
Then, add these same transformations on your inference script (shown here in the `record.py` script):
+
```diff
action_values = predict_action(
observation_frame,
diff --git a/docs/source/cameras.mdx b/docs/source/cameras.mdx
index 313d5a7cd1..604863d74a 100644
--- a/docs/source/cameras.mdx
+++ b/docs/source/cameras.mdx
@@ -7,11 +7,13 @@ LeRobot offers multiple options for video capture, including phone cameras, buil
To instantiate a camera, you need a camera identifier. This identifier might change if you reboot your computer or re-plug your camera, a behavior mostly dependant on your operating system.
To find the camera indices of the cameras plugged into your system, run the following script:
+
```bash
python -m lerobot.find_cameras opencv # or realsense for Intel Realsense cameras
```
The output will look something like this if you have two cameras connected:
+
```
--- Detected Cameras ---
Camera #0:
@@ -31,7 +33,6 @@ Camera #0:
> [!WARNING]
> When using Intel RealSense cameras in `macOS`, you could get this [error](https://github.com/IntelRealSense/librealsense/issues/12307): `Error finding RealSense cameras: failed to set power state`, this can be solved by running the same command with `sudo` permissions. Note that using RealSense cameras in `macOS` is unstable.
-
## Use Cameras
Below are two examples, demonstrating how to work with the API.
@@ -39,10 +40,10 @@ Below are two examples, demonstrating how to work with the API.
- **Asynchronous frame capture** using an OpenCV-based camera
- **Color and depth capture** using an Intel RealSense camera
-
+
```python
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig
from lerobot.cameras.opencv.camera_opencv import OpenCVCamera
@@ -70,10 +71,12 @@ try:
finally:
camera.disconnect()
```
+
+
```python
from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraConfig
from lerobot.cameras.realsense.camera_realsense import RealSenseCamera
@@ -103,15 +106,18 @@ try:
finally:
camera.disconnect()
```
+
+
-
## Use your phone
+
To use your iPhone as a camera on macOS, enable the Continuity Camera feature:
+
- Ensure your Mac is running macOS 13 or later, and your iPhone is on iOS 16 or later.
- Sign in both devices with the same Apple ID.
- Connect your devices with a USB cable or turn on Wi-Fi and Bluetooth for a wireless connection.
@@ -125,40 +131,67 @@ Your iPhone should be detected automatically when running the camera setup scrip
If you want to use your phone as a camera on Linux, follow these steps to set up a virtual camera
-1. *Install `v4l2loopback-dkms` and `v4l-utils`*. Those packages are required to create virtual camera devices (`v4l2loopback`) and verify their settings with the `v4l2-ctl` utility from `v4l-utils`. Install them using:
+1. _Install `v4l2loopback-dkms` and `v4l-utils`_. Those packages are required to create virtual camera devices (`v4l2loopback`) and verify their settings with the `v4l2-ctl` utility from `v4l-utils`. Install them using:
+
+
```python
sudo apt install v4l2loopback-dkms v4l-utils
```
-2. *Install [DroidCam](https://droidcam.app) on your phone*. This app is available for both iOS and Android.
-3. *Install [OBS Studio](https://obsproject.com)*. This software will help you manage the camera feed. Install it using [Flatpak](https://flatpak.org):
+
+
+2. _Install [DroidCam](https://droidcam.app) on your phone_. This app is available for both iOS and Android.
+3. _Install [OBS Studio](https://obsproject.com)_. This software will help you manage the camera feed. Install it using [Flatpak](https://flatpak.org):
+
+
```python
flatpak install flathub com.obsproject.Studio
```
-4. *Install the DroidCam OBS plugin*. This plugin integrates DroidCam with OBS Studio. Install it with:
+
+
+4. _Install the DroidCam OBS plugin_. This plugin integrates DroidCam with OBS Studio. Install it with:
+
+
```python
flatpak install flathub com.obsproject.Studio.Plugin.DroidCam
```
-5. *Start OBS Studio*. Launch with:
+
+
+5. _Start OBS Studio_. Launch with:
+
+
```python
flatpak run com.obsproject.Studio
```
-6. *Add your phone as a source*. Follow the instructions [here](https://droidcam.app/obs/usage). Be sure to set the resolution to `640x480`.
-7. *Adjust resolution settings*. In OBS Studio, go to `File > Settings > Video`. Change the `Base(Canvas) Resolution` and the `Output(Scaled) Resolution` to `640x480` by manually typing it in.
-8. *Start virtual camera*. In OBS Studio, follow the instructions [here](https://obsproject.com/kb/virtual-camera-guide).
-9. *Verify the virtual camera setup*. Use `v4l2-ctl` to list the devices:
+
+
+6. _Add your phone as a source_. Follow the instructions [here](https://droidcam.app/obs/usage). Be sure to set the resolution to `640x480`.
+7. _Adjust resolution settings_. In OBS Studio, go to `File > Settings > Video`. Change the `Base(Canvas) Resolution` and the `Output(Scaled) Resolution` to `640x480` by manually typing it in.
+8. _Start virtual camera_. In OBS Studio, follow the instructions [here](https://obsproject.com/kb/virtual-camera-guide).
+9. _Verify the virtual camera setup_. Use `v4l2-ctl` to list the devices:
+
+
```python
v4l2-ctl --list-devices
```
+
+
You should see an entry like:
+
```
VirtualCam (platform:v4l2loopback-000):
/dev/video1
```
-10. *Check the camera resolution*. Use `v4l2-ctl` to ensure that the virtual camera output resolution is `640x480`. Change `/dev/video1` to the port of your virtual camera from the output of `v4l2-ctl --list-devices`.
+
+10. _Check the camera resolution_. Use `v4l2-ctl` to ensure that the virtual camera output resolution is `640x480`. Change `/dev/video1` to the port of your virtual camera from the output of `v4l2-ctl --list-devices`.
+
+
```python
v4l2-ctl -d /dev/video1 --get-fmt-video
```
+
+
You should see an entry like:
+
```
>>> Format Video Capture:
>>> Width/Height : 640/480
diff --git a/docs/source/hilserl.mdx b/docs/source/hilserl.mdx
index b3ab40c898..c647a58d5e 100644
--- a/docs/source/hilserl.mdx
+++ b/docs/source/hilserl.mdx
@@ -4,18 +4,22 @@ In this tutorial you will go through the full Human-in-the-Loop Sample-Efficient
HIL-SERL is a sample-efficient reinforcement learning algorithm that combines human demonstrations with online learning and human interventions. The approach starts from a small set of human demonstrations, uses them to train a reward classifier, and then employs an actor-learner architecture where humans can intervene during policy execution to guide exploration and correct unsafe behaviors. In this tutorial, you'll use a gamepad to provide interventions and control the robot during the learning process.
-It combines three key ingredients:
- 1. **Offline demonstrations & reward classifier:** a handful of human-teleop episodes plus a vision-based success detector give the policy a shaped starting point.
- 2. **On-robot actor / learner loop with human interventions:** a distributed Soft Actor Critic (SAC) learner updates the policy while an actor explores on the physical robot; the human can jump in at any time to correct dangerous or unproductive behaviour.
- 3. **Safety & efficiency tools:** joint/end-effector (EE) bounds, crop region of interest (ROI) preprocessing and WandB monitoring keep the data useful and the hardware safe.
+It combines three key ingredients: 1. **Offline demonstrations & reward classifier:** a handful of human-teleop episodes plus a vision-based success detector give the policy a shaped starting point. 2. **On-robot actor / learner loop with human interventions:** a distributed Soft Actor Critic (SAC) learner updates the policy while an actor explores on the physical robot; the human can jump in at any time to correct dangerous or unproductive behaviour. 3. **Safety & efficiency tools:** joint/end-effector (EE) bounds, crop region of interest (ROI) preprocessing and WandB monitoring keep the data useful and the hardware safe.
Together these elements let HIL-SERL reach near-perfect task success and faster cycle times than imitation-only baselines.
-
+
-
HIL-SERL workflow, Luo et al. 2024
+
+ HIL-SERL workflow, Luo et al. 2024
+
This guide provides step-by-step instructions for training a robot policy using LeRobot's HilSerl implementation to train on a real robot.
@@ -29,6 +33,7 @@ This guide provides step-by-step instructions for training a robot policy using
## What kind of tasks can I train?
One can use HIL-SERL to train on a variety of manipulation tasks. Some recommendations:
+
- Start with a simple task to understand how the system works.
- Push cube to a goal region
- Pick and lift cube with the gripper
@@ -53,6 +58,7 @@ pip install -e ".[hilserl]"
The training process begins with proper configuration for the HILSerl environment. The configuration class of interest is `HILSerlRobotEnvConfig` in `lerobot/envs/configs.py`. Which is defined as:
+
```python
class HILSerlRobotEnvConfig(EnvConfig):
robot: RobotConfig | None = None # Main robot agent (defined in `lerobot/robots`)
@@ -72,7 +78,7 @@ class HILSerlRobotEnvConfig(EnvConfig):
reward_classifier_pretrained_path: str | None = None # For reward model
number_of_steps_after_success: int = 0 # For reward classifier, collect more positive examples after a success to train a classifier
```
-
+
### Finding Robot Workspace Bounds
@@ -131,6 +137,7 @@ Create a configuration file for recording demonstrations (or edit an existing on
5. Configure `robot`, `cameras`, and other hardware settings
Example configuration section:
+
```json
"mode": "record",
"repo_id": "username/pick_lift_cube",
@@ -150,6 +157,7 @@ HIL-Serl learns actions in the end-effector space of the robot. Therefore, the t
For that we need to define a version of the robot that takes actions in the end-effector space. Check the robot class `SO100FollowerEndEffector` and its configuration `SO100FollowerEndEffectorConfig` for the default parameters related to the end-effector space.
+
```python
class SO100FollowerEndEffectorConfig(SO100FollowerConfig):
"""Configuration for the SO100FollowerEndEffector robot."""
@@ -172,6 +180,7 @@ class SO100FollowerEndEffectorConfig(SO100FollowerConfig):
}
)
```
+
The `Teleoperator` defines the teleoperation device. You can check the list of available teleoperators in `lerobot/teleoperators`.
@@ -189,9 +198,16 @@ To setup the gamepad, you need to set the `control_mode` to `"gamepad"` and defi
```
-
+
+
+
+ Gamepad button mapping for robot control and episode management
-
Gamepad button mapping for robot control and episode management
**Setting up the SO101 leader**
@@ -215,7 +231,10 @@ During the online training, press `space` to take over the policy and `space` ag
@@ -231,6 +250,7 @@ python -m lerobot.scripts.rl.gym_manipulator --config_path src/lerobot/configs/e
```
During recording:
+
1. The robot will reset to the initial position defined in the configuration file `fixed_reset_joint_positions`
2. Complete the task successfully
3. The episode ends with a reward of 1 when you press the "success" button
@@ -239,13 +259,13 @@ During recording:
6. The process automatically continues to the next episode
7. After recording all episodes, the dataset is pushed to the Hugging Face Hub (optional) and saved locally
-
### Processing the Dataset
After collecting demonstrations, process them to determine optimal camera crops.
Reinforcement learning is sensitive to background distractions, so it is important to crop the images to the relevant workspace area.
Visual RL algorithms learn directly from pixel inputs, making them vulnerable to irrelevant visual information. Background elements like changing lighting, shadows, people moving, or objects outside the workspace can confuse the learning process. Good ROI selection should:
+
- Include only the essential workspace where the task happens
- Capture the robot's end-effector and all objects involved in the task
- Exclude unnecessary background elements and distractions
@@ -267,6 +287,7 @@ python -m lerobot.scripts.rl.crop_dataset_roi --repo-id username/pick_lift_cube
5. The script outputs cropping parameters and creates a new cropped dataset
Example output:
+
```
Selected Rectangular Regions of Interest (top, left, height, width):
observation.images.side: [180, 207, 180, 200]
@@ -274,11 +295,15 @@ observation.images.front: [180, 250, 120, 150]
```
-
+
-
Interactive cropping tool for selecting regions of interest
-
+
+ Interactive cropping tool for selecting regions of interest
+
**Updating Configuration**
@@ -294,8 +319,7 @@ Add these crop parameters to your training configuration:
**Recommended image resolution**
-Most vision-based policies have been validated on square inputs of either **128×128** (default) or **64×64** pixels. We therefore advise setting the resize_size parameter to [128, 128] – or [64, 64] if you need to save GPU memory and bandwidth. Other resolutions are possible but have not been extensively tested.
-
+Most vision-based policies have been validated on square inputs of either **128×128** (default) or **64×64** pixels. We therefore advise setting the resize_size parameter to [128, 128] – or [64, 64] if you need to save GPU memory and bandwidth. Other resolutions are possible but have not been extensively tested.
### Training a Reward Classifier
@@ -332,13 +356,13 @@ Example configuration section for data collection:
```json
{
- "mode": "record",
- "repo_id": "hf_username/dataset_name",
- "dataset_root": "data/your_dataset",
- "num_episodes": 20,
- "push_to_hub": true,
- "fps": 10,
- "number_of_steps_after_success": 15
+ "mode": "record",
+ "repo_id": "hf_username/dataset_name",
+ "dataset_root": "data/your_dataset",
+ "num_episodes": 20,
+ "push_to_hub": true,
+ "fps": 10,
+ "number_of_steps_after_success": 15
}
```
@@ -395,21 +419,25 @@ python -m lerobot.scripts.train --config_path path/to/reward_classifier_train_co
To use your trained reward classifier, configure the `HILSerlRobotEnvConfig` to use your model:
+
```python
env_config = HILSerlRobotEnvConfig(
reward_classifier_pretrained_path="path_to_your_pretrained_trained_model",
# Other environment parameters
)
```
+
+
or set the argument in the json config file.
```json
{
- "reward_classifier_pretrained_path": "path_to_your_pretrained_model"
+ "reward_classifier_pretrained_path": "path_to_your_pretrained_model"
}
```
Run `gym_manipulator.py` to test the model.
+
```bash
python -m lerobot.scripts.rl.gym_manipulator --config_path path/to/env_config.json
```
@@ -422,11 +450,13 @@ The reward classifier will automatically provide rewards based on the visual inp
Create the necessary json configuration files for the reward classifier and the environment. Check the examples [here](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/tree/main).
2. **Collect a dataset**:
+
```bash
python -m lerobot.scripts.rl.gym_manipulator --config_path src/lerobot/configs/env_config.json
```
3. **Train the classifier**:
+
```bash
python -m lerobot.scripts.train --config_path src/lerobot/configs/reward_classifier_train_config.json
```
@@ -459,6 +489,7 @@ python -m lerobot.scripts.rl.learner --config_path src/lerobot/configs/train_con
```
The learner:
+
- Initializes the policy network
- Prepares replay buffers
- Opens a `gRPC` server to communicate with actors
@@ -473,6 +504,7 @@ python -m lerobot.scripts.rl.actor --config_path src/lerobot/configs/train_confi
```
The actor:
+
- Connects to the learner via `gRPC`
- Initializes the environment
- Execute rollouts of the policy to collect experience
@@ -496,10 +528,19 @@ The training proceeds automatically:
- A successful experiment is one where the human has to intervene at the start but then reduces the amount of interventions as the policy improves. You can monitor the intervention rate in the `wandb` dashboard.
-
+
-
Example showing how human interventions help guide policy learning over time
+
+
+ Example showing how human interventions help guide policy learning over time
+
+
- The figure shows the plot of the episodic reward over interaction step. The figure shows the effect of human interventions on the policy learning.
- The orange curve is an experiment without any human interventions. While the pink and blue curves are experiments with human interventions.
@@ -510,7 +551,9 @@ The training proceeds automatically:
If you have `wandb.enable` set to `true` in your configuration, you can monitor training progress in real-time through the [Weights & Biases](https://wandb.ai/site/) dashboard.
### Guide to Human Interventions
+
The learning process is very sensitive to the intervention strategy. It will takes a few runs to understand how to intervene effectively. Some tips and hints:
+
- Allow the policy to explore for a few episodes at the start of training.
- Avoid intervening for long periods of time. Try to intervene in situation to correct the robot's behaviour when it goes off track.
- Once the policy starts achieving the task, even if its not perfect, you can limit your interventions to simple quick actions like a simple grasping commands.
@@ -518,26 +561,36 @@ The learning process is very sensitive to the intervention strategy. It will tak
The ideal behaviour is that your intervention rate should drop gradually during training as shown in the figure below.
-
+
-
Plot of the intervention rate during a training run on a pick and lift cube task
+
+
+ Plot of the intervention rate during a training run on a pick and lift cube
+ task
+
+
### Key hyperparameters to tune
Some configuration values have a disproportionate impact on training stability and speed:
- **`temperature_init`** (`policy.temperature_init`) – initial entropy temperature in SAC. Higher values encourage more exploration; lower values make the policy more deterministic early on. A good starting point is `1e-2`. We observed that setting it too high can make human interventions ineffective and slow down learning.
-- **`policy_parameters_push_frequency`** (`policy.actor_learner_config.policy_parameters_push_frequency`) – interval in *seconds* between two weight pushes from the learner to the actor. The default is `4 s`. Decrease to **1-2 s** to provide fresher weights (at the cost of more network traffic); increase only if your connection is slow, as this will reduce sample efficiency.
+- **`policy_parameters_push_frequency`** (`policy.actor_learner_config.policy_parameters_push_frequency`) – interval in _seconds_ between two weight pushes from the learner to the actor. The default is `4 s`. Decrease to **1-2 s** to provide fresher weights (at the cost of more network traffic); increase only if your connection is slow, as this will reduce sample efficiency.
- **`storage_device`** (`policy.storage_device`) – device on which the learner keeps the policy parameters. If you have spare GPU memory, set this to `"cuda"` (instead of the default `"cpu"`). Keeping the weights on-GPU removes CPU→GPU transfer overhead and can significantly increase the number of learner updates per second.
-
Congrats 🎉, you have finished this tutorial!
> [!TIP]
-> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
+> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
Paper citation:
+
```
@article{luo2024precise,
title={Precise and Dexterous Robotic Manipulation via Human-in-the-Loop Reinforcement Learning},
diff --git a/docs/source/hilserl_sim.mdx b/docs/source/hilserl_sim.mdx
index ad7a9584ae..c739be835c 100644
--- a/docs/source/hilserl_sim.mdx
+++ b/docs/source/hilserl_sim.mdx
@@ -11,7 +11,6 @@ This guide explains how to use the `gym_hil` simulation environments as an alter
Currently, the main environment is a Franka Panda robot simulation based on MuJoCo, with tasks like picking up a cube.
-
## Installation
First, install the `gym_hil` package within the LeRobot environment:
@@ -25,8 +24,6 @@ pip install -e ".[hilserl]"
- A gamepad or keyboard to control the robot
- A Nvidia GPU
-
-
## Configuration
To use `gym_hil` with LeRobot, you need to create a configuration file. An example is provided [here](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/blob/main/gym_hil_env.json). Key configuration sections include:
@@ -35,14 +32,15 @@ To use `gym_hil` with LeRobot, you need to create a configuration file. An examp
```json
{
- "type": "hil",
- "name": "franka_sim",
- "task": "PandaPickCubeGamepad-v0",
- "device": "cuda"
+ "type": "hil",
+ "name": "franka_sim",
+ "task": "PandaPickCubeGamepad-v0",
+ "device": "cuda"
}
```
Available tasks:
+
- `PandaPickCubeBase-v0`: Basic environment
- `PandaPickCubeGamepad-v0`: With gamepad control
- `PandaPickCubeKeyboard-v0`: With keyboard control
@@ -65,6 +63,7 @@ Available tasks:
```
Important parameters:
+
- `gripper_penalty`: Penalty for excessive gripper movement
- `use_gripper`: Whether to enable gripper control
- `end_effector_step_sizes`: Size of the steps in the x,y,z axes of the end-effector
@@ -76,40 +75,49 @@ Important parameters:
To run the environment, set mode to null:
+
```python
python -m lerobot.scripts.rl.gym_manipulator --config_path path/to/gym_hil_env.json
```
+
### Recording a Dataset
To collect a dataset, set the mode to `record` whilst defining the repo_id and number of episodes to record:
+
```python
python -m lerobot.scripts.rl.gym_manipulator --config_path path/to/gym_hil_env.json
```
+
### Training a Policy
To train a policy, checkout the configuration example available [here](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/blob/main/train_gym_hil_env.json) and run the actor and learner servers:
+
```python
python -m lerobot.scripts.rl.actor --config_path path/to/train_gym_hil_env.json
```
+
In a different terminal, run the learner server:
+
```python
python -m lerobot.scripts.rl.learner --config_path path/to/train_gym_hil_env.json
```
+
The simulation environment provides a safe and repeatable way to develop and test your Human-In-the-Loop reinforcement learning components before deploying to real robots.
Congrats 🎉, you have finished this tutorial!
> [!TIP]
-> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
+> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
Paper citation:
+
```
@article{luo2024precise,
title={Precise and Dexterous Robotic Manipulation via Human-in-the-Loop Reinforcement Learning},
diff --git a/docs/source/il_robots.mdx b/docs/source/il_robots.mdx
index 2e8ac36193..b18adb8f49 100644
--- a/docs/source/il_robots.mdx
+++ b/docs/source/il_robots.mdx
@@ -3,6 +3,7 @@
This tutorial will explain how to train a neural network to control a real robot autonomously.
**You'll learn:**
+
1. How to record and visualize your dataset.
2. How to train a policy using your data and prepare it for evaluation.
3. How to evaluate your policy and visualize the results.
@@ -14,7 +15,10 @@ By following these steps, you'll be able to replicate tasks, such as picking up
@@ -51,6 +55,8 @@ python -m lerobot.teleoperate \
```
+
+
```python
from lerobot.teleoperators.so101_leader import SO101LeaderConfig, SO101Leader
from lerobot.robots.so101_follower import SO101FollowerConfig, SO101Follower
@@ -74,10 +80,13 @@ while True:
action = teleop_device.get_action()
robot.send_action(action)
```
+
+
The teleoperate command will automatically:
+
1. Identify any missing calibrations and initiate the calibration procedure.
2. Connect the robot and teleop device and start teleoperation.
@@ -104,6 +113,8 @@ python -m lerobot.teleoperate \
```
+
+
```python
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig
from lerobot.teleoperators.koch_leader import KochLeaderConfig, KochLeader
@@ -134,6 +145,8 @@ while True:
action = teleop_device.get_action()
robot.send_action(action)
```
+
+
@@ -144,11 +157,13 @@ Once you're familiar with teleoperation, you can record your first dataset.
We use the Hugging Face hub features for uploading your dataset. If you haven't previously used the Hub, make sure you can login via the cli using a write-access token, this token can be generated from the [Hugging Face settings](https://huggingface.co/settings/tokens).
Add your token to the CLI by running this command:
+
```bash
huggingface-cli login --token ${HUGGINGFACE_TOKEN} --add-to-git-credential
```
Then store your Hugging Face repository name in a variable:
+
```bash
HF_USER=$(huggingface-cli whoami | head -n 1)
echo $HF_USER
@@ -174,6 +189,8 @@ python -m lerobot.record \
```
+
+
```python
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig
from lerobot.datasets.lerobot_dataset import LeRobotDataset
@@ -270,40 +287,49 @@ robot.disconnect()
teleop.disconnect()
dataset.push_to_hub()
```
+
+
#### Dataset upload
+
Locally, your dataset is stored in this folder: `~/.cache/huggingface/lerobot/{repo-id}`. At the end of data recording, your dataset will be uploaded on your Hugging Face page (e.g. https://huggingface.co/datasets/cadene/so101_test) that you can obtain by running:
+
```bash
echo https://huggingface.co/datasets/${HF_USER}/so101_test
```
+
Your dataset will be automatically tagged with `LeRobot` for the community to find it easily, and you can also add custom tags (in this case `tutorial` for example).
You can look for other LeRobot datasets on the hub by searching for `LeRobot` [tags](https://huggingface.co/datasets?other=LeRobot).
You can also push your local dataset to the Hub manually, running:
+
```bash
huggingface-cli upload ${HF_USER}/record-test ~/.cache/huggingface/lerobot/{repo-id} --repo-type dataset
```
-
#### Record function
The `record` function provides a suite of tools for capturing and managing data during robot operation:
##### 1. Data Storage
+
- Data is stored using the `LeRobotDataset` format and is stored on disk during recording.
- By default, the dataset is pushed to your Hugging Face page after recording.
- To disable uploading, use `--dataset.push_to_hub=False`.
##### 2. Checkpointing and Resuming
+
- Checkpoints are automatically created during recording.
- If an issue occurs, you can resume by re-running the same command with `--resume=true`.
- To start recording from scratch, **manually delete** the dataset directory.
##### 3. Recording Parameters
+
Set the flow of data recording using command-line arguments:
+
- `--dataset.episode_time_s=60`
Duration of each data recording episode (default: **60 seconds**).
- `--dataset.reset_time_s=60`
@@ -312,7 +338,9 @@ Set the flow of data recording using command-line arguments:
Total number of episodes to record (default: **50**).
##### 4. Keyboard Controls During Recording
+
Control the data recording flow using keyboard shortcuts:
+
- Press **Right Arrow (`→`)**: Early stop the current episode or reset time and move to the next.
- Press **Left Arrow (`←`)**: Cancel the current episode and re-record it.
- Press **Escape (`ESC`)**: Immediately stop the session, encode videos, and upload the dataset.
@@ -327,13 +355,14 @@ Avoid adding too much variation too quickly, as it may hinder your results.
If you want to dive deeper into this important topic, you can check out the [blog post](https://huggingface.co/blog/lerobot-datasets#what-makes-a-good-dataset) we wrote on what makes a good dataset.
-
#### Troubleshooting:
+
- On Linux, if the left and right arrow keys and escape key don't have any effect during data recording, make sure you've set the `$DISPLAY` environment variable. See [pynput limitations](https://pynput.readthedocs.io/en/latest/limitations.html#linux).
## Visualize a dataset
If you uploaded your dataset to the hub with `--control.push_to_hub=true`, you can [visualize your dataset online](https://huggingface.co/spaces/lerobot/visualize_dataset) by copy pasting your repo id given by:
+
```bash
echo ${HF_USER}/so101_test
```
@@ -356,6 +385,8 @@ python -m lerobot.replay \
```
+
+
```python
import time
@@ -388,6 +419,8 @@ for idx in range(dataset.num_frames):
robot.disconnect()
```
+
+
@@ -396,6 +429,7 @@ Your robot should replicate movements similar to those you recorded. For example
## Train a policy
To train a policy to control your robot, use the [`python -m lerobot.scripts.train`](../src/lerobot/scripts/train.py) script. A few arguments are required. Here is an example command:
+
```bash
python -m lerobot.scripts.train \
--dataset.repo_id=${HF_USER}/so101_test \
@@ -408,14 +442,16 @@ python -m lerobot.scripts.train \
```
Let's explain the command:
+
1. We provided the dataset as argument with `--dataset.repo_id=${HF_USER}/so101_test`.
2. We provided the policy with `policy.type=act`. This loads configurations from [`configuration_act.py`](../src/lerobot/policies/act/configuration_act.py). Importantly, this policy will automatically adapt to the number of motor states, motor actions and cameras of your robot (e.g. `laptop` and `phone`) which have been saved in your dataset.
-4. We provided `policy.device=cuda` since we are training on a Nvidia GPU, but you could use `policy.device=mps` to train on Apple silicon.
-5. We provided `wandb.enable=true` to use [Weights and Biases](https://docs.wandb.ai/quickstart) for visualizing training plots. This is optional but if you use it, make sure you are logged in by running `wandb login`.
+3. We provided `policy.device=cuda` since we are training on a Nvidia GPU, but you could use `policy.device=mps` to train on Apple silicon.
+4. We provided `wandb.enable=true` to use [Weights and Biases](https://docs.wandb.ai/quickstart) for visualizing training plots. This is optional but if you use it, make sure you are logged in by running `wandb login`.
Training should take several hours. You will find checkpoints in `outputs/train/act_so101_test/checkpoints`.
To resume training from a checkpoint, below is an example command to resume from `last` checkpoint of the `act_so101_test` policy:
+
```bash
python -m lerobot.scripts.train \
--config_path=outputs/train/act_so101_test/checkpoints/last/pretrained_model/train_config.json \
@@ -427,17 +463,20 @@ If you do not want to push your model to the hub after training use `--policy.pu
Additionally you can provide extra `tags` or specify a `license` for your model or make the model repo `private` by adding this: `--policy.private=true --policy.tags=\[ppo,rl\] --policy.license=mit`
#### Train using Collab
+
If your local computer doesn't have a powerful GPU you could utilize Google Collab to train your model by following the [ACT training notebook](./notebooks#training-act).
#### Upload policy checkpoints
Once training is done, upload the latest checkpoint with:
+
```bash
huggingface-cli upload ${HF_USER}/act_so101_test \
outputs/train/act_so101_test/checkpoints/last/pretrained_model
```
You can also upload intermediate checkpoints with:
+
```bash
CKPT=010000
huggingface-cli upload ${HF_USER}/act_so101_test${CKPT} \
@@ -467,6 +506,8 @@ python -m lerobot.record \
```
+
+
```python
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig
from lerobot.datasets.lerobot_dataset import LeRobotDataset
@@ -539,9 +580,12 @@ for episode_idx in range(NUM_EPISODES):
robot.disconnect()
dataset.push_to_hub()
```
+
+
As you can see, it's almost the same command as previously used to record your training dataset. Two things changed:
-1. There is an additional `--control.policy.path` argument which indicates the path to your policy checkpoint with (e.g. `outputs/train/eval_act_so101_test/checkpoints/last/pretrained_model`). You can also use the model repository if you uploaded a model checkpoint to the hub (e.g. `${HF_USER}/act_so101_test`).
+
+1. There is an additional `--control.policy.path` argument which indicates the path to your policy checkpoint with (e.g. `outputs/train/eval_act_so101_test/checkpoints/last/pretrained_model`). You can also use the model repository if you uploaded a model checkpoint to the hub (e.g. `${HF_USER}/act_so101_test`).
2. The name of dataset begins by `eval` to reflect that you are running inference (e.g. `${HF_USER}/eval_act_so101_test`).
diff --git a/docs/source/il_sim.mdx b/docs/source/il_sim.mdx
index 048d3147e9..193b09b1bf 100644
--- a/docs/source/il_sim.mdx
+++ b/docs/source/il_sim.mdx
@@ -3,6 +3,7 @@
This tutorial will explain how to train a neural network to control a robot in simulation with imitation learning.
**You'll learn:**
+
1. How to record a dataset in simulation with [gym-hil](https://github.com/huggingface/gym-hil) and visualize the dataset.
2. How to train a policy using your data.
3. How to evaluate your policy in simulation and visualize the results.
@@ -55,13 +56,21 @@ Note that to teleoperate the robot you have to hold the "Human Take Over Pause P
**Gamepad Controls**
-
+
+
+
+ Gamepad button mapping for robot control and episode management
-
Gamepad button mapping for robot control and episode management
**Keyboard controls**
For keyboard controls use the `spacebar` to enable control and the following keys to move the robot:
+
```bash
Arrow keys: Move in X-Y plane
Shift and Shift_R: Move in Z axis
@@ -74,14 +83,21 @@ For keyboard controls use the `spacebar` to enable control and the following key
If you uploaded your dataset to the hub you can [visualize your dataset online](https://huggingface.co/spaces/lerobot/visualize_dataset) by copy pasting your repo id.
-
+
+
+
+ Dataset visualizer
-
Dataset visualizer
-
## Train a policy
To train a policy to control your robot, use the [`python -m lerobot.scripts.train`](../src/lerobot/scripts/train.py) script. A few arguments are required. Here is an example command:
+
```bash
python -m lerobot.scripts.train \
--dataset.repo_id=${HF_USER}/il_gym \
@@ -93,25 +109,29 @@ python -m lerobot.scripts.train \
```
Let's explain the command:
+
1. We provided the dataset as argument with `--dataset.repo_id=${HF_USER}/il_gym`.
2. We provided the policy with `policy.type=act`. This loads configurations from [`configuration_act.py`](../src/lerobot/policies/act/configuration_act.py). Importantly, this policy will automatically adapt to the number of motor states, motor actions and cameras of your robot (e.g. `laptop` and `phone`) which have been saved in your dataset.
-4. We provided `policy.device=cuda` since we are training on a Nvidia GPU, but you could use `policy.device=mps` to train on Apple silicon.
-5. We provided `wandb.enable=true` to use [Weights and Biases](https://docs.wandb.ai/quickstart) for visualizing training plots. This is optional but if you use it, make sure you are logged in by running `wandb login`.
+3. We provided `policy.device=cuda` since we are training on a Nvidia GPU, but you could use `policy.device=mps` to train on Apple silicon.
+4. We provided `wandb.enable=true` to use [Weights and Biases](https://docs.wandb.ai/quickstart) for visualizing training plots. This is optional but if you use it, make sure you are logged in by running `wandb login`.
Training should take several hours, 100k steps (which is the default) will take about 1h on Nvidia A100. You will find checkpoints in `outputs/train/il_sim_test/checkpoints`.
#### Train using Collab
+
If your local computer doesn't have a powerful GPU you could utilize Google Collab to train your model by following the [ACT training notebook](./notebooks#training-act).
#### Upload policy checkpoints
Once training is done, upload the latest checkpoint with:
+
```bash
huggingface-cli upload ${HF_USER}/il_sim_test \
outputs/train/il_sim_test/checkpoints/last/pretrained_model
```
You can also upload intermediate checkpoints with:
+
```bash
CKPT=010000
huggingface-cli upload ${HF_USER}/il_sim_test${CKPT} \
@@ -144,9 +164,9 @@ mjpython -m lerobot.scripts.rl.eval_policy --config_path=path/to/eval_config_gym
> [!WARNING]
-> While the main workflow of training ACT in simulation is straightforward, there is significant room for exploring how to set up the task, define the initial state of the environment, and determine the type of data required during collection to learn the most effective policy. If your trained policy doesn't perform well, investigate the quality of the dataset it was trained on using our visualizers, as well as the action values and various hyperparameters related to ACT and the simulation.
+> While the main workflow of training ACT in simulation is straightforward, there is significant room for exploring how to set up the task, define the initial state of the environment, and determine the type of data required during collection to learn the most effective policy. If your trained policy doesn't perform well, investigate the quality of the dataset it was trained on using our visualizers, as well as the action values and various hyperparameters related to ACT and the simulation.
Congrats 🎉, you have finished this tutorial. If you want to continue with using LeRobot in simulation follow this [Tutorial on reinforcement learning in sim with HIL-SERL](https://huggingface.co/docs/lerobot/hilserl_sim)
> [!TIP]
-> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
+> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
diff --git a/docs/source/index.mdx b/docs/source/index.mdx
index b8ff56ea77..a2f919e7d8 100644
--- a/docs/source/index.mdx
+++ b/docs/source/index.mdx
@@ -1,6 +1,10 @@
diff --git a/docs/source/installation.mdx b/docs/source/installation.mdx
index 51474d8f7a..13c3600b4f 100644
--- a/docs/source/installation.mdx
+++ b/docs/source/installation.mdx
@@ -5,45 +5,56 @@
Currently only available from source.
Download our source code:
+
```bash
git clone https://github.com/huggingface/lerobot.git
cd lerobot
```
Create a virtual environment with Python 3.10, using [`Miniconda`](https://docs.anaconda.com/miniconda/install/#quick-command-line-install)
+
```bash
conda create -y -n lerobot python=3.10
```
Then activate your conda environment, you have to do this each time you open a shell to use lerobot:
+
```bash
conda activate lerobot
```
When using `miniconda`, install `ffmpeg` in your environment:
+
```bash
conda install ffmpeg -c conda-forge
```
> [!TIP]
> This usually installs `ffmpeg 7.X` for your platform compiled with the `libsvtav1` encoder. If `libsvtav1` is not supported (check supported encoders with `ffmpeg -encoders`), you can:
-> - _[On any platform]_ Explicitly install `ffmpeg 7.X` using:
-> ```bash
-> conda install ffmpeg=7.1.1 -c conda-forge
-> ```
-> - _[On Linux only]_ If you want to bring your own ffmpeg: Install [ffmpeg build dependencies](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#GettheDependencies) and [compile ffmpeg from source with libsvtav1](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#libsvtav1), and make sure you use the corresponding ffmpeg binary to your install with `which ffmpeg`.
+>
+> - _[On any platform]_ Explicitly install `ffmpeg 7.X` using:
+>
+> ```bash
+> conda install ffmpeg=7.1.1 -c conda-forge
+> ```
+>
+> - _[On Linux only]_ If you want to bring your own ffmpeg: Install [ffmpeg build dependencies](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#GettheDependencies) and [compile ffmpeg from source with libsvtav1](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#libsvtav1), and make sure you use the corresponding ffmpeg binary to your install with `which ffmpeg`.
Install 🤗 LeRobot:
+
```bash
pip install -e .
```
### Troubleshooting
+
If you encounter build errors, you may need to install additional dependencies: `cmake`, `build-essential`, and `ffmpeg libs`.
To install these for linux run:
+
```bash
sudo apt-get install cmake build-essential python-dev pkg-config libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev pkg-config
```
+
For other systems, see: [Compiling PyAV](https://pyav.org/docs/develop/overview/installation.html#bring-your-own-ffmpeg)
## Optional dependencies
@@ -51,20 +62,26 @@ For other systems, see: [Compiling PyAV](https://pyav.org/docs/develop/overview/
LeRobot provides optional extras for specific functionalities. Multiple extras can be combined (e.g., `.[aloha,feetech]`). For all available extras, refer to `pyproject.toml`.
### Simulations
+
Install environment packages: `aloha` ([gym-aloha](https://github.com/huggingface/gym-aloha)), `xarm` ([gym-xarm](https://github.com/huggingface/gym-xarm)), or `pusht` ([gym-pusht](https://github.com/huggingface/gym-pusht))
Example:
+
```bash
pip install -e ".[aloha]" # or "[pusht]" for example
```
### Motor Control
+
For Koch v1.1 install the Dynamixel SDK, for SO100/SO101/Moss install the Feetech SDK.
+
```bash
pip install -e ".[feetech]" # or "[dynamixel]" for example
```
### Experiment Tracking
+
To use [Weights and Biases](https://docs.wandb.ai/quickstart) for experiment tracking, log in with
+
```bash
wandb login
```
diff --git a/docs/source/integrate_hardware.mdx b/docs/source/integrate_hardware.mdx
index 18d73d3cdc..089126fcb1 100644
--- a/docs/source/integrate_hardware.mdx
+++ b/docs/source/integrate_hardware.mdx
@@ -21,16 +21,13 @@ Please refer to the [`MotorsBus`](https://github.com/huggingface/lerobot/blob/ma
For a good example of how it can be used, you can have a look at our own [SO101 follower implementation](https://github.com/huggingface/lerobot/blob/main/lerobot/robots/so101_follower/so101_follower.py)
Use these if compatible. Otherwise, you'll need to find or write a Python interface (not covered in this tutorial):
+
- Find an existing SDK in Python (or use bindings to C/C++)
- Or implement a basic communication wrapper (e.g., via pyserial, socket, or CANopen)
You're not alone—many community contributions use custom boards or firmware!
-For Feetech and Dynamixel, we currently support these servos:
- - Feetech:
- - STS & SMS series (protocol 0): `sts3215`, `sts3250`, `sm8512bl`
- - SCS series (protocol 1): `scs0009`
- - Dynamixel (protocol 2.0 only): `xl330-m077`, `xl330-m288`, `xl430-w250`, `xm430-w350`, `xm540-w270`, `xc430-w150`
+For Feetech and Dynamixel, we currently support these servos: - Feetech: - STS & SMS series (protocol 0): `sts3215`, `sts3250`, `sm8512bl` - SCS series (protocol 1): `scs0009` - Dynamixel (protocol 2.0 only): `xl330-m077`, `xl330-m288`, `xl430-w250`, `xm430-w350`, `xm540-w270`, `xc430-w150`
If you are using Feetech or Dynamixel servos that are not in this list, you can add those in the [Feetech table](https://github.com/huggingface/lerobot/blob/main/lerobot/motors/feetech/tables.py) or [Dynamixel table](https://github.com/huggingface/lerobot/blob/main/lerobot/motors/dynamixel/tables.py). Depending on the model, this will require you to add model-specific information. In most cases though, there shouldn't be a lot of additions to do.
@@ -41,6 +38,8 @@ In the next sections, we'll use a `FeetechMotorsBus` as the motors interface for
You’ll first need to specify the config class and a string identifier (`name`) for your robot. If your robot has special needs that you'd like to be able to change easily, it should go here (e.g. port/address, baudrate).
Here, we'll add the port name and one camera by default for our robot:
+
+
```python
from dataclasses import dataclass, field
@@ -64,6 +63,7 @@ class MyCoolRobotConfig(RobotConfig):
}
)
```
+
Have a look at our [Cameras tutorial](./cameras) to understand how to detect and add your camera.
@@ -71,6 +71,7 @@ Next, we'll create our actual robot class which inherits from `Robot`. This abst
Here we'll create a simple 5-DoF robot with one camera. It could be a simple arm but notice that the `Robot` abstract class does not assume anything on your robot's form factor. You can let you imagination run wild when designing new robots!
+
```python
from lerobot.cameras import make_cameras_from_configs
from lerobot.motors import Motor, MotorNormMode
@@ -96,10 +97,11 @@ class MyCoolRobot(Robot):
)
self.cameras = make_cameras_from_configs(config.cameras)
```
+
## Step 2: Define Observation and Action Features
-These two properties define the *interface contract* between your robot and tools that consume it (such as data collection or learning pipelines).
+These two properties define the _interface contract_ between your robot and tools that consume it (such as data collection or learning pipelines).
> [!WARNING]
> Note that these properties must be callable even if the robot is not yet connected, so avoid relying on runtime hardware state to define them.
@@ -109,6 +111,8 @@ These two properties define the *interface contract* between your robot and tool
This property should return a dictionary describing the structure of sensor outputs from your robot. The keys match what `get_observation()` returns, and the values describe either the shape (for arrays/images) or the type (for simple values).
Example for our 5-DoF arm with one camera:
+
+
```python
@property
def _motors_ft(self) -> dict[str, type]:
@@ -130,6 +134,8 @@ def _cameras_ft(self) -> dict[str, tuple]:
def observation_features(self) -> dict:
return {**self._motors_ft, **self._cameras_ft}
```
+
+
In this case, observations consist of a simple dict storing each motor's position and a camera image.
### `action_features`
@@ -137,10 +143,13 @@ In this case, observations consist of a simple dict storing each motor's positio
This property describes the commands your robot expects via `send_action()`. Again, keys must match the expected input format, and values define the shape/type of each command.
Here, we simply use the same joints proprioceptive features (`self._motors_ft`) as with `observation_features`: the action sent will simply the goal position for each motor.
+
+
```python
def action_features(self) -> dict:
return self._motors_ft
```
+
## Step 3: Handle Connection and Disconnection
@@ -150,16 +159,19 @@ These methods should handle opening and closing communication with your hardware
This property should simply reflect that communication with the robot's hardware is established. When this property is `True`, it should be possible to read and write to the hardware using `get_observation()` and `send_action()`.
+
```python
@property
def is_connected(self) -> bool:
return self.bus.is_connected and all(cam.is_connected for cam in self.cameras.values())
```
+
### `connect()`
This method should establish communication with the hardware. Moreover, if your robot needs calibration and is not calibrated, it should start a calibration procedure by default. If your robot needs some specific configuration, this should also be called here.
+
```python
def connect(self, calibrate: bool = True) -> None:
self.bus.connect()
@@ -171,25 +183,31 @@ def connect(self, calibrate: bool = True) -> None:
self.configure()
```
+
### `disconnect()`
This method should gracefully terminate communication with the hardware: free any related resources (threads or processes), close ports, etc.
Here, we already handle this in our `MotorsBus` and `Camera` classes so we just need to call their own `disconnect()` methods:
+
+
```python
def disconnect(self) -> None:
self.bus.disconnect()
for cam in self.cameras.values():
cam.disconnect()
```
+
## Step 4: Support Calibration and Configuration
LeRobot supports saving and loading calibration data automatically. This is useful for joint offsets, zero positions, or sensor alignment.
> Note that depending on your hardware, this may not apply. If that's the case, you can simply leave these methods as no-ops:
-> ```python
+
+
+```python
> @property
> def is_calibrated(self) -> bool:
> return True
@@ -202,7 +220,8 @@ LeRobot supports saving and loading calibration data automatically. This is usef
This should reflect whether your robot has the required calibration loaded.
-```python
+```
+python
@property
def is_calibrated(self) -> bool:
return self.bus.is_calibrated
@@ -216,6 +235,8 @@ The goal of the calibration is twofold:
It should implement the logic for calibration (if relevant) and update the `self.calibration` dictionary. If you are using Feetech or Dynamixel motors, our bus interfaces already include methods to help with this.
+
+
```python
def calibrate(self) -> None:
self.bus.disable_torque()
@@ -245,11 +266,13 @@ def calibrate(self) -> None:
self._save_calibration()
print("Calibration saved to", self.calibration_fpath)
```
+
### `configure()`
Use this to set up any configuration for your hardware (servos control modes, controller gains, etc.). This should usually be run at connection time and be idempotent.
+
```python
def configure(self) -> None:
with self.bus.torque_disabled():
@@ -260,6 +283,7 @@ def configure(self) -> None:
self.bus.write("I_Coefficient", motor, 0)
self.bus.write("D_Coefficient", motor, 32)
```
+
## Step 5: Implement Sensors Reading and Action Sending
@@ -269,6 +293,7 @@ These are the most important runtime functions: the core I/O loop.
Returns a dictionary of sensor values from the robot. These typically include motor states, camera frames, various sensors, etc. In the LeRobot framework, these observations are what will be fed to a policy in order to predict the actions to take. The dictionary keys and structure must match `observation_features`.
+
```python
def get_observation(self) -> dict[str, Any]:
if not self.is_connected:
@@ -284,6 +309,7 @@ def get_observation(self) -> dict[str, Any]:
return obs_dict
```
+
### `send_action()`
@@ -291,6 +317,7 @@ Takes a dictionary that matches `action_features`, and sends it to your hardware
For simplicity, we won't be adding any modification of the actions in our example here.
+
```python
def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
goal_pos = {key.removesuffix(".pos"): val for key, val in action.items()}
@@ -300,6 +327,7 @@ def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
return action
```
+
## Adding a Teleoperator
diff --git a/docs/source/notebooks.mdx b/docs/source/notebooks.mdx
index 729b31a99d..6a9c3b103c 100644
--- a/docs/source/notebooks.mdx
+++ b/docs/source/notebooks.mdx
@@ -10,8 +10,8 @@ This repository contains example notebooks for using LeRobot. These notebooks de
We provide a ready-to-run Google Colab notebook to help you train ACT policies using datasets from the Hugging Face Hub, with optional logging to Weights & Biases.
-| Notebook | Colab |
-|:---------|:------|
+| Notebook | Colab |
+| :------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Train ACT with LeRobot](https://github.com/huggingface/notebooks/blob/main/lerobot/training-act.ipynb) | [](https://colab.research.google.com/github/huggingface/notebooks/blob/main/lerobot/training-act.ipynb) |
Expected training time for 100k steps: ~1.5 hours on an NVIDIA A100 GPU with batch size of `64`.
diff --git a/docs/source/smolvla.mdx b/docs/source/smolvla.mdx
index 17a2bdf18b..880beaa1ac 100644
--- a/docs/source/smolvla.mdx
+++ b/docs/source/smolvla.mdx
@@ -3,9 +3,18 @@
SmolVLA is Hugging Face’s lightweight foundation model for robotics. Designed for easy fine-tuning on LeRobot datasets, it helps accelerate your development!
-
-
- Figure 1. SmolVLA takes as input (i) multiple cameras views, (ii) the robot’s current sensorimotor state, and (iii) a natural language instruction, encoded into contextual features used to condition the action expert when generating an action chunk.
+
+
+
+ Figure 1. SmolVLA takes as input (i) multiple cameras views, (ii) the
+ robot’s current sensorimotor state, and (iii) a natural language
+ instruction, encoded into contextual features used to condition the action
+ expert when generating an action chunk.
+
## Set Up Your Environment
@@ -32,6 +41,7 @@ We recommend checking out the dataset linked below for reference that was used i
In this dataset, we recorded 50 episodes across 5 distinct cube positions. For each position, we collected 10 episodes of pick-and-place interactions. This structure, repeating each variation several times, helped the model generalize better. We tried similar dataset with 25 episodes, and it was not enough leading to a bad performance. So, the data quality and quantity is definitely a key.
After you have your dataset available on the Hub, you are good to go to use our finetuning script to adapt SmolVLA to your application.
+
## Finetune SmolVLA on your data
@@ -56,7 +66,8 @@ cd lerobot && python -m lerobot.scripts.train \
```
-You can start with a small batch size and increase it incrementally, if the GPU allows it, as long as loading times remain short.
+ You can start with a small batch size and increase it incrementally, if the
+ GPU allows it, as long as loading times remain short.
Fine-tuning is an art. For a complete overview of the options for finetuning, run
@@ -66,12 +77,20 @@ python -m lerobot.scripts.train --help
```
-
-
- Figure 2: Comparison of SmolVLA across task variations. From left to right: (1) pick-place cube counting, (2) pick-place cube counting, (3) pick-place cube counting under perturbations, and (4) generalization on pick-and-place of the lego block with real-world SO101.
+
+
+
+ Figure 2: Comparison of SmolVLA across task variations. From left to right:
+ (1) pick-place cube counting, (2) pick-place cube counting, (3) pick-place
+ cube counting under perturbations, and (4) generalization on pick-and-place
+ of the lego block with real-world SO101.
+
-
## Evaluate the finetuned model and run it in real-time
Similarly for when recording an episode, it is recommended that you are logged in to the HuggingFace Hub. You can follow the corresponding steps: [Record a dataset](./getting_started_real_world_robot#record-a-dataset).
diff --git a/examples/4_train_policy_with_script.md b/examples/4_train_policy_with_script.md
index f17411b75e..d6cd6cc23a 100644
--- a/examples/4_train_policy_with_script.md
+++ b/examples/4_train_policy_with_script.md
@@ -1,6 +1,6 @@
This tutorial will explain the training script, how to use it, and particularly how to configure everything needed for the training run.
-> **Note:** The following assumes you're running these commands on a machine equipped with a cuda GPU. If you don't have one (or if you're using a Mac), you can add `--policy.device=cpu` (`--policy.device=mps` respectively). However, be advised that the code executes much slower on cpu.
+> **Note:** The following assumes you're running these commands on a machine equipped with a cuda GPU. If you don't have one (or if you're using a Mac), you can add `--policy.device=cpu` (`--policy.device=mps` respectively). However, be advised that the code executes much slower on cpu.
## The training script
@@ -15,17 +15,22 @@ LeRobot offers a training script at [`lerobot/scripts/train.py`](../src/lerobot/
## Overview of the configuration system
In the training script, the main function `train` expects a `TrainPipelineConfig` object:
+
+
```python
# train.py
@parser.wrap()
def train(cfg: TrainPipelineConfig):
```
+
You can inspect the `TrainPipelineConfig` defined in [`lerobot/configs/train.py`](../src/lerobot/configs/train.py) (which is heavily commented and meant to be a reference to understand any option)
When running the script, inputs for the command line are parsed thanks to the `@parser.wrap()` decorator and an instance of this class is automatically generated. Under the hood, this is done with [Draccus](https://github.com/dlwh/draccus) which is a tool dedicated to this purpose. If you're familiar with Hydra, Draccus can similarly load configurations from config files (.json, .yaml) and also override their values through command line inputs. Unlike Hydra, these configurations are pre-defined in the code through dataclasses rather than being defined entirely in config files. This allows for more rigorous serialization/deserialization, typing, and to manipulate configuration as objects directly in the code and not as dictionaries or namespaces (which enables nice features in an IDE such as autocomplete, jump-to-def, etc.)
Let's have a look at a simplified example. Amongst other attributes, the training config has the following attributes:
+
+
```python
@dataclass
class TrainPipelineConfig:
@@ -33,7 +38,11 @@ class TrainPipelineConfig:
env: envs.EnvConfig | None = None
policy: PreTrainedConfig | None = None
```
+
+
in which `DatasetConfig` for example is defined as such:
+
+
```python
@dataclass
class DatasetConfig:
@@ -41,16 +50,17 @@ class DatasetConfig:
episodes: list[int] | None = None
video_backend: str = "pyav"
```
+
This creates a hierarchical relationship where, for example assuming we have a `cfg` instance of `TrainPipelineConfig`, we can access the `repo_id` value with `cfg.dataset.repo_id`.
From the command line, we can specify this value by using a very similar syntax `--dataset.repo_id=repo/id`.
By default, every field takes its default value specified in the dataclass. If a field doesn't have a default value, it needs to be specified either from the command line or from a config file – which path is also given in the command line (more in this below). In the example above, the `dataset` field doesn't have a default value which means it must be specified.
-
## Specifying values from the CLI
Let's say that we want to train [Diffusion Policy](../src/lerobot/policies/diffusion) on the [pusht](https://huggingface.co/datasets/lerobot/pusht) dataset, using the [gym_pusht](https://github.com/huggingface/gym-pusht) environment for evaluation. The command to do so would look like this:
+
```bash
python -m lerobot.scripts.train \
--dataset.repo_id=lerobot/pusht \
@@ -59,11 +69,13 @@ python -m lerobot.scripts.train \
```
Let's break this down:
+
- To specify the dataset, we just need to specify its `repo_id` on the hub which is the only required argument in the `DatasetConfig`. The rest of the fields have default values and in this case we are fine with those so we can just add the option `--dataset.repo_id=lerobot/pusht`.
- To specify the policy, we can just select diffusion policy using `--policy` appended with `.type`. Here, `.type` is a special argument which allows us to select config classes inheriting from `draccus.ChoiceRegistry` and that have been decorated with the `register_subclass()` method. To have a better explanation of this feature, have a look at this [Draccus demo](https://github.com/dlwh/draccus?tab=readme-ov-file#more-flexible-configuration-with-choice-types). In our code, we use this mechanism mainly to select policies, environments, robots, and some other components like optimizers. The policies available to select are located in [lerobot/policies](../src/lerobot/policies)
- Similarly, we select the environment with `--env.type=pusht`. The different environment configs are available in [`lerobot/envs/configs.py`](../src/lerobot/envs/configs.py)
Let's see another example. Let's say you've been training [ACT](../src/lerobot/policies/act) on [lerobot/aloha_sim_insertion_human](https://huggingface.co/datasets/lerobot/aloha_sim_insertion_human) using the [gym-aloha](https://github.com/huggingface/gym-aloha) environment for evaluation with:
+
```bash
python -m lerobot.scripts.train \
--policy.type=act \
@@ -71,10 +83,12 @@ python -m lerobot.scripts.train \
--env.type=aloha \
--output_dir=outputs/train/act_aloha_insertion
```
+
> Notice we added `--output_dir` to explicitly tell where to write outputs from this run (checkpoints, training state, configs etc.). This is not mandatory and if you don't specify it, a default directory will be created from the current date and time, env.type and policy.type. This will typically look like `outputs/train/2025-01-24/16-10-05_aloha_act`.
We now want to train a different policy for aloha on another task. We'll change the dataset and use [lerobot/aloha_sim_transfer_cube_human](https://huggingface.co/datasets/lerobot/aloha_sim_transfer_cube_human) instead. Of course, we also need to change the task of the environment as well to match this other task.
Looking at the [`AlohaEnv`](../src/lerobot/envs/configs.py) config, the task is `"AlohaInsertion-v0"` by default, which corresponds to the task we trained on in the command above. The [gym-aloha](https://github.com/huggingface/gym-aloha?tab=readme-ov-file#description) environment also has the `AlohaTransferCube-v0` task which corresponds to this other task we want to train on. Putting this together, we can train this new policy on this different task using:
+
```bash
python -m lerobot.scripts.train \
--policy.type=act \
@@ -87,6 +101,7 @@ python -m lerobot.scripts.train \
## Loading from a config file
Now, let's assume that we want to reproduce the run just above. That run has produced a `train_config.json` file in its checkpoints, which serializes the `TrainPipelineConfig` instance it used:
+
```json
{
"dataset": {
@@ -110,34 +125,40 @@ Now, let's assume that we want to reproduce the run just above. That run has pro
```
We can then simply load the config values from this file using:
+
```bash
python -m lerobot.scripts.train \
--config_path=outputs/train/act_aloha_transfer/checkpoints/last/pretrained_model/ \
--output_dir=outputs/train/act_aloha_transfer_2
```
+
`--config_path` is also a special argument which allows to initialize the config from a local config file. It can point to a directory that contains `train_config.json` or to the config file itself directly.
Similarly to Hydra, we can still override some parameters in the CLI if we want to, e.g.:
+
```bash
python -m lerobot.scripts.train \
--config_path=outputs/train/act_aloha_transfer/checkpoints/last/pretrained_model/ \
--output_dir=outputs/train/act_aloha_transfer_2
--policy.n_action_steps=80
```
+
> Note: While `--output_dir` is not required in general, in this case we need to specify it since it will otherwise take the value from the `train_config.json` (which is `outputs/train/act_aloha_transfer`). In order to prevent accidental deletion of previous run checkpoints, we raise an error if you're trying to write in an existing directory. This is not the case when resuming a run, which is what you'll learn next.
`--config_path` can also accept the repo_id of a repo on the hub that contains a `train_config.json` file, e.g. running:
+
```bash
python -m lerobot.scripts.train --config_path=lerobot/diffusion_pusht
```
-will start a training run with the same configuration used for training [lerobot/diffusion_pusht](https://huggingface.co/lerobot/diffusion_pusht)
+will start a training run with the same configuration used for training [lerobot/diffusion_pusht](https://huggingface.co/lerobot/diffusion_pusht)
## Resume training
Being able to resume a training run is important in case it crashed or aborted for any reason. We'll demonstrate how to do that here.
Let's reuse the command from the previous run and add a few more options:
+
```bash
python -m lerobot.scripts.train \
--policy.type=act \
@@ -150,19 +171,24 @@ python -m lerobot.scripts.train \
```
Here we've taken care to set up the log frequency and checkpointing frequency to low numbers so we can showcase resumption. You should be able to see some logging and have a first checkpoint within 1 minute (depending on hardware). Wait for the first checkpoint to happen, you should see a line that looks like this in your terminal:
+
```
INFO 2025-01-24 16:10:56 ts/train.py:263 Checkpoint policy after step 100
```
+
Now let's simulate a crash by killing the process (hit `ctrl`+`c`). We can then simply resume this run from the last checkpoint available with:
+
```bash
python -m lerobot.scripts.train \
--config_path=outputs/train/run_resumption/checkpoints/last/pretrained_model/ \
--resume=true
```
+
You should see from the logging that your training picks up from where it left off.
Another reason for which you might want to resume a run is simply to extend training and add more training steps. The number of training steps is set by the option `--steps`, which is 100 000 by default.
You could double the number of steps of the previous run with:
+
```bash
python -m lerobot.scripts.train \
--config_path=outputs/train/run_resumption/checkpoints/last/pretrained_model/ \
@@ -171,7 +197,9 @@ python -m lerobot.scripts.train \
```
## Outputs of a run
+
In the output directory, there will be a folder called `checkpoints` with the following structure:
+
```bash
outputs/train/run_resumption/checkpoints
├── 000100 # checkpoint_dir for training step 100
@@ -194,6 +222,7 @@ outputs/train/run_resumption/checkpoints
In addition to the features currently in Draccus, we've added a special `.path` argument for the policy, which allows to load a policy as you would with `PreTrainedPolicy.from_pretrained()`. In that case, `path` can be a local directory that contains a checkpoint or a repo_id pointing to a pretrained policy on the hub.
For example, we could fine-tune a [policy pre-trained on the aloha transfer task](https://huggingface.co/lerobot/act_aloha_sim_transfer_cube_human) on the aloha insertion task. We can achieve this with:
+
```bash
python -m lerobot.scripts.train \
--policy.path=lerobot/act_aloha_sim_transfer_cube_human \
@@ -209,15 +238,19 @@ When doing so, keep in mind that the features of the fine-tuning dataset would h
When you start the training process, you will first see your full configuration being printed in the terminal. You can check it to make sure that you configured your run correctly. The final configuration will also be saved with the checkpoint.
After that, you will see training log like this one:
+
```
INFO 2024-08-14 13:35:12 ts/train.py:192 step:0 smpl:64 ep:1 epch:0.00 loss:1.112 grdn:15.387 lr:2.0e-07 updt_s:1.738 data_s:4.774
```
+
or evaluation log:
+
```
INFO 2024-08-14 13:38:45 ts/train.py:226 step:100 smpl:6K ep:52 epch:0.25 ∑rwrd:20.693 success:0.0% eval_s:120.266
```
These logs will also be saved in wandb if `wandb.enable` is set to `true`. Here are the meaning of some abbreviations:
+
- `smpl`: number of samples seen during training.
- `ep`: number of episodes seen during training. An episode contains multiple samples in a complete manipulation task.
- `epch`: number of time all unique samples are seen (epoch).
@@ -235,6 +268,7 @@ Some metrics are useful for initial performance profiling. For example, if you f
We'll summarize here the main use cases to remember from this tutorial.
#### Train a policy from scratch – CLI
+
```bash
python -m lerobot.scripts.train \
--policy.type=act \ # <- select 'act' policy
@@ -243,6 +277,7 @@ python -m lerobot.scripts.train \
```
#### Train a policy from scratch - config file + CLI
+
```bash
python -m lerobot.scripts.train \
--config_path=path/to/pretrained_model \ # <- can also be a repo_id
@@ -250,6 +285,7 @@ python -m lerobot.scripts.train \
```
#### Resume/continue a training run
+
```bash
python -m lerobot.scripts.train \
--config_path=checkpoint/pretrained_model/ \
@@ -258,6 +294,7 @@ python -m lerobot.scripts.train \
```
#### Fine-tuning
+
```bash
python -m lerobot.scripts.train \
--policy.path=lerobot/act_aloha_sim_transfer_cube_human \ # <- can also be a local path to a checkpoint
diff --git a/pyproject.toml b/pyproject.toml
index 878a36dbe6..e9539037b0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,8 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
[project.urls]
-homepage = "https://github.com/huggingface/lerobot"
+homepage = "https://huggingface.co/lerobot"
+documentation = "https://huggingface.co/docs/lerobot/index"
+source = "https://github.com/huggingface/lerobot"
issues = "https://github.com/huggingface/lerobot/issues"
discord = "https://discord.gg/s3KuuzsPFb"
@@ -21,109 +27,165 @@ discord = "https://discord.gg/s3KuuzsPFb"
name = "lerobot"
version = "0.1.0"
description = "🤗 LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch"
+readme = "README.md"
+license = { text = "Apache-2.0" }
+requires-python = ">=3.10"
authors = [
{ name = "Rémi Cadène", email = "re.cadene@gmail.com" },
{ name = "Simon Alibert", email = "alibert.sim@gmail.com" },
{ name = "Alexander Soare", email = "alexander.soare159@gmail.com" },
{ name = "Quentin Gallouédec", email = "quentin.gallouedec@ec-lyon.fr" },
+ { name = "Steven Palma", email = "imstevenpmwork@ieee.org" },
+ { name = "Pepijn Kooijmans", email = "pepijnkooijmans@outlook.com"},
+ { name = "Michel Aractingi", email = "michel.aractingi@gmail.com"},
{ name = "Adil Zouitine", email = "adilzouitinegm@gmail.com" },
+ { name = "Dana Aubakirova", email = "danaaubakirova17@gmail.com"},
+ { name = "Caroline Pascal", email = "caroline8.pascal@gmail.com"},
+ { name = "Martino Russi", email = "nopyeps@gmail.com"},
{ name = "Thomas Wolf", email = "thomaswolfcontact@gmail.com" },
- { name = "Steven Palma", email = "imstevenpmwork@ieee.org" },
]
-readme = "README.md"
-license = { text = "Apache-2.0" }
-requires-python = ">=3.10"
-keywords = ["robotics", "deep learning", "pytorch"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Science/Research",
- "Topic :: Software Development :: Build Tools",
- "Topic :: Scientific/Engineering :: Artificial Intelligence",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.10",
+ "Topic :: Software Development :: Build Tools",
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
]
+keywords = ["lerobot", "huggingface", "robotics", "machine learning", "artificial intelligence"]
+
dependencies = [
- "cmake>=3.29.0.1",
- "datasets>=2.19.0,<=3.6.0",
- "deepdiff>=7.0.1",
+
+ # Hugging Face dependencies
+ "datasets>=2.19.0,<=3.6.0", # TODO: Bumb dependency
"diffusers>=0.27.2",
- "draccus==0.10.0",
+ "huggingface-hub[hf-transfer,cli]>=0.27.1",
+
+ # Core dependencies
+ "cmake>=3.29.0.1",
"einops>=0.8.0",
- "flask>=3.0.3",
- "gdown>=5.1.0",
- "gymnasium==0.29.1", # TODO(rcadene, aliberts): Make gym 1.0.0 work
- "h5py>=3.10.0",
- "huggingface-hub[hf-transfer,cli]>=0.27.1 ; python_version < '4.0'",
- "imageio[ffmpeg]>=2.34.0",
- "jsonlines>=4.0.0",
- "numba>=0.59.0",
- "omegaconf>=2.3.0",
"opencv-python-headless>=4.9.0",
- "packaging>=24.2",
"av>=14.2.0",
- "pymunk>=6.6.0,<7.0.0",
- "pynput>=1.7.7",
- "pyserial>=3.5",
- "pyzmq>=26.2.1",
- "rerun-sdk>=0.21.0",
- "termcolor>=2.4.0",
"torch>=2.2.1",
"torchcodec>=0.2.1; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')",
"torchvision>=0.21.0",
+ "jsonlines>=4.0.0",
+ "packaging>=24.2",
+ "pynput>=1.7.7",
+ "pyserial>=3.5",
"wandb>=0.16.3",
- "zarr>=2.17.0",
+
+ "draccus==0.10.0", # TODO: Remove ==
+ "gymnasium>=0.29.1,<1.0.0", # TODO: Bumb dependency
+ "rerun-sdk>=0.21.0,<0.23.0", # TODO: Bumb dependency
+
+ # Support dependencies
+ "deepdiff>=7.0.1,<9.0.0",
+ "flask>=3.0.3,<4.0.0",
+ "imageio[ffmpeg]>=2.34.0,<3.0.0",
+ "termcolor>=2.4.0,<4.0.0",
]
+# Optional dependencies
[project.optional-dependencies]
-aloha = ["gym-aloha>=0.1.1 ; python_version < '4.0'"]
-docs = ["hf-doc-builder @ git+https://github.com/huggingface/doc-builder.git@main", "watchdog >= 6.0.0"]
-dev = ["pre-commit>=3.7.0", "debugpy>=1.8.1", "grpcio-tools==1.71.0"]
-dora = [
- "gym-dora @ git+https://github.com/dora-rs/dora-lerobot.git#subdirectory=gym_dora ; python_version < '4.0'",
-]
-dynamixel = ["dynamixel-sdk>=3.7.31"]
+
+# Common
+pygame-dep = ["pygame>=2.5.1"]
+placo-dep = ["placo>=0.9.6"]
+transformers-dep = ["transformers>=4.50.3,<4.52.0"] # TODO: Bumb dependency
+grpcio-dep = ["grpcio==1.71.0"]
+
+# Motors
feetech = ["feetech-servo-sdk>=1.0.0"]
-gamepad = ["pygame>=2.5.1", "hidapi>=0.14.0"]
-hopejr = ["feetech-servo-sdk>=1.0.0", "pygame>=2.5.1"]
-kinematics = ["placo>=0.9.6"]
+dynamixel = ["dynamixel-sdk>=3.7.31"]
+
+# Robots
+gamepad = ["lerobot[pygame-dep]", "hidapi>=0.14.0"]
+hopejr = ["lerobot[feetech]", "lerobot[pygame-dep]"]
+lekiwi = ["lerobot[feetech]", "pyzmq>=26.2.1"]
+kinematics = ["lerobot[placo-dep]"]
intelrealsense = [
"pyrealsense2>=2.55.1.6486 ; sys_platform != 'darwin'",
"pyrealsense2-macosx>=2.54 ; sys_platform == 'darwin'",
]
-pi0 = ["transformers>=4.50.3"]
-smolvla = ["transformers>=4.50.3", "num2words>=0.5.14", "accelerate>=1.7.0", "safetensors>=0.4.3"]
-pusht = ["gym-pusht>=0.1.5 ; python_version < '4.0'"]
stretch = [
- "hello-robot-stretch-body>=0.7.27 ; python_version < '4.0' and sys_platform == 'linux'",
+ "hello-robot-stretch-body>=0.7.27 ; sys_platform == 'linux'",
"pyrender @ git+https://github.com/mmatl/pyrender.git ; sys_platform == 'linux'",
"pyrealsense2>=2.55.1.6486 ; sys_platform != 'darwin'"
-]
-test = ["pytest>=8.1.0", "pytest-timeout>=2.4.0", "pytest-cov>=5.0.0", "pyserial>=3.5", "mock-serial>=0.0.1 ; sys_platform != 'win32'"]
-hilserl = ["transformers>=4.50.3", "gym-hil>=0.1.9", "protobuf>=5.29.3", "grpcio==1.71.0", "placo>=0.9.6"]
-umi = ["imagecodecs>=2024.1.1"]
+] # TODO: Currently not supported
+
+# Policies
+pi0 = ["lerobot[transformers-dep]"]
+smolvla = ["lerobot[transformers-dep]", "num2words>=0.5.14", "accelerate>=1.7.0", "safetensors>=0.4.3"]
+hilserl = ["lerobot[transformers-dep]", "gym-hil>=0.1.9", "protobuf>=5.29.3", "lerobot[grpcio-dep]", "lerobot[placo-dep]"]
+
+# Features
+async = ["lerobot[grpcio-dep]", "matplotlib>=3.10.3"]
+
+# Development
+docs = ["hf-doc-builder @ git+https://github.com/huggingface/doc-builder.git@main", "watchdog >= 6.0.0"]
+dev = ["pre-commit>=3.7.0", "debugpy>=1.8.1", "grpcio-tools==1.71.0"]
+test = ["pytest>=8.1.0", "pytest-timeout>=2.4.0", "pytest-cov>=5.0.0", "mock-serial>=0.0.1 ; sys_platform != 'win32'"]
video_benchmark = ["scikit-image>=0.23.2", "pandas>=2.2.2"]
-xarm = ["gym-xarm>=0.1.1 ; python_version < '4.0'"]
-async = ["grpcio==1.71.0", "matplotlib>=3.10.3"]
-[tool.poetry]
-requires-poetry = ">=2.1"
-packages = [
- { include = "lerobot", from = "src" }
-]
+# Simulation
+aloha = ["gym-aloha>=0.1.1"]
+pusht = ["gym-pusht>=0.1.5", "pymunk>=6.6.0,<7.0.0"] # TODO: Fix pymunk version in gym-pusht instead
+xarm = ["gym-xarm>=0.1.1"]
+
+# ---------------- Tool Configurations ----------------
+[tool.setuptools.packages.find]
+where = ["src"]
[tool.ruff]
-line-length = 110
target-version = "py310"
+line-length = 110
exclude = ["tests/artifacts/**/*.safetensors", "*_pb2.py", "*_pb2_grpc.py"]
[tool.ruff.lint]
-select = ["E4", "E7", "E9", "F", "I", "N", "B", "C4", "SIM"]
+# E, W: pycodestyle errors and warnings
+# F: PyFlakes
+# I: isort
+# UP: pyupgrade
+# B: flake8-bugbear (good practices, potential bugs)
+# C4: flake8-comprehensions (more concise comprehensions)
+# A: flake8-builtins (shadowing builtins)
+# SIM: flake8-simplify
+# RUF: Ruff-specific rules
+# D: pydocstyle (for docstring style/formatting)
+# S: flake8-bandit (some security checks, complements Bandit)
+# T20: flake8-print (discourage print statements in production code)
+# N: pep8-naming
+# TODO: Uncomment rules when ready to use
+select = [
+ "E", "W", "F", "I", "B", "C4", "T20", "N" # "SIM", "A", "S", "D", "RUF", "UP"
+]
+ignore = [
+ "E501", # Line too long
+ "T201", # Print statement found
+ "T203", # Pprint statement found
+ "B008", # Perform function call in argument defaults
+]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "F403"]
+[tool.ruff.lint.isort]
+combine-as-imports = true
+known-first-party = ["lerobot"]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+skip-magic-trailing-comma = false
+line-ending = "auto"
+docstring-code-format = true
+
[tool.bandit]
exclude_dirs = [
"tests",
@@ -148,6 +210,24 @@ default.extend-ignore-identifiers-re = [
"ein",
]
-[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
+# TODO: Uncomment when ready to use
+# [tool.interrogate]
+# ignore-init-module = true
+# ignore-init-method = true
+# ignore-nested-functions = false
+# ignore-magic = false
+# ignore-semiprivate = false
+# ignore-private = false
+# ignore-property-decorators = false
+# ignore-module = false
+# ignore-setters = false
+# fail-under = 80
+# output-format = "term-missing"
+# color = true
+# paths = ["src/lerobot"]
+
+# [tool.mypy]
+# python_version = "3.10"
+# warn_return_any = true
+# warn_unused_configs = true
+# ignore_missing_imports = false
diff --git a/src/lerobot/cameras/camera.py b/src/lerobot/cameras/camera.py
index 1937205b18..e435c7309a 100644
--- a/src/lerobot/cameras/camera.py
+++ b/src/lerobot/cameras/camera.py
@@ -15,7 +15,7 @@
# limitations under the License.
import abc
-from typing import Any, Dict, List
+from typing import Any
import numpy as np
@@ -69,7 +69,7 @@ def is_connected(self) -> bool:
@staticmethod
@abc.abstractmethod
- def find_cameras() -> List[Dict[str, Any]]:
+ def find_cameras() -> list[dict[str, Any]]:
"""Detects available cameras connected to the system.
Returns:
List[Dict[str, Any]]: A list of dictionaries,
diff --git a/src/lerobot/cameras/opencv/camera_opencv.py b/src/lerobot/cameras/opencv/camera_opencv.py
index 1d7a1645d5..7ad9988cca 100644
--- a/src/lerobot/cameras/opencv/camera_opencv.py
+++ b/src/lerobot/cameras/opencv/camera_opencv.py
@@ -23,7 +23,7 @@
import time
from pathlib import Path
from threading import Event, Lock, Thread
-from typing import Any, Dict, List
+from typing import Any
# Fix MSMF hardware transform compatibility for Windows before importing cv2
if platform.system() == "Windows" and "OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS" not in os.environ:
@@ -245,7 +245,7 @@ def _validate_width_and_height(self) -> None:
)
@staticmethod
- def find_cameras() -> List[Dict[str, Any]]:
+ def find_cameras() -> list[dict[str, Any]]:
"""
Detects available OpenCV cameras connected to the system.
diff --git a/src/lerobot/cameras/realsense/camera_realsense.py b/src/lerobot/cameras/realsense/camera_realsense.py
index 96531b6940..74b055fa48 100644
--- a/src/lerobot/cameras/realsense/camera_realsense.py
+++ b/src/lerobot/cameras/realsense/camera_realsense.py
@@ -19,7 +19,7 @@
import logging
import time
from threading import Event, Lock, Thread
-from typing import Any, Dict, List
+from typing import Any
import cv2
import numpy as np
@@ -194,7 +194,7 @@ def connect(self, warmup: bool = True):
logger.info(f"{self} connected.")
@staticmethod
- def find_cameras() -> List[Dict[str, Any]]:
+ def find_cameras() -> list[dict[str, Any]]:
"""
Detects available Intel RealSense cameras connected to the system.
diff --git a/src/lerobot/cameras/realsense/configuration_realsense.py b/src/lerobot/cameras/realsense/configuration_realsense.py
index 82e7c0d36b..36a86876d5 100644
--- a/src/lerobot/cameras/realsense/configuration_realsense.py
+++ b/src/lerobot/cameras/realsense/configuration_realsense.py
@@ -28,12 +28,12 @@ class RealSenseCameraConfig(CameraConfig):
Example configurations for Intel RealSense D405:
```python
# Basic configurations
- RealSenseCameraConfig("0123456789", 30, 1280, 720) # 1280x720 @ 30FPS
- RealSenseCameraConfig("0123456789", 60, 640, 480) # 640x480 @ 60FPS
+ RealSenseCameraConfig("0123456789", 30, 1280, 720) # 1280x720 @ 30FPS
+ RealSenseCameraConfig("0123456789", 60, 640, 480) # 640x480 @ 60FPS
# Advanced configurations
RealSenseCameraConfig("0123456789", 30, 640, 480, use_depth=True) # With depth sensing
- RealSenseCameraConfig("0123456789", 30, 640, 480, rotation=Cv2Rotation.ROTATE_90) # With 90° rotation
+ RealSenseCameraConfig("0123456789", 30, 640, 480, rotation=Cv2Rotation.ROTATE_90) # With 90° rotation
```
Attributes:
diff --git a/src/lerobot/configs/parser.py b/src/lerobot/configs/parser.py
index 1da7ad83f6..2296eaa20c 100644
--- a/src/lerobot/configs/parser.py
+++ b/src/lerobot/configs/parser.py
@@ -16,9 +16,9 @@
import pkgutil
import sys
from argparse import ArgumentError
+from collections.abc import Sequence
from functools import wraps
from pathlib import Path
-from typing import Sequence
import draccus
@@ -76,9 +76,8 @@ def parse_plugin_args(plugin_arg_suffix: str, args: Sequence[str]) -> dict:
- Values are the corresponding argument values
Example:
- >>> args = ['--env.discover_packages_path=my_package',
- ... '--other_arg=value']
- >>> parse_plugin_args('discover_packages_path', args)
+ >>> args = ["--env.discover_packages_path=my_package", "--other_arg=value"]
+ >>> parse_plugin_args("discover_packages_path", args)
{'env.discover_packages_path': 'my_package'}
"""
plugin_args = {}
@@ -111,7 +110,7 @@ def load_plugin(plugin_path: str) -> None:
PluginLoadError: If the plugin cannot be loaded due to import errors or if the package path is invalid.
Examples:
- >>> load_plugin("external_plugin.core") # Loads plugin from external package
+ >>> load_plugin("external_plugin.core") # Loads plugin from external package
Notes:
- The plugin package should handle its own registration during import
diff --git a/src/lerobot/configs/policies.py b/src/lerobot/configs/policies.py
index 05f3296b8b..c5b2fa09e3 100644
--- a/src/lerobot/configs/policies.py
+++ b/src/lerobot/configs/policies.py
@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
+import builtins
import json
import logging
import os
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
-from typing import Type, TypeVar
+from typing import TypeVar
import draccus
from huggingface_hub import hf_hub_download
@@ -31,7 +32,6 @@
from lerobot.utils.hub import HubMixin
from lerobot.utils.utils import auto_select_torch_device, is_amp_available, is_torch_device_available
-# Generic variable that is either PreTrainedConfig or a subclass thereof
T = TypeVar("T", bound="PreTrainedConfig")
@@ -148,7 +148,7 @@ def _save_pretrained(self, save_directory: Path) -> None:
@classmethod
def from_pretrained(
- cls: Type[T],
+ cls: builtins.type[T],
pretrained_name_or_path: str | Path,
*,
force_download: bool = False,
diff --git a/src/lerobot/configs/train.py b/src/lerobot/configs/train.py
index c088a5fa16..60a4d81d52 100644
--- a/src/lerobot/configs/train.py
+++ b/src/lerobot/configs/train.py
@@ -11,11 +11,11 @@
# 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 builtins
import datetime as dt
import os
from dataclasses import dataclass, field
from pathlib import Path
-from typing import Type
import draccus
from huggingface_hub import hf_hub_download
@@ -135,7 +135,7 @@ def _save_pretrained(self, save_directory: Path) -> None:
@classmethod
def from_pretrained(
- cls: Type["TrainPipelineConfig"],
+ cls: builtins.type["TrainPipelineConfig"],
pretrained_name_or_path: str | Path,
*,
force_download: bool = False,
diff --git a/src/lerobot/datasets/card_template.md b/src/lerobot/datasets/card_template.md
index 7ee27df95d..ee26a78f57 100644
--- a/src/lerobot/datasets/card_template.md
+++ b/src/lerobot/datasets/card_template.md
@@ -1,7 +1,8 @@
---
# For reference on dataset card metadata, see the spec: https://github.com/huggingface/hub-docs/blob/main/datasetcard.md?plain=1
# Doc / guide: https://huggingface.co/docs/hub/datasets-cards
-{{ card_data }}
+# prettier-ignore
+{{card_data}}
---
This dataset was created using [LeRobot](https://github.com/huggingface/lerobot).
diff --git a/src/lerobot/datasets/lerobot_dataset.py b/src/lerobot/datasets/lerobot_dataset.py
index 1a3dd1e1b0..46feed2bf5 100644
--- a/src/lerobot/datasets/lerobot_dataset.py
+++ b/src/lerobot/datasets/lerobot_dataset.py
@@ -16,8 +16,8 @@
import contextlib
import logging
import shutil
+from collections.abc import Callable
from pathlib import Path
-from typing import Callable
import datasets
import numpy as np
diff --git a/src/lerobot/datasets/push_dataset_to_hub/utils.py b/src/lerobot/datasets/push_dataset_to_hub/utils.py
index 6aca7b03b7..5f6363a77b 100644
--- a/src/lerobot/datasets/push_dataset_to_hub/utils.py
+++ b/src/lerobot/datasets/push_dataset_to_hub/utils.py
@@ -16,7 +16,6 @@
import inspect
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
-from typing import Dict
import datasets
import numpy
@@ -77,7 +76,7 @@ def check_repo_id(repo_id: str) -> None:
# TODO(aliberts): remove
-def calculate_episode_data_index(hf_dataset: datasets.Dataset) -> Dict[str, torch.Tensor]:
+def calculate_episode_data_index(hf_dataset: datasets.Dataset) -> dict[str, torch.Tensor]:
"""
Calculate episode data index for the provided HuggingFace Dataset. Relies on episode_index column of hf_dataset.
diff --git a/src/lerobot/datasets/sampler.py b/src/lerobot/datasets/sampler.py
index 2f6c15c150..79ac7a4b22 100644
--- a/src/lerobot/datasets/sampler.py
+++ b/src/lerobot/datasets/sampler.py
@@ -13,7 +13,7 @@
# 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 typing import Iterator, Union
+from collections.abc import Iterator
import torch
@@ -22,7 +22,7 @@ class EpisodeAwareSampler:
def __init__(
self,
episode_data_index: dict,
- episode_indices_to_use: Union[list, None] = None,
+ episode_indices_to_use: list | None = None,
drop_n_first_frames: int = 0,
drop_n_last_frames: int = 0,
shuffle: bool = False,
diff --git a/src/lerobot/datasets/transforms.py b/src/lerobot/datasets/transforms.py
index 3ac1d57715..f992275b7c 100644
--- a/src/lerobot/datasets/transforms.py
+++ b/src/lerobot/datasets/transforms.py
@@ -14,13 +14,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
+from collections.abc import Callable, Sequence
from dataclasses import dataclass, field
-from typing import Any, Callable, Sequence
+from typing import Any
import torch
from torchvision.transforms import v2
-from torchvision.transforms.v2 import Transform
-from torchvision.transforms.v2 import functional as F # noqa: N812
+from torchvision.transforms.v2 import (
+ Transform,
+ functional as F, # noqa: N812
+)
class RandomSubsetApply(Transform):
diff --git a/src/lerobot/envs/configs.py b/src/lerobot/envs/configs.py
index de969d618b..ef381e9e7c 100644
--- a/src/lerobot/envs/configs.py
+++ b/src/lerobot/envs/configs.py
@@ -14,7 +14,7 @@
import abc
from dataclasses import dataclass, field
-from typing import Any, Optional
+from typing import Any
import draccus
@@ -179,10 +179,10 @@ class EnvTransformConfig:
add_joint_velocity_to_observation: bool = False
add_current_to_observation: bool = False
add_ee_pose_to_observation: bool = False
- crop_params_dict: Optional[dict[str, tuple[int, int, int, int]]] = None
- resize_size: Optional[tuple[int, int]] = None
+ crop_params_dict: dict[str, tuple[int, int, int, int]] | None = None
+ resize_size: tuple[int, int] | None = None
control_time_s: float = 20.0
- fixed_reset_joint_positions: Optional[Any] = None
+ fixed_reset_joint_positions: Any | None = None
reset_time_s: float = 5.0
use_gripper: bool = True
gripper_quantization_threshold: float | None = 0.8
@@ -195,21 +195,21 @@ class EnvTransformConfig:
class HILSerlRobotEnvConfig(EnvConfig):
"""Configuration for the HILSerlRobotEnv environment."""
- robot: Optional[RobotConfig] = None
- teleop: Optional[TeleoperatorConfig] = None
- wrapper: Optional[EnvTransformConfig] = None
+ robot: RobotConfig | None = None
+ teleop: TeleoperatorConfig | None = None
+ wrapper: EnvTransformConfig | None = None
fps: int = 10
name: str = "real_robot"
mode: str = None # Either "record", "replay", None
- repo_id: Optional[str] = None
- dataset_root: Optional[str] = None
+ repo_id: str | None = None
+ dataset_root: str | None = None
task: str = ""
num_episodes: int = 10 # only for record mode
episode: int = 0
device: str = "cuda"
push_to_hub: bool = True
- pretrained_policy_name_or_path: Optional[str] = None
- reward_classifier_pretrained_path: Optional[str] = None
+ pretrained_policy_name_or_path: str | None = None
+ reward_classifier_pretrained_path: str | None = None
# For the reward classifier, to record more positive examples after a success
number_of_steps_after_success: int = 0
@@ -248,18 +248,18 @@ class HILEnvConfig(EnvConfig):
}
)
################# args from hilserlrobotenv
- reward_classifier_pretrained_path: Optional[str] = None
- robot_config: Optional[RobotConfig] = None
- teleop_config: Optional[TeleoperatorConfig] = None
- wrapper: Optional[EnvTransformConfig] = None
+ reward_classifier_pretrained_path: str | None = None
+ robot_config: RobotConfig | None = None
+ teleop_config: TeleoperatorConfig | None = None
+ wrapper: EnvTransformConfig | None = None
mode: str = None # Either "record", "replay", None
- repo_id: Optional[str] = None
- dataset_root: Optional[str] = None
+ repo_id: str | None = None
+ dataset_root: str | None = None
num_episodes: int = 10 # only for record mode
episode: int = 0
device: str = "cuda"
push_to_hub: bool = True
- pretrained_policy_name_or_path: Optional[str] = None
+ pretrained_policy_name_or_path: str | None = None
# For the reward classifier, to record more positive examples after a success
number_of_steps_after_success: int = 0
############################
diff --git a/src/lerobot/find_cameras.py b/src/lerobot/find_cameras.py
index aff2f8c195..be8f272eee 100644
--- a/src/lerobot/find_cameras.py
+++ b/src/lerobot/find_cameras.py
@@ -32,7 +32,7 @@
import logging
import time
from pathlib import Path
-from typing import Any, Dict, List
+from typing import Any
import numpy as np
from PIL import Image
@@ -46,14 +46,14 @@
logger = logging.getLogger(__name__)
-def find_all_opencv_cameras() -> List[Dict[str, Any]]:
+def find_all_opencv_cameras() -> list[dict[str, Any]]:
"""
Finds all available OpenCV cameras plugged into the system.
Returns:
A list of all available OpenCV cameras with their metadata.
"""
- all_opencv_cameras_info: List[Dict[str, Any]] = []
+ all_opencv_cameras_info: list[dict[str, Any]] = []
logger.info("Searching for OpenCV cameras...")
try:
opencv_cameras = OpenCVCamera.find_cameras()
@@ -66,14 +66,14 @@ def find_all_opencv_cameras() -> List[Dict[str, Any]]:
return all_opencv_cameras_info
-def find_all_realsense_cameras() -> List[Dict[str, Any]]:
+def find_all_realsense_cameras() -> list[dict[str, Any]]:
"""
Finds all available RealSense cameras plugged into the system.
Returns:
A list of all available RealSense cameras with their metadata.
"""
- all_realsense_cameras_info: List[Dict[str, Any]] = []
+ all_realsense_cameras_info: list[dict[str, Any]] = []
logger.info("Searching for RealSense cameras...")
try:
realsense_cameras = RealSenseCamera.find_cameras()
@@ -88,7 +88,7 @@ def find_all_realsense_cameras() -> List[Dict[str, Any]]:
return all_realsense_cameras_info
-def find_and_print_cameras(camera_type_filter: str | None = None) -> List[Dict[str, Any]]:
+def find_and_print_cameras(camera_type_filter: str | None = None) -> list[dict[str, Any]]:
"""
Finds available cameras based on an optional filter and prints their information.
@@ -99,7 +99,7 @@ def find_and_print_cameras(camera_type_filter: str | None = None) -> List[Dict[s
Returns:
A list of all available cameras matching the filter, with their metadata.
"""
- all_cameras_info: List[Dict[str, Any]] = []
+ all_cameras_info: list[dict[str, Any]] = []
if camera_type_filter:
camera_type_filter = camera_type_filter.lower()
@@ -153,7 +153,7 @@ def save_image(
logger.error(f"Failed to save image for camera {camera_identifier} (type {camera_type}): {e}")
-def create_camera_instance(cam_meta: Dict[str, Any]) -> Dict[str, Any] | None:
+def create_camera_instance(cam_meta: dict[str, Any]) -> dict[str, Any] | None:
"""Create and connect to a camera instance based on metadata."""
cam_type = cam_meta.get("type")
cam_id = cam_meta.get("id")
@@ -190,7 +190,7 @@ def create_camera_instance(cam_meta: Dict[str, Any]) -> Dict[str, Any] | None:
def process_camera_image(
- cam_dict: Dict[str, Any], output_dir: Path, current_time: float
+ cam_dict: dict[str, Any], output_dir: Path, current_time: float
) -> concurrent.futures.Future | None:
"""Capture and process an image from a single camera."""
cam = cam_dict["instance"]
@@ -216,7 +216,7 @@ def process_camera_image(
return None
-def cleanup_cameras(cameras_to_use: List[Dict[str, Any]]):
+def cleanup_cameras(cameras_to_use: list[dict[str, Any]]):
"""Disconnect all cameras."""
logger.info(f"Disconnecting {len(cameras_to_use)} cameras...")
for cam_dict in cameras_to_use:
diff --git a/src/lerobot/motors/motors_bus.py b/src/lerobot/motors/motors_bus.py
index 26522c7c9e..597bcd3c4d 100644
--- a/src/lerobot/motors/motors_bus.py
+++ b/src/lerobot/motors/motors_bus.py
@@ -224,7 +224,7 @@ class MotorsBus(abc.ABC):
```bash
python -m lerobot.find_port.py
>>> Finding all available ports for the MotorsBus.
- >>> ['/dev/tty.usbmodem575E0032081', '/dev/tty.usbmodem575E0031751']
+ >>> ["/dev/tty.usbmodem575E0032081", "/dev/tty.usbmodem575E0031751"]
>>> Remove the usb cable from your MotorsBus and press Enter when done.
>>> The port of this MotorsBus is /dev/tty.usbmodem575E0031751.
>>> Reconnect the usb cable.
diff --git a/src/lerobot/policies/act/modeling_act.py b/src/lerobot/policies/act/modeling_act.py
index aa81d3cd2b..4a048e63d3 100644
--- a/src/lerobot/policies/act/modeling_act.py
+++ b/src/lerobot/policies/act/modeling_act.py
@@ -21,8 +21,8 @@
import math
from collections import deque
+from collections.abc import Callable
from itertools import chain
-from typing import Callable
import einops
import numpy as np
@@ -216,7 +216,7 @@ def __init__(self, temporal_ensemble_coeff: float, chunk_size: int) -> None:
continue
avg *= exp_weights[:i].sum()
avg += item * exp_weights[i]
- avg /= exp_weights[:i+1].sum()
+ avg /= exp_weights[: i + 1].sum()
print("online", avg)
```
"""
diff --git a/src/lerobot/policies/diffusion/modeling_diffusion.py b/src/lerobot/policies/diffusion/modeling_diffusion.py
index 6dad8fb895..24b273967e 100644
--- a/src/lerobot/policies/diffusion/modeling_diffusion.py
+++ b/src/lerobot/policies/diffusion/modeling_diffusion.py
@@ -22,7 +22,7 @@
import math
from collections import deque
-from typing import Callable
+from collections.abc import Callable
import einops
import numpy as np
diff --git a/src/lerobot/policies/pi0/paligemma_with_expert.py b/src/lerobot/policies/pi0/paligemma_with_expert.py
index f0f5713e57..edc34b7c56 100644
--- a/src/lerobot/policies/pi0/paligemma_with_expert.py
+++ b/src/lerobot/policies/pi0/paligemma_with_expert.py
@@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from typing import List, Optional, Union
import torch
import torch.version
@@ -228,12 +227,12 @@ def embed_language_tokens(self, tokens: torch.Tensor):
# TODO: break down this huge forward into modules or functions
def forward(
self,
- attention_mask: Optional[torch.Tensor] = None,
- position_ids: Optional[torch.LongTensor] = None,
- past_key_values: Optional[Union[List[torch.FloatTensor], Cache]] = None,
- inputs_embeds: List[torch.FloatTensor] = None,
- use_cache: Optional[bool] = None,
- fill_kv_cache: Optional[bool] = None,
+ attention_mask: torch.Tensor | None = None,
+ position_ids: torch.LongTensor | None = None,
+ past_key_values: list[torch.FloatTensor] | Cache | None = None,
+ inputs_embeds: list[torch.FloatTensor] = None,
+ use_cache: bool | None = None,
+ fill_kv_cache: bool | None = None,
):
models = [self.paligemma.language_model, self.gemma_expert.model]
diff --git a/src/lerobot/policies/pretrained.py b/src/lerobot/policies/pretrained.py
index d18b798a82..d745c901c4 100644
--- a/src/lerobot/policies/pretrained.py
+++ b/src/lerobot/policies/pretrained.py
@@ -12,20 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
+import builtins
import logging
import os
from importlib.resources import files
from pathlib import Path
from tempfile import TemporaryDirectory
-from typing import List, Type, TypeVar
+from typing import TypeVar
import packaging
import safetensors
from huggingface_hub import HfApi, ModelCard, ModelCardData, hf_hub_download
from huggingface_hub.constants import SAFETENSORS_SINGLE_FILE
from huggingface_hub.errors import HfHubHTTPError
-from safetensors.torch import load_model as load_model_as_safetensor
-from safetensors.torch import save_model as save_model_as_safetensor
+from safetensors.torch import load_model as load_model_as_safetensor, save_model as save_model_as_safetensor
from torch import Tensor, nn
from lerobot.configs.policies import PreTrainedConfig
@@ -67,7 +67,7 @@ def _save_pretrained(self, save_directory: Path) -> None:
@classmethod
def from_pretrained(
- cls: Type[T],
+ cls: builtins.type[T],
pretrained_name_or_path: str | Path,
*,
config: PreTrainedConfig | None = None,
@@ -223,7 +223,7 @@ def push_model_to_hub(
logging.info(f"Model pushed to {commit_info.repo_url.url}")
def generate_model_card(
- self, dataset_repo_id: str, model_type: str, license: str | None, tags: List[str] | None
+ self, dataset_repo_id: str, model_type: str, license: str | None, tags: list[str] | None
) -> ModelCard:
base_model = "lerobot/smolvla_base" if model_type == "smolvla" else None # Set a base model
diff --git a/src/lerobot/policies/sac/modeling_sac.py b/src/lerobot/policies/sac/modeling_sac.py
index 93cfe6c93c..878f3cdd85 100644
--- a/src/lerobot/policies/sac/modeling_sac.py
+++ b/src/lerobot/policies/sac/modeling_sac.py
@@ -16,8 +16,9 @@
# limitations under the License.
import math
+from collections.abc import Callable
from dataclasses import asdict
-from typing import Callable, Literal
+from typing import Literal
import einops
import numpy as np
diff --git a/src/lerobot/policies/smolvla/smolvlm_with_expert.py b/src/lerobot/policies/smolvla/smolvlm_with_expert.py
index 07eae80892..f3d1a693a2 100644
--- a/src/lerobot/policies/smolvla/smolvlm_with_expert.py
+++ b/src/lerobot/policies/smolvla/smolvlm_with_expert.py
@@ -13,7 +13,6 @@
# limitations under the License.
import copy
-from typing import List, Optional
import torch
from torch import nn
@@ -403,12 +402,12 @@ def get_model_layers(self, models: list) -> list:
def forward(
self,
- attention_mask: Optional[torch.Tensor] = None,
- position_ids: Optional[torch.LongTensor] = None,
- past_key_values: Optional[List[torch.FloatTensor]] = None,
- inputs_embeds: List[torch.FloatTensor] = None,
- use_cache: Optional[bool] = None,
- fill_kv_cache: Optional[bool] = None,
+ attention_mask: torch.Tensor | None = None,
+ position_ids: torch.LongTensor | None = None,
+ past_key_values: list[torch.FloatTensor] | None = None,
+ inputs_embeds: list[torch.FloatTensor] = None,
+ use_cache: bool | None = None,
+ fill_kv_cache: bool | None = None,
):
models = [self.get_vlm_model().text_model, self.lm_expert]
model_layers = self.get_model_layers(models)
diff --git a/src/lerobot/policies/tdmpc/modeling_tdmpc.py b/src/lerobot/policies/tdmpc/modeling_tdmpc.py
index c276893874..664fe863dd 100644
--- a/src/lerobot/policies/tdmpc/modeling_tdmpc.py
+++ b/src/lerobot/policies/tdmpc/modeling_tdmpc.py
@@ -24,9 +24,9 @@
# ruff: noqa: N806
from collections import deque
+from collections.abc import Callable
from copy import deepcopy
from functools import partial
-from typing import Callable
import einops
import numpy as np
diff --git a/src/lerobot/policies/vqbet/modeling_vqbet.py b/src/lerobot/policies/vqbet/modeling_vqbet.py
index 59c820a961..b271298a3f 100644
--- a/src/lerobot/policies/vqbet/modeling_vqbet.py
+++ b/src/lerobot/policies/vqbet/modeling_vqbet.py
@@ -18,7 +18,7 @@
import warnings
from collections import deque
-from typing import Callable, List
+from collections.abc import Callable
import einops
import numpy as np
@@ -901,7 +901,7 @@ class MLP(torch.nn.Sequential):
def __init__(
self,
in_channels: int,
- hidden_channels: List[int],
+ hidden_channels: list[int],
):
layers = []
in_dim = in_channels
diff --git a/src/lerobot/policies/vqbet/vqbet_utils.py b/src/lerobot/policies/vqbet/vqbet_utils.py
index 03b02a280d..e0afe55852 100644
--- a/src/lerobot/policies/vqbet/vqbet_utils.py
+++ b/src/lerobot/policies/vqbet/vqbet_utils.py
@@ -17,10 +17,10 @@
# limitations under the License.
import math
+from collections.abc import Callable
from functools import partial
from math import ceil
from random import randrange
-from typing import Callable
import torch
import torch.distributed as distributed
@@ -198,7 +198,7 @@ def __init__(self, config: VQBeTConfig):
# report number of parameters
n_params = sum(p.numel() for p in self.parameters())
- print("number of parameters: {:.2f}M".format(n_params / 1e6))
+ print(f"number of parameters: {n_params / 1e6:.2f}M")
def forward(self, input, targets=None):
device = input.device
@@ -255,7 +255,7 @@ def configure_parameters(self):
blacklist_weight_modules = (torch.nn.LayerNorm, torch.nn.Embedding)
for mn, m in self.named_modules():
for pn, _p in m.named_parameters():
- fpn = "{}.{}".format(mn, pn) if mn else pn # full param name
+ fpn = f"{mn}.{pn}" if mn else pn # full param name
if pn.endswith("bias"):
# all biases will not be decayed
no_decay.add(fpn)
diff --git a/src/lerobot/record.py b/src/lerobot/record.py
index c8184d40b4..0b1af192e8 100644
--- a/src/lerobot/record.py
+++ b/src/lerobot/record.py
@@ -62,7 +62,6 @@
from dataclasses import asdict, dataclass
from pathlib import Path
from pprint import pformat
-from typing import List
from lerobot.cameras import ( # noqa: F401
CameraConfig, # noqa: F401
@@ -190,7 +189,7 @@ def record_loop(
events: dict,
fps: int,
dataset: LeRobotDataset | None = None,
- teleop: Teleoperator | List[Teleoperator] | None = None,
+ teleop: Teleoperator | list[Teleoperator] | None = None,
policy: PreTrainedPolicy | None = None,
control_time_s: int | None = None,
single_task: str | None = None,
diff --git a/src/lerobot/robots/hope_jr/hope_jr.mdx b/src/lerobot/robots/hope_jr/hope_jr.mdx
index 2f9ec9d892..72aa8f9239 100644
--- a/src/lerobot/robots/hope_jr/hope_jr.mdx
+++ b/src/lerobot/robots/hope_jr/hope_jr.mdx
@@ -9,6 +9,7 @@
Follow the [installation instructions](https://github.com/huggingface/lerobot#installation) to install LeRobot.
Install LeRobot with HopeJR dependencies:
+
```bash
pip install -e ".[hopejr]"
```
@@ -40,35 +41,39 @@ python -m lerobot.calibrate \
When running the calibration script, a calibration GUI will pop up. Finger joints are named as follows:
**Thumb**:
+
- **CMC**: base joint connecting thumb to hand
- **MCP**: knuckle joint
- **PIP**: first finger joint
- **DIP** : fingertip joint
**Index, Middle, Ring, and Pinky fingers**:
+
- **Radial flexor**: Moves base of finger towards the thumb
- **Ulnar flexor**: Moves base of finger towards the pinky
- **PIP/DIP**: Flexes the distal and proximal phalanx of the finger
Each one of these will need to be calibrated individually via the GUI.
- Note that ulnar and radial flexors should have ranges of the same size (but with different offsets) in order to get symmetric movement.
+Note that ulnar and radial flexors should have ranges of the same size (but with different offsets) in order to get symmetric movement.
-
-
+ width="100%"
+ >
Use the calibration interface to set the range boundaries for each joint as shown above.
-
-
+ width="100%"
+ >
Once you have set the appropriate boundaries for all joints, click "Save" to save the calibration values to the motors.
@@ -122,16 +127,18 @@ python -m lerobot.calibrate \
```
This will open a calibration GUI where you can set the range limits for each motor. The arm motions are organized as follows:
+
- **Shoulder**: pitch, yaw, and roll
- **Elbow**: flex
- **Wrist**: pitch, yaw, and roll
-
-
+ width="100%"
+ >
Use the calibration interface to set the range boundaries for each joint. Move each joint through its full range of motion and adjust the minimum and maximum values accordingly. Once you have set the appropriate boundaries for all joints, save the calibration.
@@ -169,6 +176,7 @@ Calibration saved to /Users/your_username/.cache/huggingface/lerobot/calibration
Due to global variable conflicts in the Feetech middleware, teleoperation for arm and hand must run in separate shell sessions:
### Hand
+
```bash
python -m lerobot.teleoperate \
--robot.type=hope_jr_hand \
@@ -184,6 +192,7 @@ python -m lerobot.teleoperate \
```
### Arm
+
```bash
python -m lerobot.teleoperate \
--robot.type=hope_jr_arm \
diff --git a/src/lerobot/robots/koch_follower/koch.mdx b/src/lerobot/robots/koch_follower/koch.mdx
index f70a1802c6..d0b991e748 100644
--- a/src/lerobot/robots/koch_follower/koch.mdx
+++ b/src/lerobot/robots/koch_follower/koch.mdx
@@ -10,15 +10,16 @@ For a visual walkthrough of the assembly process, you can refer to [this video t
> [!WARNING]
> Since the production of this video, we simplified the configuration phase. Because of this, two things differ from the instructions in that video:
+>
> - Don't plug in all the motor cables right away and wait to be instructed to do so in [Configure the motors](#configure-the-motors).
> - Don't screw in the controller board (PCB) to the base right away and wait for being instructed to do so in [Configure the motors](#configure-the-motors).
-
## Install LeRobot 🤗
To install LeRobot follow, our [Installation Guide](./installation)
In addition to these instructions, you need to install the Dynamixel SDK:
+
```bash
pip install -e ".[dynamixel]"
```
@@ -28,6 +29,7 @@ pip install -e ".[dynamixel]"
### 1. Find the USB ports associated with each arm
To find the port for each bus servo adapter, run this script:
+
```bash
python -m lerobot.find_port
```
@@ -54,6 +56,7 @@ Where the found port is: `/dev/tty.usbmodem575E0032081` corresponding to your le
On Linux, you might need to give access to the USB ports by running:
+
```bash
sudo chmod 666 /dev/ttyACM0
sudo chmod 666 /dev/ttyACM1
@@ -99,9 +102,11 @@ python -m lerobot.setup_motors \
--robot.type=koch_follower \
--robot.port=/dev/tty.usbmodem575E0031751 # <- paste here the port found at previous step
```
+
+
```python
from lerobot.robots.koch_follower import KochFollower, KochFollowerConfig
@@ -112,10 +117,13 @@ config = KochFollowerConfig(
follower = KochFollower(config)
follower.setup_motors()
```
+
+
You should see the following instruction.
+
```
Connect the controller board to the 'gripper' motor only and press enter.
```
@@ -125,22 +133,26 @@ As instructed, plug the gripper's motor. Make sure it's the only motor connected
Troubleshooting
- If you get an error at that point, check your cables and make sure they are plugged in properly:
-
-
Power supply
-
USB cable between your computer and the controller board
-
The 3-pin cable from the controller board to the motor
-
+If you get an error at that point, check your cables and make sure they are plugged in properly:
+
+
+
Power supply
+
USB cable between your computer and the controller board
+
The 3-pin cable from the controller board to the motor
+
+
+If you are using a Waveshare controller board, make sure that the two jumpers are set on the `B` channel (USB).
- If you are using a Waveshare controller board, make sure that the two jumpers are set on the `B` channel (USB).
You should then see the following message:
+
```
'gripper' motor id set to 6
```
Followed by the next instruction:
+
```
Connect the controller board to the 'wrist_roll' motor only and press enter.
```
@@ -155,6 +167,7 @@ Repeat the operation for each motor as instructed.
When you are done, the script will simply finish, at which point the motors are ready to be used. You can now plug the 3-pin cable from each motor to the next one, and the cable from the first motor (the 'shoulder pan' with id=1) to the controller board, which can now be attached to the base of the arm.
#### Leader
+
Do the same steps for the leader arm but modify the command or script accordingly.
@@ -165,9 +178,11 @@ python -m lerobot.setup_motors \
--teleop.type=koch_leader \
--teleop.port=/dev/tty.usbmodem575E0031751 \ # <- paste here the port found at previous step
```
+
+
```python
from lerobot.teleoperators.koch_leader import KochLeader, KochLeaderConfig
@@ -178,6 +193,8 @@ config = KochLeaderConfig(
leader = KochLeader(config)
leader.setup_motors()
```
+
+
@@ -199,9 +216,11 @@ python -m lerobot.calibrate \
--robot.port=/dev/tty.usbmodem58760431551 \ # <- The port of your robot
--robot.id=my_awesome_follower_arm # <- Give the robot a unique name
```
+
+
```python
from lerobot.robots.koch_follower import KochFollowerConfig, KochFollower
@@ -215,6 +234,8 @@ follower.connect(calibrate=False)
follower.calibrate()
follower.disconnect()
```
+
+
@@ -233,9 +254,11 @@ python -m lerobot.calibrate \
--teleop.port=/dev/tty.usbmodem58760431551 \ # <- The port of your robot
--teleop.id=my_awesome_leader_arm # <- Give the robot a unique name
```
+
+
```python
from lerobot.teleoperators.koch_leader import KochLeaderConfig, KochLeader
@@ -249,10 +272,12 @@ leader.connect(calibrate=False)
leader.calibrate()
leader.disconnect()
```
+
+
Congrats 🎉, your robot is all set to learn a task on its own. Start training it by following this tutorial: [Getting started with real-world robots](./getting_started_real_world_robot)
> [!TIP]
-> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
+> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
diff --git a/src/lerobot/robots/lekiwi/lekiwi.mdx b/src/lerobot/robots/lekiwi/lekiwi.mdx
index 61b1c05c17..bb70fd26b7 100644
--- a/src/lerobot/robots/lekiwi/lekiwi.mdx
+++ b/src/lerobot/robots/lekiwi/lekiwi.mdx
@@ -8,31 +8,43 @@ Follow this [README](https://github.com/SIGRobotics-UIUC/LeKiwi). It contains th
And advise if it's your first time printing or if you don't own a 3D printer.
### Wired version
+
If you have the **wired** LeKiwi version, you can skip the installation of the Raspberry Pi and setting up SSH. You can also run all commands directly on your PC for both the LeKiwi scripts and the leader arm scripts for teleoperating.
## Install software on Pi
+
Now we have to set up the remote PC that will run on the LeKiwi Robot. This is normally a Raspberry Pi, but can be any PC that can run on 5V and has enough usb ports (2 or more) for the cameras and motor control board.
### Install OS
+
For setting up the Raspberry Pi and its SD-card see: [Setup PI](https://www.raspberrypi.com/documentation/computers/getting-started.html). Here is explained how to download the [Imager](https://www.raspberrypi.com/software/) to install Raspberry Pi OS or Ubuntu.
### Setup SSH
+
After setting up your Pi, you should enable and set up [SSH](https://www.raspberrypi.com/news/coding-on-raspberry-pi-remotely-with-visual-studio-code/) (Secure Shell Protocol) so you can log in to the Pi from your laptop without requiring a screen, keyboard, and mouse on the Pi. A great tutorial on how to do this can be found [here](https://www.raspberrypi.com/documentation/computers/remote-access.html#ssh). Logging into your Pi can be done in your Command Prompt (cmd) or, if you use VSCode you can use [this](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh) extension.
### Install LeRobot on Pi 🤗
On your Raspberry Pi install LeRobot using our [Installation Guide](./installation)
-In addition to these instructions, you need to install the Feetech sdk on your Pi:
+In addition to these instructions, you need to install the Feetech SDK & ZeroMQ on your Pi:
+
```bash
-pip install -e ".[feetech]"
+pip install -e ".[lekiwi]"
```
## Install LeRobot locally
+
If you already have installed LeRobot on your laptop/pc you can skip this step; otherwise, please follow along as we do the same steps we did on the Pi.
Follow our [Installation Guide](./installation)
+In addition to these instructions, you need to install the Feetech SDK & ZeroMQ on your laptop/pc:
+
+```bash
+pip install -e ".[lekiwi]"
+```
+
Great :hugs:! You are now done installing LeRobot, and we can begin assembling the SO100/SO101 arms and the mobile base :robot:.
Every time you now want to use LeRobot, you can go to the `~/lerobot` folder where we installed LeRobot and run one of the commands.
@@ -46,6 +58,7 @@ First, we will assemble the two SO100/SO101 arms. One to attach to the mobile ba
### Find the USB ports associated with motor board
To find the port for each bus servo adapter, run this script:
+
```bash
python -m lerobot.find_port
```
@@ -72,6 +85,7 @@ Where the found port is: `/dev/tty.usbmodem575E0032081` corresponding to your bo
On Linux, you might need to give access to the USB ports by running:
+
```bash
sudo chmod 666 /dev/ttyACM0
sudo chmod 666 /dev/ttyACM1
@@ -96,6 +110,7 @@ Where the found port is: `/dev/ttyACM0` corresponding to your board.
### Configure motors
+
The instructions for configuring the motors can be found in the SO101 [docs](./so101#configure-the-motors). Besides the ids for the arm motors, we also need to set the motor ids for the mobile base. These need to be in a specific order to work. Below an image of the motor ids and motor mounting positions for the mobile base. Note that we only use one Motor Control board on LeKiwi. This means the motor ids for the wheels are 7, 8 and 9.
You can run this command to setup motors for LeKiwi. It will first setup the motors for arm (id 6..1) and then setup motors for wheels (9,8,7)
@@ -113,27 +128,36 @@ python -m lerobot.setup_motors \
If you are having trouble connecting to the Mobile SO100, follow these steps to diagnose and resolve the issue.
#### 1. Verify IP Address Configuration
+
Make sure that the correct IP for the Pi is used in the commands or in your code. To check the Raspberry Pi's IP address, run (on the Pi command line):
+
```bash
hostname -I
```
#### 2. Check if Pi is reachable from laptop/pc
+
Try pinging the Raspberry Pi from your laptop:
+
```bach
ping
```
If the ping fails:
+
- Ensure the Pi is powered on and connected to the same network.
- Check if SSH is enabled on the Pi.
#### 3. Try SSH connection
+
If you can't SSH into the Pi, it might not be properly connected. Use:
+
```bash
ssh @
```
+
If you get a connection error:
+
- Ensure SSH is enabled on the Pi by running:
```bash
sudo raspi-config
@@ -158,10 +182,13 @@ python -m lerobot.calibrate \
We unified the calibration method for most robots, thus, the calibration steps for this SO100 arm are the same as the steps for the Koch and SO101. First, we have to move the robot to the position where each joint is in the middle of its range, then we press `Enter`. Secondly, we move all joints through their full range of motion. A video of this same process for the SO101 as reference can be found [here](https://huggingface.co/docs/lerobot/en/so101#calibration-video).
### Wired version
+
If you have the **wired** LeKiwi version, please run all commands on your laptop.
### Calibrate leader arm
+
Then, to calibrate the leader arm (which is attached to the laptop/pc). Run the following command of API example on your laptop:
+
@@ -171,9 +198,11 @@ python -m lerobot.calibrate \
--teleop.port=/dev/tty.usbmodem58760431551 \ # <- The port of your robot
--teleop.id=my_awesome_leader_arm # <- Give the robot a unique name
```
+
+
```python
from lerobot.teleoperators.so100_leader import SO100LeaderConfig, SO100Leader
@@ -187,6 +216,8 @@ leader.connect(calibrate=False)
leader.calibrate()
leader.disconnect()
```
+
+
@@ -196,6 +227,7 @@ leader.disconnect()
> If you're using a Mac, you might need to give Terminal permission to access your keyboard for teleoperation. Go to System Preferences > Security & Privacy > Input Monitoring and check the box for Terminal.
To teleoperate, SSH into your Raspberry Pi, and run `conda activate lerobot` and this command:
+
```bash
python -m lerobot.robots.lekiwi.lekiwi_host --robot.id=my_awesome_kiwi
```
@@ -206,7 +238,7 @@ Then on your laptop, also run `conda activate lerobot` and run the API example,
python examples/lekiwi/teleoperate.py
```
-You should see on your laptop something like this: ```[INFO] Connected to remote robot at tcp://172.17.133.91:5555 and video stream at tcp://172.17.133.91:5556.``` Now you can move the leader arm and use the keyboard (w,a,s,d) to drive forward, left, backwards, right. And use (z,x) to turn left or turn right. You can use (r,f) to increase and decrease the speed of the mobile robot. There are three speed modes, see the table below:
+You should see on your laptop something like this: `[INFO] Connected to remote robot at tcp://172.17.133.91:5555 and video stream at tcp://172.17.133.91:5556.` Now you can move the leader arm and use the keyboard (w,a,s,d) to drive forward, left, backwards, right. And use (z,x) to turn left or turn right. You can use (r,f) to increase and decrease the speed of the mobile robot. There are three speed modes, see the table below:
| Speed Mode | Linear Speed (m/s) | Rotation Speed (deg/s) |
| ---------- | ------------------ | ---------------------- |
@@ -214,7 +246,6 @@ You should see on your laptop something like this: ```[INFO] Connected to remote
| Medium | 0.25 | 60 |
| Slow | 0.1 | 30 |
-
| Key | Action |
| --- | -------------- |
| W | Move forward |
@@ -227,9 +258,10 @@ You should see on your laptop something like this: ```[INFO] Connected to remote
| F | Decrease speed |
> [!TIP]
-> If you use a different keyboard, you can change the keys for each command in the [`LeKiwiConfig`](../src/lerobot/robot_devices/robots/configs.py).
+> If you use a different keyboard, you can change the keys for each command in the [`LeKiwiConfig`](../src/lerobot/robot_devices/robots/configs.py).
### Wired version
+
If you have the **wired** LeKiwi version, please run all commands on your laptop.
## Record a dataset
@@ -239,26 +271,32 @@ Once you're familiar with teleoperation, you can record your first dataset.
We use the Hugging Face hub features for uploading your dataset. If you haven't previously used the Hub, make sure you can login via the cli using a write-access token, this token can be generated from the [Hugging Face settings](https://huggingface.co/settings/tokens).
Add your token to the CLI by running this command:
+
```bash
huggingface-cli login --token ${HUGGINGFACE_TOKEN} --add-to-git-credential
```
Then store your Hugging Face repository name in a variable:
+
```bash
HF_USER=$(huggingface-cli whoami | head -n 1)
echo $HF_USER
```
Now you can record a dataset. To record episodes and upload your dataset to the hub, execute this API example tailored for LeKiwi. Make sure to first adapt the `remote_ip`, `repo_id`, `port` and `task` in the script. If you would like to run the script for longer you can increase `NB_CYCLES_CLIENT_CONNECTION`.
+
```bash
python examples/lekiwi/record.py
```
#### Dataset upload
+
Locally, your dataset is stored in this folder: `~/.cache/huggingface/lerobot/{repo-id}`. At the end of data recording, your dataset will be uploaded on your Hugging Face page (e.g. https://huggingface.co/datasets/cadene/so101_test) that you can obtain by running:
+
```bash
echo https://huggingface.co/datasets/${HF_USER}/so101_test
```
+
Your dataset will be automatically tagged with `LeRobot` for the community to find it easily, and you can also add custom tags (in this case `tutorial` for example).
You can look for other LeRobot datasets on the hub by searching for `LeRobot` [tags](https://huggingface.co/datasets?other=LeRobot).
@@ -274,14 +312,13 @@ Avoid adding too much variation too quickly, as it may hinder your results.
If you want to dive deeper into this important topic, you can check out the [blog post](https://huggingface.co/blog/lerobot-datasets#what-makes-a-good-dataset) we wrote on what makes a good dataset.
#### Troubleshooting:
-- On Linux, if the left and right arrow keys and escape key don't have any effect during data recording, make sure you've set the `$DISPLAY` environment variable. See [pynput limitations](https://pynput.readthedocs.io/en/latest/limitations.html#linux).
+- On Linux, if the left and right arrow keys and escape key don't have any effect during data recording, make sure you've set the `$DISPLAY` environment variable. See [pynput limitations](https://pynput.readthedocs.io/en/latest/limitations.html#linux).
## Replay an episode
To replay an episode run the API example below, make sure to change `remote_ip`, `port`, LeRobotDatasetId and episode index.
-
```bash
python examples/lekiwi/replay.py
```
@@ -297,4 +334,4 @@ python examples/lekiwi/evaluate.py
```
> [!TIP]
-> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
+> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
diff --git a/src/lerobot/robots/lekiwi/lekiwi_client.py b/src/lerobot/robots/lekiwi/lekiwi_client.py
index 0ce259bb68..9a80014013 100644
--- a/src/lerobot/robots/lekiwi/lekiwi_client.py
+++ b/src/lerobot/robots/lekiwi/lekiwi_client.py
@@ -18,11 +18,10 @@
import json
import logging
from functools import cached_property
-from typing import Any, Dict, Optional, Tuple
+from typing import Any
import cv2
import numpy as np
-import zmq
from lerobot.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
@@ -35,6 +34,9 @@ class LeKiwiClient(Robot):
name = "lekiwi_client"
def __init__(self, config: LeKiwiClientConfig):
+ import zmq
+
+ self._zmq = zmq
super().__init__(config)
self.config = config
self.id = config.id
@@ -117,6 +119,7 @@ def connect(self) -> None:
"LeKiwi Daemon is already connected. Do not run `robot.connect()` twice."
)
+ zmq = self._zmq
self.zmq_context = zmq.Context()
self.zmq_cmd_socket = self.zmq_context.socket(zmq.PUSH)
zmq_cmd_locator = f"tcp://{self.remote_ip}:{self.port_zmq_cmd}"
@@ -139,8 +142,9 @@ def connect(self) -> None:
def calibrate(self) -> None:
pass
- def _poll_and_get_latest_message(self) -> Optional[str]:
+ def _poll_and_get_latest_message(self) -> str | None:
"""Polls the ZMQ socket for a limited time and returns the latest message string."""
+ zmq = self._zmq
poller = zmq.Poller()
poller.register(self.zmq_observation_socket, zmq.POLLIN)
@@ -167,7 +171,7 @@ def _poll_and_get_latest_message(self) -> Optional[str]:
return last_msg
- def _parse_observation_json(self, obs_string: str) -> Optional[Dict[str, Any]]:
+ def _parse_observation_json(self, obs_string: str) -> dict[str, Any] | None:
"""Parses the JSON observation string."""
try:
return json.loads(obs_string)
@@ -175,7 +179,7 @@ def _parse_observation_json(self, obs_string: str) -> Optional[Dict[str, Any]]:
logging.error(f"Error decoding JSON observation: {e}")
return None
- def _decode_image_from_b64(self, image_b64: str) -> Optional[np.ndarray]:
+ def _decode_image_from_b64(self, image_b64: str) -> np.ndarray | None:
"""Decodes a base64 encoded image string to an OpenCV image."""
if not image_b64:
return None
@@ -191,18 +195,18 @@ def _decode_image_from_b64(self, image_b64: str) -> Optional[np.ndarray]:
return None
def _remote_state_from_obs(
- self, observation: Dict[str, Any]
- ) -> Tuple[Dict[str, np.ndarray], Dict[str, Any]]:
+ self, observation: dict[str, Any]
+ ) -> tuple[dict[str, np.ndarray], dict[str, Any]]:
"""Extracts frames, and state from the parsed observation."""
flat_state = {key: observation.get(key, 0.0) for key in self._state_order}
state_vec = np.array([flat_state[key] for key in self._state_order], dtype=np.float32)
- obs_dict: Dict[str, Any] = {**flat_state, "observation.state": state_vec}
+ obs_dict: dict[str, Any] = {**flat_state, "observation.state": state_vec}
# Decode images
- current_frames: Dict[str, np.ndarray] = {}
+ current_frames: dict[str, np.ndarray] = {}
for cam_name, image_b64 in observation.items():
if cam_name not in self._cameras_ft:
continue
@@ -212,7 +216,7 @@ def _remote_state_from_obs(
return current_frames, obs_dict
- def _get_data(self) -> Tuple[Dict[str, np.ndarray], Dict[str, Any], Dict[str, Any]]:
+ def _get_data(self) -> tuple[dict[str, np.ndarray], dict[str, Any], dict[str, Any]]:
"""
Polls the video socket for the latest observation data.
diff --git a/src/lerobot/robots/robot.py b/src/lerobot/robots/robot.py
index 6820645ccf..2a90043809 100644
--- a/src/lerobot/robots/robot.py
+++ b/src/lerobot/robots/robot.py
@@ -13,8 +13,9 @@
# limitations under the License.
import abc
+import builtins
from pathlib import Path
-from typing import Any, Type
+from typing import Any
import draccus
@@ -39,7 +40,7 @@ class Robot(abc.ABC):
"""
# Set these in ALL subclasses
- config_class: Type[RobotConfig]
+ config_class: builtins.type[RobotConfig]
name: str
def __init__(self, config: RobotConfig):
diff --git a/src/lerobot/robots/so100_follower/so100.mdx b/src/lerobot/robots/so100_follower/so100.mdx
index f5eea6aef1..d9ff922c55 100644
--- a/src/lerobot/robots/so100_follower/so100.mdx
+++ b/src/lerobot/robots/so100_follower/so100.mdx
@@ -11,6 +11,7 @@ Follow this [README](https://github.com/TheRobotStudio/SO-ARM100/blob/main/SO100
To install LeRobot, follow our [Installation Guide](./installation)
In addition to these instructions, you need to install the Feetech SDK:
+
```bash
pip install -e ".[feetech]"
```
@@ -23,6 +24,7 @@ Unlike the SO-101, the motor connectors are not easily accessible once the arm i
### 1. Find the USB ports associated with each arm
To find the port for each bus servo adapter, run this script:
+
```bash
python -m lerobot.find_port
```
@@ -49,6 +51,7 @@ Where the found port is: `/dev/tty.usbmodem575E0032081` corresponding to your le
On Linux, you might need to give access to the USB ports by running:
+
```bash
sudo chmod 666 /dev/ttyACM0
sudo chmod 666 /dev/ttyACM1
@@ -94,9 +97,11 @@ python -m lerobot.setup_motors \
--robot.type=so100_follower \
--robot.port=/dev/tty.usbmodem585A0076841 # <- paste here the port found at previous step
```
+
+
```python
from lerobot.robots.so100_follower import SO100Follower, SO100FollowerConfig
@@ -107,10 +112,13 @@ config = SO100FollowerConfig(
follower = SO100Follower(config)
follower.setup_motors()
```
+
+
You should see the following instruction
+
```
Connect the controller board to the 'gripper' motor only and press enter.
```
@@ -120,22 +128,26 @@ As instructed, plug the gripper's motor. Make sure it's the only motor connected
Troubleshooting
- If you get an error at that point, check your cables and make sure they are plugged in properly:
-
-
Power supply
-
USB cable between your computer and the controller board
-
The 3-pin cable from the controller board to the motor
-
+If you get an error at that point, check your cables and make sure they are plugged in properly:
+
+
+
Power supply
+
USB cable between your computer and the controller board
+
The 3-pin cable from the controller board to the motor
+
If you are using a Waveshare controller board, make sure that the two jumpers are set on the `B` channel (USB).
+
You should then see the following message:
+
```
'gripper' motor id set to 6
```
Followed by the next instruction:
+
```
Connect the controller board to the 'wrist_roll' motor only and press enter.
```
@@ -150,6 +162,7 @@ Repeat the operation for each motor as instructed.
When you are done, the script will simply finish, at which point the motors are ready to be used. You can now plug the 3-pin cable from each motor to the next one, and the cable from the first motor (the 'shoulder pan' with id=1) to the controller board, which can now be attached to the base of the arm.
#### Leader
+
Do the same steps for the leader arm.
@@ -162,6 +175,7 @@ python -m lerobot.setup_motors \
+
```python
from lerobot.teleoperators.so100_leader import SO100Leader, SO100LeaderConfig
@@ -172,6 +186,8 @@ config = SO100LeaderConfig(
leader = SO100Leader(config)
leader.setup_motors()
```
+
+
@@ -184,7 +200,10 @@ leader.setup_motors()
@@ -193,6 +212,7 @@ leader.setup_motors()
Follow the video for removing gears. You need to remove the gear for the motors of the leader arm. As a result, you will only use the position encoding of the motor and reduce friction to more easily operate the leader arm.
### Clean Parts
+
Remove all support material from the 3D-printed parts. The easiest way to do this is using a small screwdriver to get underneath the support material.
### Additional Guidance
@@ -202,7 +222,10 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
@@ -216,75 +239,117 @@ This video provides visual guidance for assembling the arms, but it doesn't spec
### First Motor
**Step 2: Insert Wires**
+
- Insert two wires into the first motor.
-
+
**Step 3: Install in Base**
+
- Place the first motor into the base.
-
+
**Step 4: Secure Motor**
+
- Fasten the motor with 4 screws. Two from the bottom and two from top.
**Step 5: Attach Motor Holder**
+
- Slide over the first motor holder and fasten it using two screws (one on each side).
-
+
**Step 6: Attach Motor Horns**
+
- Install both motor horns, securing the top horn with a screw. Try not to move the motor position when attaching the motor horn, especially for the leader arms, where we removed the gears.
-
+
- Video adding motor horn
+
+ Video adding motor horn
+
**Step 7: Attach Shoulder Part**
+
- Route one wire to the back of the robot and the other to the left or towards you (see photo).
- Attach the shoulder part.
-
+
**Step 8: Secure Shoulder**
+
- Tighten the shoulder part with 4 screws on top and 4 on the bottom
-*(access bottom holes by turning the shoulder).*
+ _(access bottom holes by turning the shoulder)._
---
### Second Motor Assembly
**Step 9: Install Motor 2**
+
- Slide the second motor in from the top and link the wire from motor 1 to motor 2.
-
+
**Step 10: Attach Shoulder Holder**
+
- Add the shoulder motor holder.
- Ensure the wire from motor 1 to motor 2 goes behind the holder while the other wire is routed upward (see photo).
- This part can be tight to assemble, you can use a workbench like the image or a similar setup to push the part around the motor.
-
-
-
+
+
+
**Step 11: Secure Motor 2**
+
- Fasten the second motor with 4 screws.
**Step 12: Attach Motor Horn**
+
- Attach both motor horns to motor 2, again use the horn screw.
**Step 13: Attach Base**
+
- Install the base attachment using 2 screws.
**Step 14: Attach Upper Arm**
+
- Attach the upper arm with 4 screws on each side.
@@ -294,89 +359,144 @@ This video provides visual guidance for assembling the arms, but it doesn't spec
### Third Motor Assembly
**Step 15: Install Motor 3**
+
- Route the motor cable from motor 2 through the cable holder to motor 3, then secure motor 3 with 4 screws.
**Step 16: Attach Motor Horn**
+
- Attach both motor horns to motor 3 and secure one again with a horn screw.
-
+
**Step 17: Attach Forearm**
+
- Connect the forearm to motor 3 using 4 screws on each side.
-
+
---
### Fourth Motor Assembly
**Step 18: Install Motor 4**
+
- Slide in motor 4, attach the cable from motor 3, and secure the cable in its holder with a screw.
-
-
+
+
**Step 19: Attach Motor Holder 4**
+
- Install the fourth motor holder (a tight fit). Ensure one wire is routed upward and the wire from motor 3 is routed downward (see photo).
-
+
**Step 20: Secure Motor 4 & Attach Horn**
+
- Fasten motor 4 with 4 screws and attach its motor horns, use for one a horn screw.
-
+
---
### Wrist Assembly
**Step 21: Install Motor 5**
+
- Insert motor 5 into the wrist holder and secure it with 2 front screws.
-
+
**Step 22: Attach Wrist**
+
- Connect the wire from motor 4 to motor 5. And already insert the other wire for the gripper.
- Secure the wrist to motor 4 using 4 screws on both sides.
-
+
**Step 23: Attach Wrist Horn**
+
- Install only one motor horn on the wrist motor and secure it with a horn screw.
-
+
---
### Follower Configuration
**Step 24: Attach Gripper**
+
- Attach the gripper to motor 5.
-
+
**Step 25: Install Gripper Motor**
+
- Insert the gripper motor, connect the motor wire from motor 5 to motor 6, and secure it with 3 screws on each side.
-
+
**Step 26: Attach Gripper Horn & Claw**
+
- Attach the motor horns and again use a horn screw.
- Install the gripper claw and secure it with 4 screws on both sides.
-
+
**Step 27: Mount Controller**
+
- Attach the motor controller to the back of the robot.
-
-
+
+
-*Assembly complete – proceed to Leader arm assembly.*
+_Assembly complete – proceed to Leader arm assembly._
---
@@ -385,31 +505,54 @@ This video provides visual guidance for assembling the arms, but it doesn't spec
For the leader configuration, perform **Steps 1–23**. Make sure that you removed the motor gears from the motors.
**Step 24: Attach Leader Holder**
+
- Mount the leader holder onto the wrist and secure it with a screw.
-
+
**Step 25: Attach Handle**
+
- Attach the handle to motor 5 using 4 screws.
-
+
**Step 26: Install Gripper Motor**
+
- Insert the gripper motor, secure it with 3 screws on each side, attach a motor horn using a horn screw, and connect the motor wire.
-
+
**Step 27: Attach Trigger**
+
- Attach the follower trigger with 4 screws.
-
+
**Step 28: Mount Controller**
+
- Attach the motor controller to the back of the robot.
-
-
+
+
## Calibrate
@@ -430,9 +573,11 @@ python -m lerobot.calibrate \
--robot.port=/dev/tty.usbmodem58760431551 \ # <- The port of your robot
--robot.id=my_awesome_follower_arm # <- Give the robot a unique name
```
+
+
```python
from lerobot.robots.so100_follower import SO100FollowerConfig, SO100Follower
@@ -446,6 +591,8 @@ follower.connect(calibrate=False)
follower.calibrate()
follower.disconnect()
```
+
+
@@ -464,9 +611,11 @@ python -m lerobot.calibrate \
--teleop.port=/dev/tty.usbmodem58760431551 \ # <- The port of your robot
--teleop.id=my_awesome_leader_arm # <- Give the robot a unique name
```
+
+
```python
from lerobot.teleoperators.so100_leader import SO100LeaderConfig, SO100Leader
@@ -480,10 +629,12 @@ leader.connect(calibrate=False)
leader.calibrate()
leader.disconnect()
```
+
+
Congrats 🎉, your robot is all set to learn a task on its own. Start training it by following this tutorial: [Getting started with real-world robots](./getting_started_real_world_robot)
> [!TIP]
-> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
+> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
diff --git a/src/lerobot/robots/so101_follower/so101.mdx b/src/lerobot/robots/so101_follower/so101.mdx
index c49807d938..e84336e174 100644
--- a/src/lerobot/robots/so101_follower/so101.mdx
+++ b/src/lerobot/robots/so101_follower/so101.mdx
@@ -12,6 +12,7 @@ And advise if it's your first time printing or if you don't own a 3D printer.
To install LeRobot, follow our [Installation Guide](./installation)
In addition to these instructions, you need to install the Feetech SDK:
+
```bash
pip install -e ".[feetech]"
```
@@ -20,16 +21,17 @@ pip install -e ".[feetech]"
The follower arm uses 6x STS3215 motors with 1/345 gearing. The leader, however, uses three differently geared motors to make sure it can both sustain its own weight and it can be moved without requiring much force. Which motor is needed for which joint is shown in the table below.
-| Leader-Arm Axis | Motor | Gear Ratio |
-|-----------------|:-------:|:----------:|
-| Base / Shoulder Pan | 1 | 1 / 191 |
-| Shoulder Lift | 2 | 1 / 345 |
-| Elbow Flex | 3 | 1 / 191 |
-| Wrist Flex | 4 | 1 / 147 |
-| Wrist Roll | 5 | 1 / 147 |
-| Gripper | 6 | 1 / 147 |
+| Leader-Arm Axis | Motor | Gear Ratio |
+| ------------------- | :---: | :--------: |
+| Base / Shoulder Pan | 1 | 1 / 191 |
+| Shoulder Lift | 2 | 1 / 345 |
+| Elbow Flex | 3 | 1 / 191 |
+| Wrist Flex | 4 | 1 / 147 |
+| Wrist Roll | 5 | 1 / 147 |
+| Gripper | 6 | 1 / 147 |
### Clean Parts
+
Remove all support material from the 3D-printed parts. The easiest way to do this is using a small screwdriver to get underneath the support material.
### Joint 1
@@ -44,7 +46,10 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
@@ -57,7 +62,10 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
@@ -69,7 +77,10 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
@@ -81,7 +92,10 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
@@ -93,7 +107,10 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
@@ -109,7 +126,10 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
@@ -123,7 +143,10 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
@@ -135,6 +158,7 @@ Remove all support material from the 3D-printed parts. The easiest way to do thi
### 1. Find the USB ports associated with each arm
To find the port for each bus servo adapter, run this script:
+
```bash
python -m lerobot.find_port
```
@@ -161,6 +185,7 @@ Where the found port is: `/dev/tty.usbmodem575E0032081` corresponding to your le
On Linux, you might need to give access to the USB ports by running:
+
```bash
sudo chmod 666 /dev/ttyACM0
sudo chmod 666 /dev/ttyACM1
@@ -198,7 +223,10 @@ The video below shows the sequence of steps for setting the motor ids.
@@ -214,9 +242,11 @@ python -m lerobot.setup_motors \
--robot.type=so101_follower \
--robot.port=/dev/tty.usbmodem585A0076841 # <- paste here the port found at previous step
```
+
+
```python
from lerobot.robots.so101_follower import SO101Follower, SO101FollowerConfig
@@ -227,10 +257,13 @@ config = SO101FollowerConfig(
follower = SO101Follower(config)
follower.setup_motors()
```
+
+
You should see the following instruction
+
```bash
Connect the controller board to the 'gripper' motor only and press enter.
```
@@ -240,22 +273,26 @@ As instructed, plug the gripper's motor. Make sure it's the only motor connected
Troubleshooting
- If you get an error at that point, check your cables and make sure they are plugged in properly:
-
-
Power supply
-
USB cable between your computer and the controller board
-
The 3-pin cable from the controller board to the motor
-
+If you get an error at that point, check your cables and make sure they are plugged in properly:
+
+
+
Power supply
+
USB cable between your computer and the controller board
+
The 3-pin cable from the controller board to the motor
+
+
+If you are using a Waveshare controller board, make sure that the two jumpers are set on the `B` channel (USB).
- If you are using a Waveshare controller board, make sure that the two jumpers are set on the `B` channel (USB).
You should then see the following message:
+
```bash
'gripper' motor id set to 6
```
Followed by the next instruction:
+
```bash
Connect the controller board to the 'wrist_roll' motor only and press enter.
```
@@ -270,6 +307,7 @@ Repeat the operation for each motor as instructed.
When you are done, the script will simply finish, at which point the motors are ready to be used. You can now plug the 3-pin cable from each motor to the next one, and the cable from the first motor (the 'shoulder pan' with id=1) to the controller board, which can now be attached to the base of the arm.
#### Leader
+
Do the same steps for the leader arm.
@@ -280,9 +318,11 @@ python -m lerobot.setup_motors \
--teleop.type=so101_leader \
--teleop.port=/dev/tty.usbmodem575E0031751 # <- paste here the port found at previous step
```
+
+
```python
from lerobot.teleoperators.so101_leader import SO101Leader, SO101LeaderConfig
@@ -293,6 +333,8 @@ config = SO101LeaderConfig(
leader = SO101Leader(config)
leader.setup_motors()
```
+
+
@@ -314,9 +356,11 @@ python -m lerobot.calibrate \
--robot.port=/dev/tty.usbmodem58760431551 \ # <- The port of your robot
--robot.id=my_awesome_follower_arm # <- Give the robot a unique name
```
+
+
```python
from lerobot.robots.so101_follower import SO101FollowerConfig, SO101Follower
@@ -330,6 +374,8 @@ follower.connect(calibrate=False)
follower.calibrate()
follower.disconnect()
```
+
+
@@ -339,7 +385,10 @@ The video below shows how to perform the calibration. First you need to move the
@@ -356,9 +405,11 @@ python -m lerobot.calibrate \
--teleop.port=/dev/tty.usbmodem58760431551 \ # <- The port of your robot
--teleop.id=my_awesome_leader_arm # <- Give the robot a unique name
```
+
+
```python
from lerobot.teleoperators.so101_leader import SO101LeaderConfig, SO101Leader
@@ -372,10 +423,12 @@ leader.connect(calibrate=False)
leader.calibrate()
leader.disconnect()
```
+
+
Congrats 🎉, your robot is all set to learn a task on its own. Start training it by following this tutorial: [Getting started with real-world robots](./getting_started_real_world_robot)
> [!TIP]
-> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
+> If you have any questions or need help, please reach out on [Discord](https://discord.com/invite/s3KuuzsPFb).
diff --git a/src/lerobot/robots/stretch3/README.md b/src/lerobot/robots/stretch3/README.md
index 982e725719..7247322863 100644
--- a/src/lerobot/robots/stretch3/README.md
+++ b/src/lerobot/robots/stretch3/README.md
@@ -5,16 +5,17 @@ This tutorial explains how to use [Stretch 3](https://hello-robot.com/stretch-3-
Familiarize yourself with Stretch by following its [tutorials](https://docs.hello-robot.com/0.3/getting_started/hello_robot/) (recommended).
To use LeRobot on Stretch, 3 options are available:
+
- [tethered setup](https://docs.hello-robot.com/0.3/getting_started/connecting_to_stretch/#tethered-setup)
- [untethered setup](https://docs.hello-robot.com/0.3/getting_started/connecting_to_stretch/#untethered-setup)
- ssh directly into Stretch (you will first need to install and configure openssh-server on stretch using one of the two above setups)
-
## Install LeRobot
On Stretch's CLI, follow these steps:
1. [Install Miniconda](https://docs.anaconda.com/miniconda/#quick-command-line-install):
+
```bash
mkdir -p ~/miniconda3
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh
@@ -24,6 +25,7 @@ rm ~/miniconda3/miniconda.sh
```
2. Comment out these lines in `~/.profile` (this can mess up paths used by conda and ~/.local/bin should already be in your PATH)
+
```
# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/.local/bin" ] ; then
@@ -34,21 +36,25 @@ fi
3. Restart shell or `source ~/.bashrc`
4. Create and activate a fresh conda environment for lerobot
+
```bash
conda create -y -n lerobot python=3.10 && conda activate lerobot
```
5. Clone LeRobot:
+
```bash
git clone https://github.com/huggingface/lerobot.git ~/lerobot
```
6. When using `miniconda`, install `ffmpeg` in your environment:
+
```bash
conda install ffmpeg -c conda-forge
```
7. Install LeRobot with stretch dependencies:
+
```bash
cd ~/lerobot && pip install -e ".[stretch]"
```
@@ -56,6 +62,7 @@ cd ~/lerobot && pip install -e ".[stretch]"
> **Note:** If you get this message, you can ignore it: `ERROR: pip's dependency resolver does not currently take into account all the packages that are installed.`
8. Run a [system check](https://docs.hello-robot.com/0.3/getting_started/stretch_hardware_overview/#system-check) to make sure your robot is ready:
+
```bash
stretch_system_check.py
```
@@ -63,6 +70,7 @@ stretch_system_check.py
> **Note:** You may need to free the "robot process" after booting Stretch by running `stretch_free_robot_process.py`. For more info this Stretch's [doc](https://docs.hello-robot.com/0.3/getting_started/stretch_hardware_overview/#turning-off-gamepad-teleoperation).
You should get something like this:
+
```bash
For use with S T R E T C H (R) from Hello Robot Inc.
---------------------------------------------------------------------
@@ -89,11 +97,13 @@ Serial Number = stretch-se3-3054
**Calibrate (Optional)**
Before operating Stretch, you need to [home](https://docs.hello-robot.com/0.3/getting_started/stretch_hardware_overview/#homing) it first. Be mindful about giving Stretch some space as this procedure will move the robot's arm and gripper. Now run this command:
+
```bash
python lerobot/scripts/control_robot.py \
--robot.type=stretch \
--control.type=calibrate
```
+
This is equivalent to running `stretch_robot_home.py`
> **Note:** If you run any of the LeRobot scripts below and Stretch is not properly homed, it will automatically home/calibrate first.
@@ -104,28 +114,33 @@ Before trying teleoperation, you need to activate the gamepad controller by pres
Now try out teleoperation (see above documentation to learn about the gamepad controls):
> **NOTE:** To visualize the data, enable `--control.display_data=true`. This streams the data using `rerun`.
+
```bash
python lerobot/scripts/control_robot.py \
--robot.type=stretch \
--control.type=teleoperate
```
+
This is essentially the same as running `stretch_gamepad_teleop.py`
**Record a dataset**
Once you're familiar with the gamepad controls and after a bit of practice, you can try to record your first dataset with Stretch.
If you want to use the Hugging Face hub features for uploading your dataset and you haven't previously done it, make sure you've logged in using a write-access token, which can be generated from the [Hugging Face settings](https://huggingface.co/settings/tokens):
+
```bash
huggingface-cli login --token ${HUGGINGFACE_TOKEN} --add-to-git-credential
```
Store your Hugging Face repository name in a variable to run these commands:
+
```bash
HF_USER=$(huggingface-cli whoami | head -n 1)
echo $HF_USER
```
Record one episode:
+
```bash
python lerobot/scripts/control_robot.py \
--robot.type=stretch \
@@ -145,6 +160,7 @@ python lerobot/scripts/control_robot.py \
**Replay an episode**
Now try to replay this episode (make sure the robot's initial position is the same):
+
```bash
python lerobot/scripts/control_robot.py \
--robot.type=stretch \
diff --git a/src/lerobot/robots/viperx/README.md b/src/lerobot/robots/viperx/README.md
index 445368e7aa..4e90c99c71 100644
--- a/src/lerobot/robots/viperx/README.md
+++ b/src/lerobot/robots/viperx/README.md
@@ -4,12 +4,12 @@ This tutorial explains how to use [Aloha and Aloha 2 stationary](https://www.tro
Follow the [documentation from Trossen Robotics](https://docs.trossenrobotics.com/aloha_docs/2.0/getting_started/stationary/hardware_setup.html) for setting up the hardware and plugging the 4 arms and 4 cameras to your computer.
-
## Install LeRobot
On your computer:
1. [Install Miniconda](https://docs.anaconda.com/miniconda/#quick-command-line-install):
+
```bash
mkdir -p ~/miniconda3
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh
@@ -21,29 +21,34 @@ rm ~/miniconda3/miniconda.sh
2. Restart shell or `source ~/.bashrc`
3. Create and activate a fresh conda environment for lerobot
+
```bash
conda create -y -n lerobot python=3.10 && conda activate lerobot
```
4. Clone LeRobot:
+
```bash
git clone https://github.com/huggingface/lerobot.git ~/lerobot
```
5. When using `miniconda`, install `ffmpeg` in your environment:
+
```bash
conda install ffmpeg -c conda-forge
```
6. Install LeRobot with dependencies for the Aloha motors (dynamixel) and cameras (intelrealsense):
+
```bash
cd ~/lerobot && pip install -e ".[dynamixel, intelrealsense]"
```
## Teleoperate
-**/!\ FOR SAFETY, READ THIS /!\**
+\*\*/!\ FOR SAFETY, READ THIS /!\*\*
Teleoperation consists in manually operating the leader arms to move the follower arms. Importantly:
+
1. Make sure your leader arms are in the same position as the follower arms, so that the follower arms don't move too fast to match the leader arms,
2. Our code assumes that your robot has been assembled following Trossen Robotics instructions. This allows us to skip calibration, as we use the pre-defined calibration files in `.cache/calibration/aloha_default`. If you replace a motor, make sure you follow the exact instructions from Trossen Robotics.
@@ -59,6 +64,7 @@ python lerobot/scripts/control_robot.py \
```
By adding `--robot.max_relative_target=5`, we override the default value for `max_relative_target` defined in [`AlohaRobotConfig`](lerobot/robot_devices/robots/configs.py). It is expected to be `5` to limit the magnitude of the movement for more safety, but the teleoperation won't be smooth. When you feel confident, you can disable this limit by adding `--robot.max_relative_target=null` to the command line:
+
```bash
python lerobot/scripts/control_robot.py \
--robot.type=aloha \
@@ -71,17 +77,20 @@ python lerobot/scripts/control_robot.py \
Once you're familiar with teleoperation, you can record your first dataset with Aloha.
If you want to use the Hugging Face hub features for uploading your dataset and you haven't previously done it, make sure you've logged in using a write-access token, which can be generated from the [Hugging Face settings](https://huggingface.co/settings/tokens):
+
```bash
huggingface-cli login --token ${HUGGINGFACE_TOKEN} --add-to-git-credential
```
Store your Hugging Face repository name in a variable to run these commands:
+
```bash
HF_USER=$(huggingface-cli whoami | head -n 1)
echo $HF_USER
```
Record 2 episodes and upload your dataset to the hub:
+
```bash
python lerobot/scripts/control_robot.py \
--robot.type=aloha \
@@ -101,11 +110,13 @@ python lerobot/scripts/control_robot.py \
## Visualize a dataset
If you uploaded your dataset to the hub with `--control.push_to_hub=true`, you can [visualize your dataset online](https://huggingface.co/spaces/lerobot/visualize_dataset) by copy pasting your repo id given by:
+
```bash
echo ${HF_USER}/aloha_test
```
If you didn't upload with `--control.push_to_hub=false`, you can also visualize it locally with:
+
```bash
python -m lerobot.scripts.visualize_dataset_html \
--repo-id ${HF_USER}/aloha_test
@@ -113,10 +124,11 @@ python -m lerobot.scripts.visualize_dataset_html \
## Replay an episode
-**/!\ FOR SAFETY, READ THIS /!\**
+\*\*/!\ FOR SAFETY, READ THIS /!\*\*
Replay consists in automatically replaying the sequence of actions (i.e. goal positions for your motors) recorded in a given dataset episode. Make sure the current initial position of your robot is similar to the one in your episode, so that your follower arms don't move too fast to go to the first goal positions. For safety, you might want to add `--robot.max_relative_target=5` to your command line as explained above.
Now try to replay the first episode on your robot:
+
```bash
python lerobot/scripts/control_robot.py \
--robot.type=aloha \
@@ -130,6 +142,7 @@ python lerobot/scripts/control_robot.py \
## Train a policy
To train a policy to control your robot, use the [`python -m lerobot.scripts.train`](../src/lerobot/scripts/train.py) script. A few arguments are required. Here is an example command:
+
```bash
python -m lerobot.scripts.train \
--dataset.repo_id=${HF_USER}/aloha_test \
@@ -141,10 +154,11 @@ python -m lerobot.scripts.train \
```
Let's explain it:
+
1. We provided the dataset as argument with `--dataset.repo_id=${HF_USER}/aloha_test`.
2. We provided the policy with `policy.type=act`. This loads configurations from [`configuration_act.py`](../src/lerobot/policies/act/configuration_act.py). Importantly, this policy will automatically adapt to the number of motor states, motor actions and cameras of your robot (e.g. `laptop` and `phone`) which have been saved in your dataset.
-4. We provided `policy.device=cuda` since we are training on a Nvidia GPU, but you could use `policy.device=mps` to train on Apple silicon.
-5. We provided `wandb.enable=true` to use [Weights and Biases](https://docs.wandb.ai/quickstart) for visualizing training plots. This is optional but if you use it, make sure you are logged in by running `wandb login`.
+3. We provided `policy.device=cuda` since we are training on a Nvidia GPU, but you could use `policy.device=mps` to train on Apple silicon.
+4. We provided `wandb.enable=true` to use [Weights and Biases](https://docs.wandb.ai/quickstart) for visualizing training plots. This is optional but if you use it, make sure you are logged in by running `wandb login`.
For more information on the `train` script see the previous tutorial: [`examples/4_train_policy_with_script.md`](../examples/4_train_policy_with_script.md)
@@ -153,6 +167,7 @@ Training should take several hours. You will find checkpoints in `outputs/train/
## Evaluate your policy
You can use the `record` function from [`lerobot/scripts/control_robot.py`](../src/lerobot/scripts/control_robot.py) but with a policy checkpoint as input. For instance, run this command to record 10 evaluation episodes:
+
```bash
python lerobot/scripts/control_robot.py \
--robot.type=aloha \
@@ -171,7 +186,8 @@ python lerobot/scripts/control_robot.py \
```
As you can see, it's almost the same command as previously used to record your training dataset. Two things changed:
-1. There is an additional `--control.policy.path` argument which indicates the path to your policy checkpoint with (e.g. `outputs/train/eval_act_aloha_test/checkpoints/last/pretrained_model`). You can also use the model repository if you uploaded a model checkpoint to the hub (e.g. `${HF_USER}/act_aloha_test`).
+
+1. There is an additional `--control.policy.path` argument which indicates the path to your policy checkpoint with (e.g. `outputs/train/eval_act_aloha_test/checkpoints/last/pretrained_model`). You can also use the model repository if you uploaded a model checkpoint to the hub (e.g. `${HF_USER}/act_aloha_test`).
2. The name of dataset begins by `eval` to reflect that you are running inference (e.g. `${HF_USER}/eval_act_aloha_test`).
3. We use `--control.num_image_writer_processes=1` instead of the default value (`0`). On our computer, using a dedicated process to write images from the 4 cameras on disk allows to reach constant 30 fps during inference. Feel free to explore different values for `--control.num_image_writer_processes`.
diff --git a/src/lerobot/scripts/eval.py b/src/lerobot/scripts/eval.py
index d85ac27b39..7c5aec48a9 100644
--- a/src/lerobot/scripts/eval.py
+++ b/src/lerobot/scripts/eval.py
@@ -50,12 +50,12 @@
import logging
import threading
import time
+from collections.abc import Callable
from contextlib import nullcontext
from copy import deepcopy
from dataclasses import asdict
from pathlib import Path
from pprint import pformat
-from typing import Callable
import einops
import gymnasium as gym
diff --git a/src/lerobot/scripts/rl/crop_dataset_roi.py b/src/lerobot/scripts/rl/crop_dataset_roi.py
index 0b71b53634..69904b7401 100644
--- a/src/lerobot/scripts/rl/crop_dataset_roi.py
+++ b/src/lerobot/scripts/rl/crop_dataset_roi.py
@@ -18,7 +18,6 @@
import json
from copy import deepcopy
from pathlib import Path
-from typing import Dict, Tuple
import cv2
import torch
@@ -162,10 +161,10 @@ def get_image_from_lerobot_dataset(dataset: LeRobotDataset):
def convert_lerobot_dataset_to_cropper_lerobot_dataset(
original_dataset: LeRobotDataset,
- crop_params_dict: Dict[str, Tuple[int, int, int, int]],
+ crop_params_dict: dict[str, tuple[int, int, int, int]],
new_repo_id: str,
new_dataset_root: str,
- resize_size: Tuple[int, int] = (128, 128),
+ resize_size: tuple[int, int] = (128, 128),
push_to_hub: bool = False,
task: str = "",
) -> LeRobotDataset:
diff --git a/src/lerobot/scripts/rl/gym_manipulator.py b/src/lerobot/scripts/rl/gym_manipulator.py
index 673043b6eb..c8be6b7dd7 100644
--- a/src/lerobot/scripts/rl/gym_manipulator.py
+++ b/src/lerobot/scripts/rl/gym_manipulator.py
@@ -39,8 +39,9 @@
import logging
import time
from collections import deque
+from collections.abc import Sequence
from threading import Lock
-from typing import Annotated, Any, Sequence
+from typing import Annotated, Any
import gymnasium as gym
import numpy as np
diff --git a/src/lerobot/scripts/rl/learner.py b/src/lerobot/scripts/rl/learner.py
index cb88895cfa..f9f3901ce7 100644
--- a/src/lerobot/scripts/rl/learner.py
+++ b/src/lerobot/scripts/rl/learner.py
@@ -87,12 +87,10 @@
from lerobot.utils.random_utils import set_seed
from lerobot.utils.train_utils import (
get_step_checkpoint_dir,
+ load_training_state as utils_load_training_state,
save_checkpoint,
update_last_checkpoint,
)
-from lerobot.utils.train_utils import (
- load_training_state as utils_load_training_state,
-)
from lerobot.utils.transition import move_state_dict_to_device, move_transition_to_device
from lerobot.utils.utils import (
format_big_number,
diff --git a/src/lerobot/scripts/server/configs.py b/src/lerobot/scripts/server/configs.py
index 7058842ae4..5be46485ea 100644
--- a/src/lerobot/scripts/server/configs.py
+++ b/src/lerobot/scripts/server/configs.py
@@ -12,8 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from collections.abc import Callable
from dataclasses import dataclass, field
-from typing import Callable
import torch
diff --git a/src/lerobot/scripts/server/robot_client.py b/src/lerobot/scripts/server/robot_client.py
index 44d9cdf776..68166de6f8 100644
--- a/src/lerobot/scripts/server/robot_client.py
+++ b/src/lerobot/scripts/server/robot_client.py
@@ -36,10 +36,11 @@
import pickle # nosec
import threading
import time
+from collections.abc import Callable
from dataclasses import asdict
from pprint import pformat
from queue import Queue
-from typing import Any, Callable, Optional
+from typing import Any
import draccus
import grpc
@@ -231,7 +232,7 @@ def _inspect_action_queue(self):
def _aggregate_action_queues(
self,
incoming_actions: list[TimedAction],
- aggregate_fn: Optional[Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = None,
+ aggregate_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None,
):
"""Finds the same timestep actions in the queue and aggregates them using the aggregate_fn"""
if aggregate_fn is None:
diff --git a/src/lerobot/scripts/visualize_dataset.py b/src/lerobot/scripts/visualize_dataset.py
index 37db66ddf4..51ead0dd10 100644
--- a/src/lerobot/scripts/visualize_dataset.py
+++ b/src/lerobot/scripts/visualize_dataset.py
@@ -65,8 +65,8 @@
import gc
import logging
import time
+from collections.abc import Iterator
from pathlib import Path
-from typing import Iterator
import numpy as np
import rerun as rr
diff --git a/src/lerobot/teleoperators/homunculus/homunculus_arm.py b/src/lerobot/teleoperators/homunculus/homunculus_arm.py
index dfce0c88eb..6f5137af93 100644
--- a/src/lerobot/teleoperators/homunculus/homunculus_arm.py
+++ b/src/lerobot/teleoperators/homunculus/homunculus_arm.py
@@ -18,7 +18,7 @@
import threading
from collections import deque
from pprint import pformat
-from typing import Deque, Dict, Optional
+from typing import Deque
import serial
@@ -60,7 +60,7 @@ def __init__(self, config: HomunculusArmConfig):
self.n: int = n
self.alpha: float = 2 / (n + 1)
# one deque *per joint* so we can inspect raw history if needed
- self._buffers: Dict[str, Deque[int]] = {
+ self._buffers: dict[str, Deque[int]] = {
joint: deque(maxlen=n)
for joint in (
"shoulder_pitch",
@@ -73,7 +73,7 @@ def __init__(self, config: HomunculusArmConfig):
)
}
# running EMA value per joint – lazily initialised on first read
- self._ema: Dict[str, Optional[float]] = dict.fromkeys(self._buffers)
+ self._ema: dict[str, float | None] = dict.fromkeys(self._buffers)
self._state: dict[str, float] | None = None
self.new_state_event = threading.Event()
@@ -217,9 +217,9 @@ def _normalize(self, values: dict[str, int]) -> dict[str, float]:
return normalized_values
- def _apply_ema(self, raw: Dict[str, int]) -> Dict[str, float]:
+ def _apply_ema(self, raw: dict[str, int]) -> dict[str, float]:
"""Update buffers & running EMA values; return smoothed dict."""
- smoothed: Dict[str, float] = {}
+ smoothed: dict[str, float] = {}
for joint, value in raw.items():
# maintain raw history
self._buffers[joint].append(value)
diff --git a/src/lerobot/teleoperators/homunculus/homunculus_glove.py b/src/lerobot/teleoperators/homunculus/homunculus_glove.py
index d367a2a7c6..7b0ced9f60 100644
--- a/src/lerobot/teleoperators/homunculus/homunculus_glove.py
+++ b/src/lerobot/teleoperators/homunculus/homunculus_glove.py
@@ -18,7 +18,7 @@
import threading
from collections import deque
from pprint import pformat
-from typing import Deque, Dict, Optional
+from typing import Deque
import serial
@@ -97,9 +97,9 @@ def __init__(self, config: HomunculusGloveConfig):
self.n: int = n
self.alpha: float = 2 / (n + 1)
# one deque *per joint* so we can inspect raw history if needed
- self._buffers: Dict[str, Deque[int]] = {joint: deque(maxlen=n) for joint in self.joints}
+ self._buffers: dict[str, Deque[int]] = {joint: deque(maxlen=n) for joint in self.joints}
# running EMA value per joint – lazily initialised on first read
- self._ema: Dict[str, Optional[float]] = dict.fromkeys(self._buffers)
+ self._ema: dict[str, float | None] = dict.fromkeys(self._buffers)
self._state: dict[str, float] | None = None
self.new_state_event = threading.Event()
@@ -248,9 +248,9 @@ def _normalize(self, values: dict[str, int]) -> dict[str, float]:
return normalized_values
- def _apply_ema(self, raw: Dict[str, int]) -> Dict[str, int]:
+ def _apply_ema(self, raw: dict[str, int]) -> dict[str, int]:
"""Update buffers & running EMA values; return smoothed dict as integers."""
- smoothed: Dict[str, int] = {}
+ smoothed: dict[str, int] = {}
for joint, value in raw.items():
# maintain raw history
self._buffers[joint].append(value)
diff --git a/src/lerobot/teleoperators/teleoperator.py b/src/lerobot/teleoperators/teleoperator.py
index 49f259c17c..c360ee7bba 100644
--- a/src/lerobot/teleoperators/teleoperator.py
+++ b/src/lerobot/teleoperators/teleoperator.py
@@ -13,8 +13,9 @@
# limitations under the License.
import abc
+import builtins
from pathlib import Path
-from typing import Any, Type
+from typing import Any
import draccus
@@ -37,7 +38,7 @@ class Teleoperator(abc.ABC):
"""
# Set these in ALL subclasses
- config_class: Type[TeleoperatorConfig]
+ config_class: builtins.type[TeleoperatorConfig]
name: str
def __init__(self, config: TeleoperatorConfig):
diff --git a/src/lerobot/templates/lerobot_modelcard_template.md b/src/lerobot/templates/lerobot_modelcard_template.md
index 64ad7196c8..7b7aaa84af 100644
--- a/src/lerobot/templates/lerobot_modelcard_template.md
+++ b/src/lerobot/templates/lerobot_modelcard_template.md
@@ -1,7 +1,8 @@
---
# For reference on model card metadata, see the spec: https://github.com/huggingface/hub-docs/blob/main/modelcard.md?plain=1
# Doc / guide: https://huggingface.co/docs/hub/model-cards
-{{ card_data }}
+# prettier-ignore
+{{card_data}}
---
# Model Card for {{ model_name | default("Model ID", true) }}
@@ -53,7 +54,7 @@ python -m lerobot.scripts.train \
--wandb.enable=true
```
-*Writes checkpoints to `outputs/train//checkpoints/`.*
+_Writes checkpoints to `outputs/train//checkpoints/`._
### Evaluate the policy/run inference
@@ -71,4 +72,4 @@ Prefix the dataset repo with **eval\_** and supply `--policy.path` pointing to a
## Model Details
-* **License:** {{ license | default("\[More Information Needed]", true) }}
+- **License:** {{ license | default("\[More Information Needed]", true) }}
diff --git a/src/lerobot/utils/benchmark.py b/src/lerobot/utils/benchmark.py
index 4b08e6f6d8..d9e5e62bbf 100644
--- a/src/lerobot/utils/benchmark.py
+++ b/src/lerobot/utils/benchmark.py
@@ -46,11 +46,13 @@ class TimeBenchmark(ContextDecorator):
benchmark = TimeBenchmark()
+
def context_manager_example():
with benchmark:
time.sleep(0.01)
print(f"Block took {benchmark.result_ms:.2f} milliseconds")
+
threads = []
for _ in range(3):
t1 = threading.Thread(target=context_manager_example)
diff --git a/src/lerobot/utils/buffer.py b/src/lerobot/utils/buffer.py
index 7f8d989dd2..d9ffa899cf 100644
--- a/src/lerobot/utils/buffer.py
+++ b/src/lerobot/utils/buffer.py
@@ -15,8 +15,9 @@
# limitations under the License.
import functools
+from collections.abc import Callable, Sequence
from contextlib import suppress
-from typing import Callable, Sequence, TypedDict
+from typing import TypedDict
import torch
import torch.nn.functional as F # noqa: N812
diff --git a/src/lerobot/utils/hub.py b/src/lerobot/utils/hub.py
index df7435c0fc..566701b31c 100644
--- a/src/lerobot/utils/hub.py
+++ b/src/lerobot/utils/hub.py
@@ -12,9 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import builtins
from pathlib import Path
from tempfile import TemporaryDirectory
-from typing import Any, Type, TypeVar
+from typing import Any, TypeVar
from huggingface_hub import HfApi
from huggingface_hub.utils import validate_hf_hub_args
@@ -85,7 +86,7 @@ def _save_pretrained(self, save_directory: Path) -> None:
@classmethod
@validate_hf_hub_args
def from_pretrained(
- cls: Type[T],
+ cls: builtins.type[T],
pretrained_name_or_path: str | Path,
*,
force_download: bool = False,
diff --git a/src/lerobot/utils/random_utils.py b/src/lerobot/utils/random_utils.py
index 31fed1da6f..da3ecf37fe 100644
--- a/src/lerobot/utils/random_utils.py
+++ b/src/lerobot/utils/random_utils.py
@@ -14,9 +14,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import random
+from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
-from typing import Any, Generator
+from typing import Any
import numpy as np
import torch
diff --git a/src/lerobot/utils/utils.py b/src/lerobot/utils/utils.py
index 7a9717dcec..6e13646b0c 100644
--- a/src/lerobot/utils/utils.py
+++ b/src/lerobot/utils/utils.py
@@ -185,10 +185,10 @@ def print_cuda_memory_usage():
gc.collect()
# Also clear the cache if you want to fully release the memory
torch.cuda.empty_cache()
- print("Current GPU Memory Allocated: {:.2f} MB".format(torch.cuda.memory_allocated(0) / 1024**2))
- print("Maximum GPU Memory Allocated: {:.2f} MB".format(torch.cuda.max_memory_allocated(0) / 1024**2))
- print("Current GPU Memory Reserved: {:.2f} MB".format(torch.cuda.memory_reserved(0) / 1024**2))
- print("Maximum GPU Memory Reserved: {:.2f} MB".format(torch.cuda.max_memory_reserved(0) / 1024**2))
+ print(f"Current GPU Memory Allocated: {torch.cuda.memory_allocated(0) / 1024**2:.2f} MB")
+ print(f"Maximum GPU Memory Allocated: {torch.cuda.max_memory_allocated(0) / 1024**2:.2f} MB")
+ print(f"Current GPU Memory Reserved: {torch.cuda.memory_reserved(0) / 1024**2:.2f} MB")
+ print(f"Maximum GPU Memory Reserved: {torch.cuda.max_memory_reserved(0) / 1024**2:.2f} MB")
def capture_timestamp_utc():
diff --git a/tests/configs/test_plugin_loading.py b/tests/configs/test_plugin_loading.py
index e81057c958..3ec60a4858 100644
--- a/tests/configs/test_plugin_loading.py
+++ b/tests/configs/test_plugin_loading.py
@@ -15,9 +15,9 @@
# limitations under the License.
import sys
+from collections.abc import Generator
from dataclasses import dataclass
from pathlib import Path
-from typing import Generator
import pytest
diff --git a/tests/mocks/mock_dynamixel.py b/tests/mocks/mock_dynamixel.py
index 00403d146f..84026fc344 100644
--- a/tests/mocks/mock_dynamixel.py
+++ b/tests/mocks/mock_dynamixel.py
@@ -15,7 +15,7 @@
# limitations under the License.
import abc
-from typing import Callable
+from collections.abc import Callable
import dynamixel_sdk as dxl
import serial
diff --git a/tests/mocks/mock_feetech.py b/tests/mocks/mock_feetech.py
index 041c094217..33cbc41d6f 100644
--- a/tests/mocks/mock_feetech.py
+++ b/tests/mocks/mock_feetech.py
@@ -15,7 +15,7 @@
# limitations under the License.
import abc
-from typing import Callable
+from collections.abc import Callable
import scservo_sdk as scs
import serial
diff --git a/tests/motors/test_dynamixel.py b/tests/motors/test_dynamixel.py
index d990b5b0f1..e0dbe713a2 100644
--- a/tests/motors/test_dynamixel.py
+++ b/tests/motors/test_dynamixel.py
@@ -16,7 +16,7 @@
import re
import sys
-from typing import Generator
+from collections.abc import Generator
from unittest.mock import MagicMock, patch
import pytest
diff --git a/tests/motors/test_feetech.py b/tests/motors/test_feetech.py
index d6ea1db208..31e4a9018b 100644
--- a/tests/motors/test_feetech.py
+++ b/tests/motors/test_feetech.py
@@ -16,7 +16,7 @@
import re
import sys
-from typing import Generator
+from collections.abc import Generator
from unittest.mock import MagicMock, patch
import pytest
diff --git a/tests/utils/test_replay_buffer.py b/tests/utils/test_replay_buffer.py
index 2602760326..a53d7ba8cb 100644
--- a/tests/utils/test_replay_buffer.py
+++ b/tests/utils/test_replay_buffer.py
@@ -15,7 +15,7 @@
# limitations under the License.
import sys
-from typing import Callable
+from collections.abc import Callable
import pytest
import torch