mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-23 17:56:07 +00:00
Refactorgym_manipulator.py using the universal pipeline (#1650)
* Migrate gym_manipulator to use the pipeline Added get_teleop_events function to capture relevant events from teleop devices unrelated to actions * Added the capability to record a dataset * Added the replay functionality with the pipeline * Refactored `actor.py` to use the pipeline * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * RL works at this commit - fixed actor.py and bugs in gym_manipulator * change folder structure to reduce the size of gym_manip * Refactored hilserl config * Remove dataset and mode from HilSerlEnvConfig to a GymManipulatorConfig to reduce verbose of configs during training * format docs * removed get_teleop_events from abc * Refactor environment configuration and processing pipeline for GymHIL support. Removed device attribute from HILSerlRobotEnvConfig, added DummyTeleopDevice for simulation, and updated processor creation to accommodate GymHIL environments. * Improved typing for HILRobotEnv config and GymManipulator config * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Migrated `gym_manipulator` to use a more modular structure similar to phone teleop * Refactor gripper handling and transition processing in HIL and robot kinematic processors - Updated gripper position handling to use a consistent key format across processors - Improved the EEReferenceAndDelta class to handle reference joint positions. - Added support for discrete gripper actions in the GripperVelocityToJoint processor. - Refactored the gym manipulator to improve modularity and clarity in processing steps. * Added delta_action_processor mapping wrapper * Added missing file delta_action_processor and improved imports in `gym_manipulator` * nit * Added missing file joint_observation_processor * Enhance processing architecture with new teleoperation processors - Introduced `AddTeleopActionAsComplimentaryData` and `AddTeleopEventsAsInfo` for integrating teleoperator actions and events into transitions. - Added `Torch2NumpyActionProcessor` and `Numpy2TorchActionProcessor` for seamless conversion between PyTorch tensors and NumPy arrays. - Updated `__init__.py` to include new processors in module exports, improving modularity and clarity in the processing pipeline. - GymHIL is now fully supported with HIL using the pipeline * Refactor configuration structure for gym_hil integration - Renamed sections for better readability, such as changing "Gym Wrappers Configuration" to "Processor Configuration." - Enhanced documentation with clear examples for dataset collection and policy evaluation configurations. * Enhance reset configuration and teleoperation event handling - Added `terminate_on_success` parameter to `ResetConfig` and `InterventionActionProcessor` for controlling episode termination behavior upon success detection. - Updated documentation to clarify the impact of `terminate_on_success` on data collection for reward classifier training. - Refactored teleoperation event handling to use `TeleopEvents` constants for improved readability and maintainability across various modules. * fix(keyboard teleop), delta action keys * Added transform features and feature contract * Added transform features for image crop * Enum for TeleopEvents * Update tranform_features delta action proc --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
+382
-56
@@ -4,7 +4,13 @@ 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.
|
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.
|
Together these elements let HIL-SERL reach near-perfect task success and faster cycle times than imitation-only baselines.
|
||||||
|
|
||||||
@@ -56,30 +62,243 @@ pip install -e ".[hilserl]"
|
|||||||
|
|
||||||
### Understanding Configuration
|
### Understanding Configuration
|
||||||
|
|
||||||
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:
|
The training process begins with proper configuration for the HILSerl environment. The main configuration class is `GymManipulatorConfig` in `lerobot/scripts/rl/gym_manipulator.py`, which contains nested `HILSerlRobotEnvConfig` and `DatasetConfig`. The configuration is organized into focused, nested sub-configs:
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
```python
|
```python
|
||||||
|
class GymManipulatorConfig:
|
||||||
|
env: HILSerlRobotEnvConfig # Environment configuration (nested)
|
||||||
|
dataset: DatasetConfig # Dataset recording/replay configuration (nested)
|
||||||
|
mode: str | None = None # "record", "replay", or None (for training)
|
||||||
|
device: str = "cpu" # Compute device
|
||||||
|
|
||||||
class HILSerlRobotEnvConfig(EnvConfig):
|
class HILSerlRobotEnvConfig(EnvConfig):
|
||||||
robot: RobotConfig | None = None # Main robot agent (defined in `lerobot/robots`)
|
robot: RobotConfig | None = None # Main robot agent (defined in `lerobot/robots`)
|
||||||
teleop: TeleoperatorConfig | None = None # Teleoperator agent, e.g., gamepad or leader arm, (defined in `lerobot/teleoperators`)
|
teleop: TeleoperatorConfig | None = None # Teleoperator agent, e.g., gamepad or leader arm
|
||||||
wrapper: EnvTransformConfig | None = None # Environment wrapper settings; check `lerobot/scripts/server/gym_manipulator.py`
|
processor: HILSerlProcessorConfig # Processing pipeline configuration (nested)
|
||||||
fps: int = 10 # Control frequency
|
|
||||||
name: str = "real_robot" # Environment name
|
name: str = "real_robot" # Environment name
|
||||||
mode: str = None # "record", "replay", or None (for training)
|
task: str | None = None # Task identifier
|
||||||
repo_id: str | None = None # LeRobot dataset repository ID
|
fps: int = 10 # Control frequency
|
||||||
dataset_root: str | None = None # Local dataset root (optional)
|
|
||||||
task: str = "" # Task identifier
|
# Nested processor configuration
|
||||||
num_episodes: int = 10 # Number of episodes for recording
|
class HILSerlProcessorConfig:
|
||||||
episode: int = 0 # episode index for replay
|
control_mode: str = "gamepad" # Control mode
|
||||||
device: str = "cuda" # Compute device
|
observation: ObservationConfig | None = None # Observation processing settings
|
||||||
push_to_hub: bool = True # Whether to push the recorded datasets to Hub
|
image_preprocessing: ImagePreprocessingConfig | None = None # Image crop/resize settings
|
||||||
pretrained_policy_name_or_path: str | None = None # For policy loading
|
gripper: GripperConfig | None = None # Gripper control and penalty settings
|
||||||
reward_classifier_pretrained_path: str | None = None # For reward model
|
reset: ResetConfig | None = None # Environment reset and timing settings
|
||||||
number_of_steps_after_success: int = 0 # For reward classifier, collect more positive examples after a success to train a classifier
|
inverse_kinematics: InverseKinematicsConfig | None = None # IK processing settings
|
||||||
|
reward_classifier: RewardClassifierConfig | None = None # Reward classifier settings
|
||||||
|
max_gripper_pos: float | None = 100.0 # Maximum gripper position
|
||||||
|
|
||||||
|
# Sub-configuration classes
|
||||||
|
class ObservationConfig:
|
||||||
|
add_joint_velocity_to_observation: bool = False # Add joint velocities to state
|
||||||
|
add_current_to_observation: bool = False # Add motor currents to state
|
||||||
|
add_ee_pose_to_observation: bool = False # Add end-effector pose to state
|
||||||
|
display_cameras: bool = False # Display camera feeds during execution
|
||||||
|
|
||||||
|
class ImagePreprocessingConfig:
|
||||||
|
crop_params_dict: dict[str, tuple[int, int, int, int]] | None = None # Image cropping parameters
|
||||||
|
resize_size: tuple[int, int] | None = None # Target image size
|
||||||
|
|
||||||
|
class GripperConfig:
|
||||||
|
use_gripper: bool = True # Enable gripper control
|
||||||
|
gripper_penalty: float = 0.0 # Penalty for inappropriate gripper usage
|
||||||
|
gripper_penalty_in_reward: bool = False # Include gripper penalty in reward
|
||||||
|
|
||||||
|
class ResetConfig:
|
||||||
|
fixed_reset_joint_positions: Any | None = None # Joint positions for reset
|
||||||
|
reset_time_s: float = 5.0 # Time to wait during reset
|
||||||
|
control_time_s: float = 20.0 # Maximum episode duration
|
||||||
|
terminate_on_success: bool = True # Whether to terminate episodes on success detection
|
||||||
|
|
||||||
|
class InverseKinematicsConfig:
|
||||||
|
urdf_path: str | None = None # Path to robot URDF file
|
||||||
|
target_frame_name: str | None = None # End-effector frame name
|
||||||
|
end_effector_bounds: dict[str, list[float]] | None = None # EE workspace bounds
|
||||||
|
end_effector_step_sizes: dict[str, float] | None = None # EE step sizes per axis
|
||||||
|
|
||||||
|
class RewardClassifierConfig:
|
||||||
|
pretrained_path: str | None = None # Path to pretrained reward classifier
|
||||||
|
success_threshold: float = 0.5 # Success detection threshold
|
||||||
|
success_reward: float = 1.0 # Reward value for successful episodes
|
||||||
|
|
||||||
|
# Dataset configuration
|
||||||
|
class DatasetConfig:
|
||||||
|
repo_id: str # LeRobot dataset repository ID
|
||||||
|
dataset_root: str # Local dataset root directory
|
||||||
|
task: str # Task identifier
|
||||||
|
num_episodes: int # Number of episodes for recording
|
||||||
|
episode: int # Episode index for replay
|
||||||
|
push_to_hub: bool # Whether to push datasets to Hub
|
||||||
```
|
```
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
### Processor Pipeline Architecture
|
||||||
|
|
||||||
|
HIL-SERL uses a modular processor pipeline architecture that processes robot observations and actions through a series of composable steps. The pipeline is divided into two main components:
|
||||||
|
|
||||||
|
#### Environment Processor Pipeline
|
||||||
|
|
||||||
|
The environment processor (`env_processor`) handles incoming observations and environment state:
|
||||||
|
|
||||||
|
1. **VanillaObservationProcessor**: Converts raw robot observations into standardized format
|
||||||
|
2. **JointVelocityProcessor** (optional): Adds joint velocity information to observations
|
||||||
|
3. **MotorCurrentProcessor** (optional): Adds motor current readings to observations
|
||||||
|
4. **ForwardKinematicsJointsToEE** (optional): Computes end-effector pose from joint positions
|
||||||
|
5. **ImageCropResizeProcessor** (optional): Crops and resizes camera images
|
||||||
|
6. **TimeLimitProcessor** (optional): Enforces episode time limits
|
||||||
|
7. **GripperPenaltyProcessor** (optional): Applies penalties for inappropriate gripper usage
|
||||||
|
8. **RewardClassifierProcessor** (optional): Automated reward detection using vision models
|
||||||
|
9. **ToBatchProcessor**: Converts data to batch format for neural network processing
|
||||||
|
10. **DeviceProcessor**: Moves data to the specified compute device (CPU/GPU)
|
||||||
|
|
||||||
|
#### Action Processor Pipeline
|
||||||
|
|
||||||
|
The action processor (`action_processor`) handles outgoing actions and human interventions:
|
||||||
|
|
||||||
|
1. **AddTeleopActionAsComplimentaryData**: Captures teleoperator actions for logging
|
||||||
|
2. **AddTeleopEventsAsInfo**: Records intervention events and episode control signals
|
||||||
|
3. **AddRobotObservationAsComplimentaryData**: Stores raw robot state for processing
|
||||||
|
4. **InterventionActionProcessor**: Handles human interventions and episode termination
|
||||||
|
5. **Inverse Kinematics Pipeline** (when enabled):
|
||||||
|
- **MapDeltaActionToRobotAction**: Converts delta actions to robot action format
|
||||||
|
- **EEReferenceAndDelta**: Computes end-effector reference and delta movements
|
||||||
|
- **EEBoundsAndSafety**: Enforces workspace safety bounds
|
||||||
|
- **InverseKinematicsEEToJoints**: Converts end-effector actions to joint targets
|
||||||
|
- **GripperVelocityToJoint**: Handles gripper control commands
|
||||||
|
|
||||||
|
#### Configuration Examples
|
||||||
|
|
||||||
|
**Basic Observation Processing**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"processor": {
|
||||||
|
"observation": {
|
||||||
|
"add_joint_velocity_to_observation": true,
|
||||||
|
"add_current_to_observation": false,
|
||||||
|
"display_cameras": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Image Processing**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"processor": {
|
||||||
|
"image_preprocessing": {
|
||||||
|
"crop_params_dict": {
|
||||||
|
"observation.images.front": [180, 250, 120, 150],
|
||||||
|
"observation.images.side": [180, 207, 180, 200]
|
||||||
|
},
|
||||||
|
"resize_size": [128, 128]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inverse Kinematics Setup**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"processor": {
|
||||||
|
"inverse_kinematics": {
|
||||||
|
"urdf_path": "path/to/robot.urdf",
|
||||||
|
"target_frame_name": "end_effector",
|
||||||
|
"end_effector_bounds": {
|
||||||
|
"min": [0.16, -0.08, 0.03],
|
||||||
|
"max": [0.24, 0.2, 0.1]
|
||||||
|
},
|
||||||
|
"end_effector_step_sizes": {
|
||||||
|
"x": 0.02,
|
||||||
|
"y": 0.02,
|
||||||
|
"z": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Observation Processing
|
||||||
|
|
||||||
|
The HIL-SERL framework supports additional observation processing features that can improve policy learning:
|
||||||
|
|
||||||
|
#### Joint Velocity Processing
|
||||||
|
|
||||||
|
Enable joint velocity estimation to provide the policy with motion information:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"processor": {
|
||||||
|
"observation": {
|
||||||
|
"add_joint_velocity_to_observation": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This processor:
|
||||||
|
|
||||||
|
- Estimates joint velocities using finite differences between consecutive joint position readings
|
||||||
|
- Adds velocity information to the observation state vector
|
||||||
|
- Useful for policies that need motion awareness for dynamic tasks
|
||||||
|
|
||||||
|
#### Motor Current Processing
|
||||||
|
|
||||||
|
Monitor motor currents to detect contact forces and load conditions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"processor": {
|
||||||
|
"observation": {
|
||||||
|
"add_current_to_observation": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This processor:
|
||||||
|
|
||||||
|
- Reads motor current values from the robot's control system
|
||||||
|
- Adds current measurements to the observation state vector
|
||||||
|
- Helps detect contact events, object weights, and mechanical resistance
|
||||||
|
- Useful for contact-rich manipulation tasks
|
||||||
|
|
||||||
|
#### Combined Observation Processing
|
||||||
|
|
||||||
|
You can enable multiple observation processing features simultaneously:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"processor": {
|
||||||
|
"observation": {
|
||||||
|
"add_joint_velocity_to_observation": true,
|
||||||
|
"add_current_to_observation": true,
|
||||||
|
"add_ee_pose_to_observation": false,
|
||||||
|
"display_cameras": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Enabling additional observation features increases the state space dimensionality, which may require adjusting your policy network architecture and potentially collecting more training data.
|
||||||
|
|
||||||
### Finding Robot Workspace Bounds
|
### Finding Robot Workspace Bounds
|
||||||
|
|
||||||
Before collecting demonstrations, you need to determine the appropriate operational bounds for your robot.
|
Before collecting demonstrations, you need to determine the appropriate operational bounds for your robot.
|
||||||
@@ -130,22 +349,56 @@ With the bounds defined, you can safely collect demonstrations for training. Tra
|
|||||||
|
|
||||||
Create a configuration file for recording demonstrations (or edit an existing one like [env_config_so100.json](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/blob/main/env_config_so100.json)):
|
Create a configuration file for recording demonstrations (or edit an existing one like [env_config_so100.json](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/blob/main/env_config_so100.json)):
|
||||||
|
|
||||||
1. Set `mode` to `"record"`
|
1. Set `mode` to `"record"` at the root level
|
||||||
2. Specify a unique `repo_id` for your dataset (e.g., "username/task_name")
|
2. Specify a unique `repo_id` for your dataset in the `dataset` section (e.g., "username/task_name")
|
||||||
3. Set `num_episodes` to the number of demonstrations you want to collect
|
3. Set `num_episodes` in the `dataset` section to the number of demonstrations you want to collect
|
||||||
4. Set `crop_params_dict` to `null` initially (we'll determine crops later)
|
4. Set `env.processor.image_preprocessing.crop_params_dict` to `{}` initially (we'll determine crops later)
|
||||||
5. Configure `robot`, `cameras`, and other hardware settings
|
5. Configure `env.robot`, `env.teleop`, and other hardware settings in the `env` section
|
||||||
|
|
||||||
Example configuration section:
|
Example configuration section:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"mode": "record",
|
{
|
||||||
"repo_id": "username/pick_lift_cube",
|
"env": {
|
||||||
"dataset_root": null,
|
"type": "gym_manipulator",
|
||||||
"task": "pick_and_lift",
|
"name": "real_robot",
|
||||||
"num_episodes": 15,
|
"fps": 10,
|
||||||
"episode": 0,
|
"processor": {
|
||||||
"push_to_hub": true
|
"control_mode": "gamepad",
|
||||||
|
"observation": {
|
||||||
|
"display_cameras": false
|
||||||
|
},
|
||||||
|
"image_preprocessing": {
|
||||||
|
"crop_params_dict": {},
|
||||||
|
"resize_size": [128, 128]
|
||||||
|
},
|
||||||
|
"gripper": {
|
||||||
|
"use_gripper": true,
|
||||||
|
"gripper_penalty": 0.0
|
||||||
|
},
|
||||||
|
"reset": {
|
||||||
|
"reset_time_s": 5.0,
|
||||||
|
"control_time_s": 20.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"robot": {
|
||||||
|
// ... robot configuration ...
|
||||||
|
},
|
||||||
|
"teleop": {
|
||||||
|
// ... teleoperator configuration ...
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dataset": {
|
||||||
|
"repo_id": "username/pick_lift_cube",
|
||||||
|
"dataset_root": null,
|
||||||
|
"task": "pick_and_lift",
|
||||||
|
"num_episodes": 15,
|
||||||
|
"episode": 0,
|
||||||
|
"push_to_hub": true
|
||||||
|
},
|
||||||
|
"mode": "record",
|
||||||
|
"device": "cpu"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using a Teleoperation Device
|
### Using a Teleoperation Device
|
||||||
@@ -191,10 +444,20 @@ The gamepad provides a very convenient way to control the robot and the episode
|
|||||||
To setup the gamepad, you need to set the `control_mode` to `"gamepad"` and define the `teleop` section in the configuration file.
|
To setup the gamepad, you need to set the `control_mode` to `"gamepad"` and define the `teleop` section in the configuration file.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
"teleop": {
|
"teleop": {
|
||||||
"type": "gamepad",
|
"type": "gamepad",
|
||||||
"use_gripper": true
|
"use_gripper": true
|
||||||
},
|
},
|
||||||
|
"processor": {
|
||||||
|
"control_mode": "gamepad",
|
||||||
|
"gripper": {
|
||||||
|
"use_gripper": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -216,11 +479,21 @@ The SO101 leader arm has reduced gears that allows it to move and track the foll
|
|||||||
To setup the SO101 leader, you need to set the `control_mode` to `"leader"` and define the `teleop` section in the configuration file.
|
To setup the SO101 leader, you need to set the `control_mode` to `"leader"` and define the `teleop` section in the configuration file.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
"teleop": {
|
"teleop": {
|
||||||
"type": "so101_leader",
|
"type": "so101_leader",
|
||||||
"port": "/dev/tty.usbmodem585A0077921", # check your port number
|
"port": "/dev/tty.usbmodem585A0077921",
|
||||||
"use_degrees": true
|
"use_degrees": true
|
||||||
},
|
},
|
||||||
|
"processor": {
|
||||||
|
"control_mode": "leader",
|
||||||
|
"gripper": {
|
||||||
|
"use_gripper": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In order to annotate the success/failure of the episode, **you will need** to use a keyboard to press `s` for success, `esc` for failure.
|
In order to annotate the success/failure of the episode, **you will need** to use a keyboard to press `s` for success, `esc` for failure.
|
||||||
@@ -251,7 +524,7 @@ python -m lerobot.scripts.rl.gym_manipulator --config_path src/lerobot/configs/e
|
|||||||
|
|
||||||
During recording:
|
During recording:
|
||||||
|
|
||||||
1. The robot will reset to the initial position defined in the configuration file `fixed_reset_joint_positions`
|
1. The robot will reset to the initial position defined in the configuration file `env.processor.reset.fixed_reset_joint_positions`
|
||||||
2. Complete the task successfully
|
2. Complete the task successfully
|
||||||
3. The episode ends with a reward of 1 when you press the "success" button
|
3. The episode ends with a reward of 1 when you press the "success" button
|
||||||
4. If the time limit is reached, or the fail button is pressed, the episode ends with a reward of 0
|
4. If the time limit is reached, or the fail button is pressed, the episode ends with a reward of 0
|
||||||
@@ -310,11 +583,19 @@ observation.images.front: [180, 250, 120, 150]
|
|||||||
Add these crop parameters to your training configuration:
|
Add these crop parameters to your training configuration:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"crop_params_dict": {
|
{
|
||||||
"observation.images.side": [180, 207, 180, 200],
|
"env": {
|
||||||
"observation.images.front": [180, 250, 120, 150]
|
"processor": {
|
||||||
},
|
"image_preprocessing": {
|
||||||
"resize_size": [128, 128]
|
"crop_params_dict": {
|
||||||
|
"observation.images.side": [180, 207, 180, 200],
|
||||||
|
"observation.images.front": [180, 250, 120, 150]
|
||||||
|
},
|
||||||
|
"resize_size": [128, 128]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Recommended image resolution**
|
**Recommended image resolution**
|
||||||
@@ -343,26 +624,52 @@ python -m lerobot.scripts.rl.gym_manipulator --config_path src/lerobot/configs/r
|
|||||||
|
|
||||||
**Key Parameters for Data Collection**
|
**Key Parameters for Data Collection**
|
||||||
|
|
||||||
- **mode**: set it to `"record"` to collect a dataset
|
- **mode**: set it to `"record"` to collect a dataset (at root level)
|
||||||
- **repo_id**: `"hf_username/dataset_name"`, name of the dataset and repo on the hub
|
- **dataset.repo_id**: `"hf_username/dataset_name"`, name of the dataset and repo on the hub
|
||||||
- **num_episodes**: Number of episodes to record
|
- **dataset.num_episodes**: Number of episodes to record
|
||||||
- **number_of_steps_after_success**: Number of additional frames to record after a success (reward=1) is detected
|
- **env.processor.reset.terminate_on_success**: Whether to automatically terminate episodes when success is detected (default: `true`)
|
||||||
- **fps**: Number of frames per second to record
|
- **env.fps**: Number of frames per second to record
|
||||||
- **push_to_hub**: Whether to push the dataset to the hub
|
- **dataset.push_to_hub**: Whether to push the dataset to the hub
|
||||||
|
|
||||||
The `number_of_steps_after_success` parameter is crucial as it allows you to collect more positive examples. When a success is detected, the system will continue recording for the specified number of steps while maintaining the reward=1 label. Otherwise, there won't be enough states in the dataset labeled to 1 to train a good classifier.
|
The `env.processor.reset.terminate_on_success` parameter allows you to control episode termination behavior. When set to `false`, episodes will continue even after success is detected, allowing you to collect more positive examples with the reward=1 label. This is crucial for training reward classifiers as it provides more success state examples in your dataset. When set to `true` (default), episodes terminate immediately upon success detection.
|
||||||
|
|
||||||
|
**Important**: For reward classifier training, set `terminate_on_success: false` to collect sufficient positive examples. For regular HIL-SERL training, keep it as `true` to enable automatic episode termination when the task is completed successfully.
|
||||||
|
|
||||||
Example configuration section for data collection:
|
Example configuration section for data collection:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"env": {
|
||||||
|
"type": "gym_manipulator",
|
||||||
|
"name": "real_robot",
|
||||||
|
"fps": 10,
|
||||||
|
"processor": {
|
||||||
|
"reset": {
|
||||||
|
"reset_time_s": 5.0,
|
||||||
|
"control_time_s": 20.0,
|
||||||
|
"terminate_on_success": false
|
||||||
|
},
|
||||||
|
"gripper": {
|
||||||
|
"use_gripper": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"robot": {
|
||||||
|
// ... robot configuration ...
|
||||||
|
},
|
||||||
|
"teleop": {
|
||||||
|
// ... teleoperator configuration ...
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dataset": {
|
||||||
|
"repo_id": "hf_username/dataset_name",
|
||||||
|
"dataset_root": "data/your_dataset",
|
||||||
|
"task": "reward_classifier_task",
|
||||||
|
"num_episodes": 20,
|
||||||
|
"episode": 0,
|
||||||
|
"push_to_hub": true
|
||||||
|
},
|
||||||
"mode": "record",
|
"mode": "record",
|
||||||
"repo_id": "hf_username/dataset_name",
|
"device": "cpu"
|
||||||
"dataset_root": "data/your_dataset",
|
|
||||||
"num_episodes": 20,
|
|
||||||
"push_to_hub": true,
|
|
||||||
"fps": 10,
|
|
||||||
"number_of_steps_after_success": 15
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -421,9 +728,17 @@ To use your trained reward classifier, configure the `HILSerlRobotEnvConfig` to
|
|||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
```python
|
```python
|
||||||
env_config = HILSerlRobotEnvConfig(
|
config = GymManipulatorConfig(
|
||||||
reward_classifier_pretrained_path="path_to_your_pretrained_trained_model",
|
env=HILSerlRobotEnvConfig(
|
||||||
# Other environment parameters
|
processor=HILSerlProcessorConfig(
|
||||||
|
reward_classifier=RewardClassifierConfig(
|
||||||
|
pretrained_path="path_to_your_pretrained_trained_model"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# Other environment parameters
|
||||||
|
),
|
||||||
|
dataset=DatasetConfig(...),
|
||||||
|
mode=None # For training
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
@@ -432,7 +747,18 @@ or set the argument in the json config file.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"reward_classifier_pretrained_path": "path_to_your_pretrained_model"
|
"env": {
|
||||||
|
"processor": {
|
||||||
|
"reward_classifier": {
|
||||||
|
"pretrained_path": "path_to_your_pretrained_model",
|
||||||
|
"success_threshold": 0.7,
|
||||||
|
"success_reward": 1.0
|
||||||
|
},
|
||||||
|
"reset": {
|
||||||
|
"terminate_on_success": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+56
-30
@@ -32,9 +32,12 @@ To use `gym_hil` with LeRobot, you need to create a configuration file. An examp
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "hil",
|
"env": {
|
||||||
"name": "franka_sim",
|
"type": "gym_manipulator",
|
||||||
"task": "PandaPickCubeGamepad-v0",
|
"name": "gym_hil",
|
||||||
|
"task": "PandaPickCubeGamepad-v0",
|
||||||
|
"fps": 10
|
||||||
|
},
|
||||||
"device": "cuda"
|
"device": "cuda"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -45,28 +48,40 @@ Available tasks:
|
|||||||
- `PandaPickCubeGamepad-v0`: With gamepad control
|
- `PandaPickCubeGamepad-v0`: With gamepad control
|
||||||
- `PandaPickCubeKeyboard-v0`: With keyboard control
|
- `PandaPickCubeKeyboard-v0`: With keyboard control
|
||||||
|
|
||||||
### Gym Wrappers Configuration
|
### Processor Configuration
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"wrapper": {
|
{
|
||||||
"gripper_penalty": -0.02,
|
"env": {
|
||||||
"control_time_s": 15.0,
|
"processor": {
|
||||||
"use_gripper": true,
|
"control_mode": "gamepad",
|
||||||
"fixed_reset_joint_positions": [0.0, 0.195, 0.0, -2.43, 0.0, 2.62, 0.785],
|
"gripper": {
|
||||||
"end_effector_step_sizes": {
|
"use_gripper": true,
|
||||||
"x": 0.025,
|
"gripper_penalty": -0.02
|
||||||
"y": 0.025,
|
},
|
||||||
"z": 0.025
|
"reset": {
|
||||||
},
|
"control_time_s": 15.0,
|
||||||
"control_mode": "gamepad"
|
"fixed_reset_joint_positions": [
|
||||||
|
0.0, 0.195, 0.0, -2.43, 0.0, 2.62, 0.785
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inverse_kinematics": {
|
||||||
|
"end_effector_step_sizes": {
|
||||||
|
"x": 0.025,
|
||||||
|
"y": 0.025,
|
||||||
|
"z": 0.025
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Important parameters:
|
Important parameters:
|
||||||
|
|
||||||
- `gripper_penalty`: Penalty for excessive gripper movement
|
- `gripper.gripper_penalty`: Penalty for excessive gripper movement
|
||||||
- `use_gripper`: Whether to enable gripper control
|
- `gripper.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
|
- `inverse_kinematics.end_effector_step_sizes`: Size of the steps in the x,y,z axes of the end-effector
|
||||||
- `control_mode`: Set to `"gamepad"` to use a gamepad controller
|
- `control_mode`: Set to `"gamepad"` to use a gamepad controller
|
||||||
|
|
||||||
## Running with HIL RL of LeRobot
|
## Running with HIL RL of LeRobot
|
||||||
@@ -75,39 +90,50 @@ Important parameters:
|
|||||||
|
|
||||||
To run the environment, set mode to null:
|
To run the environment, set mode to null:
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
```bash
|
||||||
```python
|
|
||||||
python -m lerobot.scripts.rl.gym_manipulator --config_path path/to/gym_hil_env.json
|
python -m lerobot.scripts.rl.gym_manipulator --config_path path/to/gym_hil_env.json
|
||||||
```
|
```
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
|
|
||||||
### Recording a Dataset
|
### Recording a Dataset
|
||||||
|
|
||||||
To collect a dataset, set the mode to `record` whilst defining the repo_id and number of episodes to record:
|
To collect a dataset, set the mode to `record` whilst defining the repo_id and number of episodes to record:
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
```json
|
||||||
```python
|
{
|
||||||
|
"env": {
|
||||||
|
"type": "gym_manipulator",
|
||||||
|
"name": "gym_hil",
|
||||||
|
"task": "PandaPickCubeGamepad-v0"
|
||||||
|
},
|
||||||
|
"dataset": {
|
||||||
|
"repo_id": "username/sim_dataset",
|
||||||
|
"dataset_root": null,
|
||||||
|
"task": "pick_cube",
|
||||||
|
"num_episodes": 10,
|
||||||
|
"episode": 0,
|
||||||
|
"push_to_hub": true
|
||||||
|
},
|
||||||
|
"mode": "record"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
python -m lerobot.scripts.rl.gym_manipulator --config_path path/to/gym_hil_env.json
|
python -m lerobot.scripts.rl.gym_manipulator --config_path path/to/gym_hil_env.json
|
||||||
```
|
```
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
|
|
||||||
### Training a Policy
|
### 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:
|
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:
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
```bash
|
||||||
```python
|
|
||||||
python -m lerobot.scripts.rl.actor --config_path path/to/train_gym_hil_env.json
|
python -m lerobot.scripts.rl.actor --config_path path/to/train_gym_hil_env.json
|
||||||
```
|
```
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
|
|
||||||
In a different terminal, run the learner server:
|
In a different terminal, run the learner server:
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
```bash
|
||||||
```python
|
|
||||||
python -m lerobot.scripts.rl.learner --config_path path/to/train_gym_hil_env.json
|
python -m lerobot.scripts.rl.learner --config_path path/to/train_gym_hil_env.json
|
||||||
```
|
```
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
+53
-5
@@ -24,11 +24,36 @@ pip install -e ".[hilserl]"
|
|||||||
|
|
||||||
To use `gym_hil` with LeRobot, you need to use a configuration file. An example config file can be found [here](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/blob/main/env_config_gym_hil_il.json).
|
To use `gym_hil` with LeRobot, you need to use a configuration file. An example config file can be found [here](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/blob/main/env_config_gym_hil_il.json).
|
||||||
|
|
||||||
To teleoperate and collect a dataset, we need to modify this config file and you should add your `repo_id` here: `"repo_id": "il_gym",` and `"num_episodes": 30,` and make sure you set `mode` to `record`, "mode": "record".
|
To teleoperate and collect a dataset, we need to modify this config file. Here's an example configuration for imitation learning data collection:
|
||||||
|
|
||||||
If you do not have a Nvidia GPU also change `"device": "cuda"` parameter in the config file (for example to `mps` for MacOS).
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"type": "gym_manipulator",
|
||||||
|
"name": "gym_hil",
|
||||||
|
"task": "PandaPickCubeGamepad-v0",
|
||||||
|
"fps": 10
|
||||||
|
},
|
||||||
|
"dataset": {
|
||||||
|
"repo_id": "your_username/il_gym",
|
||||||
|
"dataset_root": null,
|
||||||
|
"task": "pick_cube",
|
||||||
|
"num_episodes": 30,
|
||||||
|
"episode": 0,
|
||||||
|
"push_to_hub": true
|
||||||
|
},
|
||||||
|
"mode": "record",
|
||||||
|
"device": "cuda"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
By default the config file assumes you use a controller. To use your keyboard please change the envoirment specified at `"task"` in the config file and set it to `"PandaPickCubeKeyboard-v0"`.
|
Key configuration points:
|
||||||
|
|
||||||
|
- Set your `repo_id` in the `dataset` section: `"repo_id": "your_username/il_gym"`
|
||||||
|
- Set `num_episodes: 30` to collect 30 demonstration episodes
|
||||||
|
- Ensure `mode` is set to `"record"`
|
||||||
|
- If you don't have an NVIDIA GPU, change `"device": "cuda"` to `"mps"` for macOS or `"cpu"`
|
||||||
|
- To use keyboard instead of gamepad, change `"task"` to `"PandaPickCubeKeyboard-v0"`
|
||||||
|
|
||||||
Then we can run this command to start:
|
Then we can run this command to start:
|
||||||
|
|
||||||
@@ -140,9 +165,32 @@ huggingface-cli upload ${HF_USER}/il_sim_test${CKPT} \
|
|||||||
|
|
||||||
## Evaluate your policy in Sim
|
## Evaluate your policy in Sim
|
||||||
|
|
||||||
To evaluate your policy we have to use the config file that can be found [here](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/blob/main/eval_config_gym_hil.json).
|
To evaluate your policy we have to use a configuration file. An example can be found [here](https://huggingface.co/datasets/aractingi/lerobot-example-config-files/blob/main/eval_config_gym_hil.json).
|
||||||
|
|
||||||
Make sure to replace the `repo_id` with the dataset you trained on, for example `pepijn223/il_sim_dataset` and replace the `pretrained_policy_name_or_path` with your model id, for example `pepijn223/il_sim_model`
|
Here's an example evaluation configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"type": "gym_manipulator",
|
||||||
|
"name": "gym_hil",
|
||||||
|
"task": "PandaPickCubeGamepad-v0",
|
||||||
|
"fps": 10
|
||||||
|
},
|
||||||
|
"dataset": {
|
||||||
|
"repo_id": "your_username/il_sim_dataset",
|
||||||
|
"dataset_root": null,
|
||||||
|
"task": "pick_cube"
|
||||||
|
},
|
||||||
|
"pretrained_policy_name_or_path": "your_username/il_sim_model",
|
||||||
|
"device": "cuda"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure to replace:
|
||||||
|
|
||||||
|
- `repo_id` with the dataset you trained on (e.g., `your_username/il_sim_dataset`)
|
||||||
|
- `pretrained_policy_name_or_path` with your model ID (e.g., `your_username/il_sim_model`)
|
||||||
|
|
||||||
Then you can run this command to visualize your trained policy
|
Then you can run this command to visualize your trained policy
|
||||||
|
|
||||||
|
|||||||
+57
-86
@@ -161,35 +161,73 @@ class XarmEnv(EnvConfig):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VideoRecordConfig:
|
class ImagePreprocessingConfig:
|
||||||
"""Configuration for video recording in ManiSkill environments."""
|
crop_params_dict: dict[str, tuple[int, int, int, int]] | None = None
|
||||||
|
resize_size: tuple[int, int] | None = None
|
||||||
enabled: bool = False
|
|
||||||
record_dir: str = "videos"
|
|
||||||
trajectory_name: str = "trajectory"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EnvTransformConfig:
|
class RewardClassifierConfig:
|
||||||
"""Configuration for environment wrappers."""
|
"""Configuration for reward classification."""
|
||||||
|
|
||||||
|
pretrained_path: str | None = None
|
||||||
|
success_threshold: float = 0.5
|
||||||
|
success_reward: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InverseKinematicsConfig:
|
||||||
|
"""Configuration for inverse kinematics processing."""
|
||||||
|
|
||||||
|
urdf_path: str | None = None
|
||||||
|
target_frame_name: str | None = None
|
||||||
|
end_effector_bounds: dict[str, list[float]] | None = None
|
||||||
|
end_effector_step_sizes: dict[str, float] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ObservationConfig:
|
||||||
|
"""Configuration for observation processing."""
|
||||||
|
|
||||||
# ee_action_space_params: EEActionSpaceConfig = field(default_factory=EEActionSpaceConfig)
|
|
||||||
control_mode: str = "gamepad"
|
|
||||||
display_cameras: bool = False
|
|
||||||
add_joint_velocity_to_observation: bool = False
|
add_joint_velocity_to_observation: bool = False
|
||||||
add_current_to_observation: bool = False
|
add_current_to_observation: bool = False
|
||||||
add_ee_pose_to_observation: bool = False
|
add_ee_pose_to_observation: bool = False
|
||||||
crop_params_dict: dict[str, tuple[int, int, int, int]] | None = None
|
display_cameras: bool = False
|
||||||
resize_size: tuple[int, int] | None = None
|
|
||||||
control_time_s: float = 20.0
|
|
||||||
fixed_reset_joint_positions: Any | None = None
|
@dataclass
|
||||||
reset_time_s: float = 5.0
|
class GripperConfig:
|
||||||
|
"""Configuration for gripper control and penalties."""
|
||||||
|
|
||||||
use_gripper: bool = True
|
use_gripper: bool = True
|
||||||
gripper_quantization_threshold: float | None = 0.8
|
|
||||||
gripper_penalty: float = 0.0
|
gripper_penalty: float = 0.0
|
||||||
gripper_penalty_in_reward: bool = False
|
gripper_penalty_in_reward: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ResetConfig:
|
||||||
|
"""Configuration for environment reset behavior."""
|
||||||
|
|
||||||
|
fixed_reset_joint_positions: Any | None = None
|
||||||
|
reset_time_s: float = 5.0
|
||||||
|
control_time_s: float = 20.0
|
||||||
|
terminate_on_success: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HILSerlProcessorConfig:
|
||||||
|
"""Configuration for environment processing pipeline."""
|
||||||
|
|
||||||
|
control_mode: str = "gamepad"
|
||||||
|
observation: ObservationConfig | None = None
|
||||||
|
image_preprocessing: ImagePreprocessingConfig | None = None
|
||||||
|
gripper: GripperConfig | None = None
|
||||||
|
reset: ResetConfig | None = None
|
||||||
|
inverse_kinematics: InverseKinematicsConfig | None = None
|
||||||
|
reward_classifier: RewardClassifierConfig | None = None
|
||||||
|
max_gripper_pos: float | None = 100.0
|
||||||
|
|
||||||
|
|
||||||
@EnvConfig.register_subclass(name="gym_manipulator")
|
@EnvConfig.register_subclass(name="gym_manipulator")
|
||||||
@dataclass
|
@dataclass
|
||||||
class HILSerlRobotEnvConfig(EnvConfig):
|
class HILSerlRobotEnvConfig(EnvConfig):
|
||||||
@@ -197,77 +235,10 @@ class HILSerlRobotEnvConfig(EnvConfig):
|
|||||||
|
|
||||||
robot: RobotConfig | None = None
|
robot: RobotConfig | None = None
|
||||||
teleop: TeleoperatorConfig | None = None
|
teleop: TeleoperatorConfig | None = None
|
||||||
wrapper: EnvTransformConfig | None = None
|
processor: HILSerlProcessorConfig = field(default_factory=HILSerlProcessorConfig)
|
||||||
fps: int = 10
|
|
||||||
name: str = "real_robot"
|
name: str = "real_robot"
|
||||||
mode: str | None = None # Either "record", "replay", None
|
|
||||||
repo_id: str | None = None
|
|
||||||
dataset_root: str | None = None
|
|
||||||
task: str | 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: 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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gym_kwargs(self) -> dict:
|
def gym_kwargs(self) -> dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@EnvConfig.register_subclass("hil")
|
|
||||||
@dataclass
|
|
||||||
class HILEnvConfig(EnvConfig):
|
|
||||||
"""Configuration for the HIL environment."""
|
|
||||||
|
|
||||||
name: str = "PandaPickCube"
|
|
||||||
task: str | None = "PandaPickCubeKeyboard-v0"
|
|
||||||
use_viewer: bool = True
|
|
||||||
gripper_penalty: float = 0.0
|
|
||||||
use_gamepad: bool = True
|
|
||||||
state_dim: int = 18
|
|
||||||
action_dim: int = 4
|
|
||||||
fps: int = 100
|
|
||||||
episode_length: int = 100
|
|
||||||
video_record: VideoRecordConfig = field(default_factory=VideoRecordConfig)
|
|
||||||
features: dict[str, PolicyFeature] = field(
|
|
||||||
default_factory=lambda: {
|
|
||||||
"action": PolicyFeature(type=FeatureType.ACTION, shape=(4,)),
|
|
||||||
"observation.image": PolicyFeature(type=FeatureType.VISUAL, shape=(3, 128, 128)),
|
|
||||||
"observation.state": PolicyFeature(type=FeatureType.STATE, shape=(18,)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
features_map: dict[str, str] = field(
|
|
||||||
default_factory=lambda: {
|
|
||||||
"action": ACTION,
|
|
||||||
"observation.image": OBS_IMAGE,
|
|
||||||
"observation.state": OBS_STATE,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
################# args from hilserlrobotenv
|
|
||||||
reward_classifier_pretrained_path: str | None = None
|
|
||||||
robot_config: RobotConfig | None = None
|
|
||||||
teleop_config: TeleoperatorConfig | None = None
|
|
||||||
wrapper: EnvTransformConfig | None = None
|
|
||||||
mode: str | None = None # Either "record", "replay", 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: str | None = None
|
|
||||||
# For the reward classifier, to record more positive examples after a success
|
|
||||||
number_of_steps_after_success: int = 0
|
|
||||||
############################
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gym_kwargs(self) -> dict:
|
|
||||||
return {
|
|
||||||
"use_viewer": self.use_viewer,
|
|
||||||
"use_gamepad": self.use_gamepad,
|
|
||||||
"gripper_penalty": self.gripper_penalty,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,7 +15,20 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from .batch_processor import ToBatchProcessor
|
from .batch_processor import ToBatchProcessor
|
||||||
|
from .delta_action_processor import MapDeltaActionToRobotAction
|
||||||
from .device_processor import DeviceProcessor
|
from .device_processor import DeviceProcessor
|
||||||
|
from .hil_processor import (
|
||||||
|
AddTeleopActionAsComplimentaryData,
|
||||||
|
AddTeleopEventsAsInfo,
|
||||||
|
GripperPenaltyProcessor,
|
||||||
|
ImageCropResizeProcessor,
|
||||||
|
InterventionActionProcessor,
|
||||||
|
Numpy2TorchActionProcessor,
|
||||||
|
RewardClassifierProcessor,
|
||||||
|
TimeLimitProcessor,
|
||||||
|
Torch2NumpyActionProcessor,
|
||||||
|
)
|
||||||
|
from .joint_observations_processor import JointVelocityProcessor, MotorCurrentProcessor
|
||||||
from .normalize_processor import NormalizerProcessor, UnnormalizerProcessor, hotswap_stats
|
from .normalize_processor import NormalizerProcessor, UnnormalizerProcessor, hotswap_stats
|
||||||
from .observation_processor import VanillaObservationProcessor
|
from .observation_processor import VanillaObservationProcessor
|
||||||
from .pipeline import (
|
from .pipeline import (
|
||||||
@@ -37,11 +50,20 @@ from .tokenizer_processor import TokenizerProcessor
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ActionProcessor",
|
"ActionProcessor",
|
||||||
|
"AddTeleopActionAsComplimentaryData",
|
||||||
|
"AddTeleopEventsAsInfo",
|
||||||
"DeviceProcessor",
|
"DeviceProcessor",
|
||||||
"DoneProcessor",
|
"DoneProcessor",
|
||||||
|
"MapDeltaActionToRobotAction",
|
||||||
"EnvTransition",
|
"EnvTransition",
|
||||||
|
"GripperPenaltyProcessor",
|
||||||
"IdentityProcessor",
|
"IdentityProcessor",
|
||||||
|
"ImageCropResizeProcessor",
|
||||||
"InfoProcessor",
|
"InfoProcessor",
|
||||||
|
"InterventionActionProcessor",
|
||||||
|
"JointVelocityProcessor",
|
||||||
|
"MapDeltaActionToRobotAction",
|
||||||
|
"MotorCurrentProcessor",
|
||||||
"NormalizerProcessor",
|
"NormalizerProcessor",
|
||||||
"UnnormalizerProcessor",
|
"UnnormalizerProcessor",
|
||||||
"hotswap_stats",
|
"hotswap_stats",
|
||||||
@@ -49,10 +71,14 @@ __all__ = [
|
|||||||
"ProcessorStep",
|
"ProcessorStep",
|
||||||
"ProcessorStepRegistry",
|
"ProcessorStepRegistry",
|
||||||
"RenameProcessor",
|
"RenameProcessor",
|
||||||
|
"RewardClassifierProcessor",
|
||||||
"RewardProcessor",
|
"RewardProcessor",
|
||||||
"RobotProcessor",
|
"RobotProcessor",
|
||||||
"ToBatchProcessor",
|
"ToBatchProcessor",
|
||||||
"TokenizerProcessor",
|
"TokenizerProcessor",
|
||||||
|
"TimeLimitProcessor",
|
||||||
|
"Numpy2TorchActionProcessor",
|
||||||
|
"Torch2NumpyActionProcessor",
|
||||||
"TransitionKey",
|
"TransitionKey",
|
||||||
"TruncatedProcessor",
|
"TruncatedProcessor",
|
||||||
"VanillaObservationProcessor",
|
"VanillaObservationProcessor",
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# !/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from torch import Tensor
|
||||||
|
|
||||||
|
from lerobot.configs.types import FeatureType, PolicyFeature
|
||||||
|
from lerobot.processor.pipeline import ActionProcessor, ProcessorStepRegistry
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorStepRegistry.register("map_delta_action_to_robot_action")
|
||||||
|
@dataclass
|
||||||
|
class MapDeltaActionToRobotAction(ActionProcessor):
|
||||||
|
"""
|
||||||
|
Map delta actions from teleoperators (gamepad, keyboard) to robot target actions
|
||||||
|
for use with inverse kinematics processors.
|
||||||
|
|
||||||
|
Expected input ACTION keys:
|
||||||
|
{
|
||||||
|
"action.delta_x": float,
|
||||||
|
"action.delta_y": float,
|
||||||
|
"action.delta_z": float,
|
||||||
|
"action.gripper": float (optional),
|
||||||
|
}
|
||||||
|
|
||||||
|
Output ACTION keys:
|
||||||
|
{
|
||||||
|
"action.enabled": bool,
|
||||||
|
"action.target_x": float,
|
||||||
|
"action.target_y": float,
|
||||||
|
"action.target_z": float,
|
||||||
|
"action.target_wx": float,
|
||||||
|
"action.target_wy": float,
|
||||||
|
"action.target_wz": float,
|
||||||
|
"action.gripper": float,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Scale factors for delta movements
|
||||||
|
position_scale: float = 1.0
|
||||||
|
rotation_scale: float = 0.0 # No rotation deltas for gamepad/keyboard
|
||||||
|
gripper_deadzone: float = 0.1 # Threshold for gripper activation
|
||||||
|
_prev_enabled: bool = field(default=False, init=False, repr=False)
|
||||||
|
|
||||||
|
def action(self, action: dict | Tensor | None) -> dict:
|
||||||
|
if action is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# NOTE (maractingi): Action can be a dict from the teleop_devices or a tensor from the policy
|
||||||
|
# TODO (maractingi): changing this target_xyz naming convention from the teleop_devices
|
||||||
|
if isinstance(action, dict):
|
||||||
|
delta_x = action.pop("action.delta_x", 0.0)
|
||||||
|
delta_y = action.pop("action.delta_y", 0.0)
|
||||||
|
delta_z = action.pop("action.delta_z", 0.0)
|
||||||
|
gripper = action.pop("action.gripper", 1.0) # Default to "stay" (1.0)
|
||||||
|
else:
|
||||||
|
delta_x = action[0].item()
|
||||||
|
delta_y = action[1].item()
|
||||||
|
delta_z = action[2].item()
|
||||||
|
gripper = action[3].item()
|
||||||
|
|
||||||
|
# Determine if the teleoperator is actively providing input
|
||||||
|
# Consider enabled if any significant movement delta is detected
|
||||||
|
position_magnitude = abs(delta_x) + abs(delta_y) + abs(delta_z)
|
||||||
|
enabled = position_magnitude > 1e-6 # Small threshold to avoid noise
|
||||||
|
|
||||||
|
# Scale the deltas appropriately
|
||||||
|
scaled_delta_x = float(delta_x) * self.position_scale
|
||||||
|
scaled_delta_y = float(delta_y) * self.position_scale
|
||||||
|
scaled_delta_z = float(delta_z) * self.position_scale
|
||||||
|
|
||||||
|
# For gamepad/keyboard, we don't have rotation input, so set to 0
|
||||||
|
# These could be extended in the future for more sophisticated teleoperators
|
||||||
|
target_wx = 0.0
|
||||||
|
target_wy = 0.0
|
||||||
|
target_wz = 0.0
|
||||||
|
|
||||||
|
# Update action with robot target format
|
||||||
|
action = {
|
||||||
|
"action.enabled": enabled,
|
||||||
|
"action.target_x": scaled_delta_x,
|
||||||
|
"action.target_y": scaled_delta_y,
|
||||||
|
"action.target_z": scaled_delta_z,
|
||||||
|
"action.target_wx": target_wx,
|
||||||
|
"action.target_wy": target_wy,
|
||||||
|
"action.target_wz": target_wz,
|
||||||
|
"action.gripper": float(gripper),
|
||||||
|
}
|
||||||
|
|
||||||
|
self._prev_enabled = enabled
|
||||||
|
return action
|
||||||
|
|
||||||
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
|
"""Transform features to match output format."""
|
||||||
|
# Update features to reflect the new action format
|
||||||
|
features.update(
|
||||||
|
{
|
||||||
|
"action.enabled": PolicyFeature(type=FeatureType.ACTION, shape=(1,)),
|
||||||
|
"action.target_x": PolicyFeature(type=FeatureType.ACTION, shape=(1,)),
|
||||||
|
"action.target_y": PolicyFeature(type=FeatureType.ACTION, shape=(1,)),
|
||||||
|
"action.target_z": PolicyFeature(type=FeatureType.ACTION, shape=(1,)),
|
||||||
|
"action.target_wx": PolicyFeature(type=FeatureType.ACTION, shape=(1,)),
|
||||||
|
"action.target_wy": PolicyFeature(type=FeatureType.ACTION, shape=(1,)),
|
||||||
|
"action.target_wz": PolicyFeature(type=FeatureType.ACTION, shape=(1,)),
|
||||||
|
"action.gripper": PolicyFeature(type=FeatureType.ACTION, shape=(1,)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return features
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._prev_enabled = False
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import torchvision.transforms.functional as F # noqa: N812
|
||||||
|
|
||||||
|
from lerobot.configs.types import PolicyFeature
|
||||||
|
from lerobot.processor.pipeline import (
|
||||||
|
ActionProcessor,
|
||||||
|
ComplementaryDataProcessor,
|
||||||
|
EnvTransition,
|
||||||
|
InfoProcessor,
|
||||||
|
ObservationProcessor,
|
||||||
|
ProcessorStepRegistry,
|
||||||
|
TransitionKey,
|
||||||
|
)
|
||||||
|
from lerobot.teleoperators.teleoperator import Teleoperator
|
||||||
|
from lerobot.teleoperators.utils import TeleopEvents
|
||||||
|
|
||||||
|
GRIPPER_KEY = "gripper"
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorStepRegistry.register("add_teleop_action_as_complementary_data")
|
||||||
|
@dataclass
|
||||||
|
class AddTeleopActionAsComplimentaryData(ComplementaryDataProcessor):
|
||||||
|
"""Add teleoperator action to transition complementary data."""
|
||||||
|
|
||||||
|
teleop_device: Teleoperator
|
||||||
|
|
||||||
|
def complementary_data(self, complementary_data: dict | None) -> dict:
|
||||||
|
complementary_data = {} if complementary_data is None else dict(complementary_data)
|
||||||
|
complementary_data["teleop_action"] = self.teleop_device.get_action()
|
||||||
|
return complementary_data
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorStepRegistry.register("add_teleop_action_as_info")
|
||||||
|
@dataclass
|
||||||
|
class AddTeleopEventsAsInfo(InfoProcessor):
|
||||||
|
"""Add teleoperator control events to transition info."""
|
||||||
|
|
||||||
|
teleop_device: Teleoperator
|
||||||
|
|
||||||
|
def info(self, info: dict | None) -> dict:
|
||||||
|
info = {} if info is None else dict(info)
|
||||||
|
teleop_events = getattr(self.teleop_device, "get_teleop_events", lambda: {})()
|
||||||
|
info.update(teleop_events)
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorStepRegistry.register("torch2numpy_action_processor")
|
||||||
|
@dataclass
|
||||||
|
class Torch2NumpyActionProcessor(ActionProcessor):
|
||||||
|
"""Convert PyTorch tensor actions to NumPy arrays."""
|
||||||
|
|
||||||
|
squeeze_batch_dim: bool = True
|
||||||
|
|
||||||
|
def action(self, action: torch.Tensor | None) -> np.ndarray | None:
|
||||||
|
if action is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(action, torch.Tensor):
|
||||||
|
raise TypeError(
|
||||||
|
f"Expected torch.Tensor or None, got {type(action).__name__}. "
|
||||||
|
"Use appropriate processor for non-tensor actions."
|
||||||
|
)
|
||||||
|
|
||||||
|
numpy_action = action.detach().cpu().numpy()
|
||||||
|
|
||||||
|
# Remove batch dimensions but preserve action dimensions
|
||||||
|
# Only squeeze if there's a batch dimension (first dim == 1)
|
||||||
|
if (
|
||||||
|
self.squeeze_batch_dim
|
||||||
|
and numpy_action.shape
|
||||||
|
and len(numpy_action.shape) > 1
|
||||||
|
and numpy_action.shape[0] == 1
|
||||||
|
):
|
||||||
|
numpy_action = numpy_action.squeeze(0)
|
||||||
|
|
||||||
|
return numpy_action
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorStepRegistry.register("numpy2torch_action_processor")
|
||||||
|
@dataclass
|
||||||
|
class Numpy2TorchActionProcessor(ActionProcessor):
|
||||||
|
"""Convert NumPy array action to PyTorch tensor."""
|
||||||
|
|
||||||
|
def action(self, action: np.ndarray | None) -> torch.Tensor | None:
|
||||||
|
if action is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(action, np.ndarray):
|
||||||
|
raise TypeError(
|
||||||
|
f"Expected np.ndarray or None, got {type(action).__name__}. "
|
||||||
|
"Use appropriate processor for non-tensor actions."
|
||||||
|
)
|
||||||
|
torch_action = torch.from_numpy(action)
|
||||||
|
return torch_action
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorStepRegistry.register("image_crop_resize_processor")
|
||||||
|
@dataclass
|
||||||
|
class ImageCropResizeProcessor(ObservationProcessor):
|
||||||
|
"""Crop and resize image observations."""
|
||||||
|
|
||||||
|
crop_params_dict: dict[str, tuple[int, int, int, int]] | None = None
|
||||||
|
resize_size: tuple[int, int] | None = None
|
||||||
|
|
||||||
|
def observation(self, observation: dict | None) -> dict | None:
|
||||||
|
if observation is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.resize_size is None and not self.crop_params_dict:
|
||||||
|
return observation
|
||||||
|
|
||||||
|
new_observation = dict(observation)
|
||||||
|
|
||||||
|
# Process all image keys in the observation
|
||||||
|
for key in observation:
|
||||||
|
if "image" not in key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
image = observation[key]
|
||||||
|
device = image.device
|
||||||
|
# NOTE (maractingi): No mps kernel for crop and resize, so we need to move to cpu
|
||||||
|
if device.type == "mps":
|
||||||
|
image = image.cpu()
|
||||||
|
# Crop if crop params are provided for this key
|
||||||
|
if self.crop_params_dict is not None and key in self.crop_params_dict:
|
||||||
|
crop_params = self.crop_params_dict[key]
|
||||||
|
image = F.crop(image, *crop_params)
|
||||||
|
if self.resize_size is not None:
|
||||||
|
image = F.resize(image, self.resize_size)
|
||||||
|
image = image.clamp(0.0, 1.0)
|
||||||
|
new_observation[key] = image.to(device)
|
||||||
|
|
||||||
|
return new_observation
|
||||||
|
|
||||||
|
def get_config(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"crop_params_dict": self.crop_params_dict,
|
||||||
|
"resize_size": self.resize_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
|
if self.resize_size is None:
|
||||||
|
return features
|
||||||
|
for key in features:
|
||||||
|
if "image" in key:
|
||||||
|
features[key] = PolicyFeature(type=features[key].type, shape=self.resize_size)
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@ProcessorStepRegistry.register("time_limit_processor")
|
||||||
|
class TimeLimitProcessor:
|
||||||
|
"""Track episode steps and enforce time limits."""
|
||||||
|
|
||||||
|
max_episode_steps: int
|
||||||
|
current_step: int = 0
|
||||||
|
|
||||||
|
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
||||||
|
truncated = transition.get(TransitionKey.TRUNCATED)
|
||||||
|
if truncated is None:
|
||||||
|
return transition
|
||||||
|
|
||||||
|
self.current_step += 1
|
||||||
|
if self.current_step >= self.max_episode_steps:
|
||||||
|
truncated = True
|
||||||
|
new_transition = transition.copy()
|
||||||
|
new_transition[TransitionKey.TRUNCATED] = truncated
|
||||||
|
return new_transition
|
||||||
|
|
||||||
|
def get_config(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"max_episode_steps": self.max_episode_steps,
|
||||||
|
}
|
||||||
|
|
||||||
|
def state_dict(self) -> dict[str, torch.Tensor]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def load_state_dict(self, state: dict[str, torch.Tensor]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.current_step = 0
|
||||||
|
|
||||||
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@ProcessorStepRegistry.register("gripper_penalty_processor")
|
||||||
|
class GripperPenaltyProcessor:
|
||||||
|
"""Apply penalty for inappropriate gripper usage."""
|
||||||
|
|
||||||
|
penalty: float = -0.01
|
||||||
|
max_gripper_pos: float = 30.0
|
||||||
|
|
||||||
|
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
||||||
|
"""Calculate gripper penalty and add to complementary data."""
|
||||||
|
action = transition.get(TransitionKey.ACTION)
|
||||||
|
complementary_data = transition.get(TransitionKey.COMPLEMENTARY_DATA)
|
||||||
|
|
||||||
|
if complementary_data is None or action is None:
|
||||||
|
return transition
|
||||||
|
|
||||||
|
current_gripper_pos = complementary_data.get("raw_joint_positions", None).get(GRIPPER_KEY, None)
|
||||||
|
if current_gripper_pos is None:
|
||||||
|
return transition
|
||||||
|
|
||||||
|
gripper_action = action[f"action.{GRIPPER_KEY}.pos"]
|
||||||
|
gripper_action_normalized = gripper_action / self.max_gripper_pos
|
||||||
|
|
||||||
|
# Normalize gripper state and action
|
||||||
|
gripper_state_normalized = current_gripper_pos / self.max_gripper_pos
|
||||||
|
|
||||||
|
# Calculate penalty boolean as in original
|
||||||
|
gripper_penalty_bool = (gripper_state_normalized < 0.5 and gripper_action_normalized > 0.5) or (
|
||||||
|
gripper_state_normalized > 0.75 and gripper_action_normalized < 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
gripper_penalty = self.penalty * int(gripper_penalty_bool)
|
||||||
|
|
||||||
|
# Add penalty information to complementary data
|
||||||
|
complementary_data = transition.get(TransitionKey.COMPLEMENTARY_DATA, {})
|
||||||
|
|
||||||
|
# Create new complementary data with penalty info
|
||||||
|
new_complementary_data = dict(complementary_data)
|
||||||
|
new_complementary_data["discrete_penalty"] = gripper_penalty
|
||||||
|
|
||||||
|
# Create new transition with updated complementary data
|
||||||
|
new_transition = transition.copy()
|
||||||
|
existing_comp_data = new_transition.get(TransitionKey.COMPLEMENTARY_DATA, {})
|
||||||
|
existing_comp_data.update(new_complementary_data)
|
||||||
|
new_transition[TransitionKey.COMPLEMENTARY_DATA] = existing_comp_data # type: ignore[misc]
|
||||||
|
return new_transition
|
||||||
|
|
||||||
|
def get_config(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"penalty": self.penalty,
|
||||||
|
"max_gripper_pos": self.max_gripper_pos,
|
||||||
|
}
|
||||||
|
|
||||||
|
def state_dict(self) -> dict[str, torch.Tensor]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def load_state_dict(self, state: dict[str, torch.Tensor]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset the processor state."""
|
||||||
|
self.last_gripper_state = None
|
||||||
|
|
||||||
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@ProcessorStepRegistry.register("intervention_action_processor")
|
||||||
|
class InterventionActionProcessor:
|
||||||
|
"""Handle human intervention actions and episode termination."""
|
||||||
|
|
||||||
|
use_gripper: bool = False
|
||||||
|
terminate_on_success: bool = True
|
||||||
|
|
||||||
|
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
||||||
|
action = transition.get(TransitionKey.ACTION)
|
||||||
|
if action is None:
|
||||||
|
return transition
|
||||||
|
|
||||||
|
# Get intervention signals from complementary data
|
||||||
|
info = transition.get(TransitionKey.INFO, {})
|
||||||
|
teleop_action = info.get("teleop_action", {})
|
||||||
|
is_intervention = info.get(TeleopEvents.IS_INTERVENTION, False)
|
||||||
|
terminate_episode = info.get(TeleopEvents.TERMINATE_EPISODE, False)
|
||||||
|
success = info.get(TeleopEvents.SUCCESS, False)
|
||||||
|
rerecord_episode = info.get(TeleopEvents.RERECORD_EPISODE, False)
|
||||||
|
|
||||||
|
new_transition = transition.copy()
|
||||||
|
|
||||||
|
# Override action if intervention is active
|
||||||
|
if is_intervention and teleop_action is not None:
|
||||||
|
if isinstance(teleop_action, dict):
|
||||||
|
# Convert teleop_action dict to tensor format
|
||||||
|
action_list = [
|
||||||
|
teleop_action.get("action.delta_x", 0.0),
|
||||||
|
teleop_action.get("action.delta_y", 0.0),
|
||||||
|
teleop_action.get("action.delta_z", 0.0),
|
||||||
|
]
|
||||||
|
if self.use_gripper:
|
||||||
|
action_list.append(teleop_action.get("gripper", 1.0))
|
||||||
|
elif isinstance(teleop_action, np.ndarray):
|
||||||
|
action_list = teleop_action.tolist()
|
||||||
|
else:
|
||||||
|
action_list = teleop_action
|
||||||
|
|
||||||
|
teleop_action_tensor = torch.tensor(action_list, dtype=action.dtype, device=action.device)
|
||||||
|
new_transition[TransitionKey.ACTION] = teleop_action_tensor
|
||||||
|
|
||||||
|
# Handle episode termination
|
||||||
|
new_transition[TransitionKey.DONE] = bool(terminate_episode) or (
|
||||||
|
self.terminate_on_success and success
|
||||||
|
)
|
||||||
|
new_transition[TransitionKey.REWARD] = float(success)
|
||||||
|
|
||||||
|
# Update info with intervention metadata
|
||||||
|
info = new_transition.get(TransitionKey.INFO, {})
|
||||||
|
info[TeleopEvents.IS_INTERVENTION] = is_intervention
|
||||||
|
info[TeleopEvents.RERECORD_EPISODE] = rerecord_episode
|
||||||
|
info[TeleopEvents.SUCCESS] = success
|
||||||
|
new_transition[TransitionKey.INFO] = info
|
||||||
|
|
||||||
|
# Update complementary data with teleop action
|
||||||
|
complementary_data = new_transition.get(TransitionKey.COMPLEMENTARY_DATA, {})
|
||||||
|
complementary_data["teleop_action"] = new_transition.get(TransitionKey.ACTION)
|
||||||
|
new_transition[TransitionKey.COMPLEMENTARY_DATA] = complementary_data
|
||||||
|
|
||||||
|
return new_transition
|
||||||
|
|
||||||
|
def get_config(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"use_gripper": self.use_gripper,
|
||||||
|
}
|
||||||
|
|
||||||
|
def state_dict(self) -> dict[str, torch.Tensor]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def load_state_dict(self, state: dict[str, torch.Tensor]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@ProcessorStepRegistry.register("reward_classifier_processor")
|
||||||
|
class RewardClassifierProcessor:
|
||||||
|
"""Apply reward classification to image observations."""
|
||||||
|
|
||||||
|
pretrained_path: str | None = None
|
||||||
|
device: str = "cpu"
|
||||||
|
success_threshold: float = 0.5
|
||||||
|
success_reward: float = 1.0
|
||||||
|
terminate_on_success: bool = True
|
||||||
|
|
||||||
|
reward_classifier: Any = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""Initialize the reward classifier after dataclass initialization."""
|
||||||
|
if self.pretrained_path is not None:
|
||||||
|
from lerobot.policies.sac.reward_model.modeling_classifier import Classifier
|
||||||
|
|
||||||
|
self.reward_classifier = Classifier.from_pretrained(self.pretrained_path)
|
||||||
|
self.reward_classifier.to(self.device)
|
||||||
|
self.reward_classifier.eval()
|
||||||
|
|
||||||
|
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
||||||
|
observation = transition.get(TransitionKey.OBSERVATION)
|
||||||
|
if observation is None or self.reward_classifier is None:
|
||||||
|
return transition
|
||||||
|
|
||||||
|
# Extract images from observation
|
||||||
|
images = {key: value for key, value in observation.items() if "image" in key}
|
||||||
|
|
||||||
|
if not images:
|
||||||
|
return transition
|
||||||
|
|
||||||
|
# Run reward classifier
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
with torch.inference_mode():
|
||||||
|
success = self.reward_classifier.predict_reward(images, threshold=self.success_threshold)
|
||||||
|
|
||||||
|
classifier_frequency = 1 / (time.perf_counter() - start_time)
|
||||||
|
|
||||||
|
# Calculate reward and termination
|
||||||
|
reward = transition.get(TransitionKey.REWARD, 0.0)
|
||||||
|
terminated = transition.get(TransitionKey.DONE, False)
|
||||||
|
|
||||||
|
if success == 1.0:
|
||||||
|
reward = self.success_reward
|
||||||
|
if self.terminate_on_success:
|
||||||
|
terminated = True
|
||||||
|
|
||||||
|
# Update transition
|
||||||
|
new_transition = transition.copy()
|
||||||
|
new_transition[TransitionKey.REWARD] = reward
|
||||||
|
new_transition[TransitionKey.DONE] = terminated
|
||||||
|
|
||||||
|
# Update info with classifier frequency
|
||||||
|
info = new_transition.get(TransitionKey.INFO, {})
|
||||||
|
info["reward_classifier_frequency"] = classifier_frequency
|
||||||
|
new_transition[TransitionKey.INFO] = info
|
||||||
|
|
||||||
|
return new_transition
|
||||||
|
|
||||||
|
def get_config(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"device": self.device,
|
||||||
|
"success_threshold": self.success_threshold,
|
||||||
|
"success_reward": self.success_reward,
|
||||||
|
"terminate_on_success": self.terminate_on_success,
|
||||||
|
}
|
||||||
|
|
||||||
|
def state_dict(self) -> dict[str, torch.Tensor]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def load_state_dict(self, state: dict[str, torch.Tensor]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
|
return features
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from lerobot.configs.types import PolicyFeature
|
||||||
|
from lerobot.processor.pipeline import (
|
||||||
|
ObservationProcessor,
|
||||||
|
ProcessorStepRegistry,
|
||||||
|
)
|
||||||
|
from lerobot.robots import Robot
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@ProcessorStepRegistry.register("joint_velocity_processor")
|
||||||
|
class JointVelocityProcessor:
|
||||||
|
"""Add joint velocity information to observations."""
|
||||||
|
|
||||||
|
joint_velocity_limits: float = 100.0
|
||||||
|
dt: float = 1.0 / 10
|
||||||
|
num_dof: int | None = None
|
||||||
|
|
||||||
|
last_joint_positions: torch.Tensor | None = None
|
||||||
|
|
||||||
|
def observation(self, observation: dict | None) -> dict | None:
|
||||||
|
if observation is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get current joint positions (assuming they're in observation.state)
|
||||||
|
current_positions = observation.get("observation.state")
|
||||||
|
if current_positions is None:
|
||||||
|
return observation
|
||||||
|
|
||||||
|
# Initialize last joint positions if not already set
|
||||||
|
if self.last_joint_positions is None:
|
||||||
|
self.last_joint_positions = current_positions.clone()
|
||||||
|
|
||||||
|
# Compute velocities
|
||||||
|
joint_velocities = (current_positions - self.last_joint_positions) / self.dt
|
||||||
|
self.last_joint_positions = current_positions.clone()
|
||||||
|
|
||||||
|
# Extend observation with velocities
|
||||||
|
extended_state = torch.cat([current_positions, joint_velocities], dim=-1)
|
||||||
|
|
||||||
|
# Create new observation dict
|
||||||
|
new_observation = dict(observation)
|
||||||
|
new_observation["observation.state"] = extended_state
|
||||||
|
|
||||||
|
return new_observation
|
||||||
|
|
||||||
|
def get_config(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"joint_velocity_limits": self.joint_velocity_limits,
|
||||||
|
"dt": self.dt,
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.last_joint_positions = None
|
||||||
|
|
||||||
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
|
if "observation.state" in features and self.num_dof is not None:
|
||||||
|
from lerobot.configs.types import PolicyFeature
|
||||||
|
|
||||||
|
original_feature = features["observation.state"]
|
||||||
|
# Double the shape to account for positions + velocities
|
||||||
|
new_shape = (original_feature.shape[0] + self.num_dof,) + original_feature.shape[1:]
|
||||||
|
features["observation.state"] = PolicyFeature(type=original_feature.type, shape=new_shape)
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@ProcessorStepRegistry.register("current_processor")
|
||||||
|
class MotorCurrentProcessor(ObservationProcessor):
|
||||||
|
"""Add motor current information to observations."""
|
||||||
|
|
||||||
|
robot: Robot | None = None
|
||||||
|
|
||||||
|
def observation(self, observation: dict | None) -> dict | None:
|
||||||
|
if observation is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get current values from robot state
|
||||||
|
if self.robot is None:
|
||||||
|
return observation
|
||||||
|
present_current_dict = self.robot.bus.sync_read("Present_Current") # type: ignore[attr-defined]
|
||||||
|
motor_currents = torch.tensor(
|
||||||
|
[present_current_dict[name] for name in self.robot.bus.motors], # type: ignore[attr-defined]
|
||||||
|
dtype=torch.float32,
|
||||||
|
).unsqueeze(0)
|
||||||
|
|
||||||
|
current_state = observation.get("observation.state")
|
||||||
|
if current_state is None:
|
||||||
|
return observation
|
||||||
|
|
||||||
|
extended_state = torch.cat([current_state, motor_currents], dim=-1)
|
||||||
|
|
||||||
|
# Create new observation dict
|
||||||
|
new_observation = dict(observation)
|
||||||
|
new_observation["observation.state"] = extended_state
|
||||||
|
|
||||||
|
return new_observation
|
||||||
|
|
||||||
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
|
if "observation.state" in features and self.robot is not None:
|
||||||
|
from lerobot.configs.types import PolicyFeature
|
||||||
|
|
||||||
|
original_feature = features["observation.state"]
|
||||||
|
# Add motor current dimensions to the original state shape
|
||||||
|
num_motors = 0
|
||||||
|
if hasattr(self.robot, "bus") and hasattr(self.robot.bus, "motors"): # type: ignore[attr-defined]
|
||||||
|
num_motors = len(self.robot.bus.motors) # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
if num_motors > 0:
|
||||||
|
new_shape = (original_feature.shape[0] + num_motors,) + original_feature.shape[1:]
|
||||||
|
features["observation.state"] = PolicyFeature(type=original_feature.type, shape=new_shape)
|
||||||
|
return features
|
||||||
@@ -53,6 +53,9 @@ class EEReferenceAndDelta:
|
|||||||
kinematics: RobotKinematics
|
kinematics: RobotKinematics
|
||||||
end_effector_step_sizes: dict
|
end_effector_step_sizes: dict
|
||||||
motor_names: list[str]
|
motor_names: list[str]
|
||||||
|
use_latched_reference: bool = (
|
||||||
|
True # If True, latch reference on enable; if False, always use current pose
|
||||||
|
)
|
||||||
|
|
||||||
reference_ee_pose: np.ndarray | None = field(default=None, init=False, repr=False)
|
reference_ee_pose: np.ndarray | None = field(default=None, init=False, repr=False)
|
||||||
_prev_enabled: bool = field(default=False, init=False, repr=False)
|
_prev_enabled: bool = field(default=False, init=False, repr=False)
|
||||||
@@ -69,7 +72,10 @@ class EEReferenceAndDelta:
|
|||||||
"raw_joint_positions is not in complementary data and is required for EEReferenceAndDelta"
|
"raw_joint_positions is not in complementary data and is required for EEReferenceAndDelta"
|
||||||
)
|
)
|
||||||
|
|
||||||
q = np.array([float(raw[n]) for n in self.motor_names], dtype=float)
|
if "reference_joint_positions" in comp:
|
||||||
|
q = comp["reference_joint_positions"]
|
||||||
|
else:
|
||||||
|
q = np.array([float(raw[n]) for n in self.motor_names], dtype=float)
|
||||||
|
|
||||||
# Current pose from FK on measured joints
|
# Current pose from FK on measured joints
|
||||||
t_curr = self.kinematics.forward_kinematics(q)
|
t_curr = self.kinematics.forward_kinematics(q)
|
||||||
@@ -85,11 +91,12 @@ class EEReferenceAndDelta:
|
|||||||
desired = None
|
desired = None
|
||||||
|
|
||||||
if enabled:
|
if enabled:
|
||||||
# Latch a reference at the rising edge; also be defensive if None
|
ref = t_curr
|
||||||
if not self._prev_enabled or self.reference_ee_pose is None:
|
if self.use_latched_reference:
|
||||||
self.reference_ee_pose = t_curr.copy()
|
# Latched reference mode: latch reference at the rising edge
|
||||||
|
if not self._prev_enabled or self.reference_ee_pose is None:
|
||||||
ref = self.reference_ee_pose if self.reference_ee_pose is not None else t_curr
|
self.reference_ee_pose = t_curr.copy()
|
||||||
|
ref = self.reference_ee_pose if self.reference_ee_pose is not None else t_curr
|
||||||
|
|
||||||
delta_p = np.array(
|
delta_p = np.array(
|
||||||
[
|
[
|
||||||
@@ -100,7 +107,6 @@ class EEReferenceAndDelta:
|
|||||||
dtype=float,
|
dtype=float,
|
||||||
)
|
)
|
||||||
r_abs = Rotation.from_rotvec([wx, wy, wz]).as_matrix()
|
r_abs = Rotation.from_rotvec([wx, wy, wz]).as_matrix()
|
||||||
|
|
||||||
desired = np.eye(4, dtype=float)
|
desired = np.eye(4, dtype=float)
|
||||||
desired[:3, :3] = ref[:3, :3] @ r_abs
|
desired[:3, :3] = ref[:3, :3] @ r_abs
|
||||||
desired[:3, 3] = ref[:3, 3] + delta_p
|
desired[:3, 3] = ref[:3, 3] + delta_p
|
||||||
@@ -292,6 +298,8 @@ class InverseKinematicsEEToJoints:
|
|||||||
else:
|
else:
|
||||||
new_act[f"action.{name}.pos"] = float(q_target[i])
|
new_act[f"action.{name}.pos"] = float(q_target[i])
|
||||||
transition[TransitionKey.ACTION] = new_act
|
transition[TransitionKey.ACTION] = new_act
|
||||||
|
if not self.initial_guess_current_joints:
|
||||||
|
transition[TransitionKey.COMPLEMENTARY_DATA]["reference_joint_positions"] = q_target
|
||||||
return transition
|
return transition
|
||||||
|
|
||||||
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
|
||||||
@@ -332,6 +340,7 @@ class GripperVelocityToJoint:
|
|||||||
speed_factor: float = 20.0
|
speed_factor: float = 20.0
|
||||||
clip_min: float = 0.0
|
clip_min: float = 0.0
|
||||||
clip_max: float = 100.0
|
clip_max: float = 100.0
|
||||||
|
discrete_gripper: bool = False
|
||||||
|
|
||||||
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
||||||
obs = transition.get(TransitionKey.OBSERVATION) or {}
|
obs = transition.get(TransitionKey.OBSERVATION) or {}
|
||||||
@@ -347,6 +356,15 @@ class GripperVelocityToJoint:
|
|||||||
transition[TransitionKey.ACTION] = new_act
|
transition[TransitionKey.ACTION] = new_act
|
||||||
return transition
|
return transition
|
||||||
|
|
||||||
|
if self.discrete_gripper:
|
||||||
|
# Discrete gripper actions are in [0, 1, 2]
|
||||||
|
# 0: open, 1: close, 2: stay
|
||||||
|
# We need to shift them to [-1, 0, 1] and then scale them to clip_max
|
||||||
|
gripper_action = act.get("action.gripper", 1.0)
|
||||||
|
gripper_action = gripper_action - 1.0
|
||||||
|
gripper_action *= self.clip_max
|
||||||
|
act["action.gripper"] = gripper_action
|
||||||
|
|
||||||
# Get current gripper position from complementary data
|
# Get current gripper position from complementary data
|
||||||
raw = comp.get("raw_joint_positions") or {}
|
raw = comp.get("raw_joint_positions") or {}
|
||||||
curr_pos = float(raw.get("gripper"))
|
curr_pos = float(raw.get("gripper"))
|
||||||
|
|||||||
@@ -62,9 +62,16 @@ from lerobot.configs import parser
|
|||||||
from lerobot.configs.train import TrainRLServerPipelineConfig
|
from lerobot.configs.train import TrainRLServerPipelineConfig
|
||||||
from lerobot.policies.factory import make_policy
|
from lerobot.policies.factory import make_policy
|
||||||
from lerobot.policies.sac.modeling_sac import SACPolicy
|
from lerobot.policies.sac.modeling_sac import SACPolicy
|
||||||
|
from lerobot.processor.pipeline import TransitionKey
|
||||||
from lerobot.robots import so100_follower # noqa: F401
|
from lerobot.robots import so100_follower # noqa: F401
|
||||||
from lerobot.scripts.rl.gym_manipulator import make_robot_env
|
from lerobot.scripts.rl.gym_manipulator import (
|
||||||
|
create_transition,
|
||||||
|
make_processors,
|
||||||
|
make_robot_env,
|
||||||
|
step_env_and_process_transition,
|
||||||
|
)
|
||||||
from lerobot.teleoperators import gamepad, so101_leader # noqa: F401
|
from lerobot.teleoperators import gamepad, so101_leader # noqa: F401
|
||||||
|
from lerobot.teleoperators.utils import TeleopEvents
|
||||||
from lerobot.transport import services_pb2, services_pb2_grpc
|
from lerobot.transport import services_pb2, services_pb2_grpc
|
||||||
from lerobot.transport.utils import (
|
from lerobot.transport.utils import (
|
||||||
bytes_to_state_dict,
|
bytes_to_state_dict,
|
||||||
@@ -236,7 +243,8 @@ def act_with_policy(
|
|||||||
|
|
||||||
logging.info("make_env online")
|
logging.info("make_env online")
|
||||||
|
|
||||||
online_env = make_robot_env(cfg=cfg.env)
|
online_env, teleop_device = make_robot_env(cfg=cfg.env)
|
||||||
|
env_processor, action_processor = make_processors(online_env, teleop_device, cfg.env, cfg.policy.device)
|
||||||
|
|
||||||
set_seed(cfg.seed)
|
set_seed(cfg.seed)
|
||||||
device = get_safe_torch_device(cfg.policy.device, log=True)
|
device = get_safe_torch_device(cfg.policy.device, log=True)
|
||||||
@@ -257,6 +265,12 @@ def act_with_policy(
|
|||||||
assert isinstance(policy, nn.Module)
|
assert isinstance(policy, nn.Module)
|
||||||
|
|
||||||
obs, info = online_env.reset()
|
obs, info = online_env.reset()
|
||||||
|
env_processor.reset()
|
||||||
|
action_processor.reset()
|
||||||
|
|
||||||
|
# Process initial observation
|
||||||
|
transition = create_transition(observation=obs, info=info)
|
||||||
|
transition = env_processor(transition)
|
||||||
|
|
||||||
# NOTE: For the moment we will solely handle the case of a single environment
|
# NOTE: For the moment we will solely handle the case of a single environment
|
||||||
sum_reward_episode = 0
|
sum_reward_episode = 0
|
||||||
@@ -274,45 +288,61 @@ def act_with_policy(
|
|||||||
logging.info("[ACTOR] Shutting down act_with_policy")
|
logging.info("[ACTOR] Shutting down act_with_policy")
|
||||||
return
|
return
|
||||||
|
|
||||||
if interaction_step >= cfg.policy.online_step_before_learning:
|
observation = transition[TransitionKey.OBSERVATION]
|
||||||
# Time policy inference and check if it meets FPS requirement
|
|
||||||
with policy_timer:
|
|
||||||
action = policy.select_action(batch=obs)
|
|
||||||
policy_fps = policy_timer.fps_last
|
|
||||||
|
|
||||||
log_policy_frequency_issue(policy_fps=policy_fps, cfg=cfg, interaction_step=interaction_step)
|
# Time policy inference and check if it meets FPS requirement
|
||||||
|
with policy_timer:
|
||||||
|
# Extract observation from transition for policy
|
||||||
|
action = policy.select_action(batch=observation)
|
||||||
|
policy_fps = policy_timer.fps_last
|
||||||
|
|
||||||
else:
|
log_policy_frequency_issue(policy_fps=policy_fps, cfg=cfg, interaction_step=interaction_step)
|
||||||
action = online_env.action_space.sample()
|
|
||||||
|
|
||||||
next_obs, reward, done, truncated, info = online_env.step(action)
|
# Use the new step function
|
||||||
|
new_transition = step_env_and_process_transition(
|
||||||
|
env=online_env,
|
||||||
|
transition=transition,
|
||||||
|
action=action,
|
||||||
|
env_processor=env_processor,
|
||||||
|
action_processor=action_processor,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract values from processed transition
|
||||||
|
next_observation = new_transition[TransitionKey.OBSERVATION]
|
||||||
|
executed_action = new_transition[TransitionKey.ACTION]
|
||||||
|
reward = new_transition[TransitionKey.REWARD]
|
||||||
|
done = new_transition.get(TransitionKey.DONE, False)
|
||||||
|
truncated = new_transition.get(TransitionKey.TRUNCATED, False)
|
||||||
|
|
||||||
sum_reward_episode += float(reward)
|
sum_reward_episode += float(reward)
|
||||||
# Increment total steps counter for intervention rate
|
|
||||||
episode_total_steps += 1
|
episode_total_steps += 1
|
||||||
|
|
||||||
# NOTE: We override the action if the intervention is True, because the action applied is the intervention action
|
# Check for intervention from transition info
|
||||||
if "is_intervention" in info and info["is_intervention"]:
|
intervention_info = new_transition[TransitionKey.INFO]
|
||||||
# NOTE: The action space for demonstration before hand is with the full action space
|
if intervention_info.get(TeleopEvents.IS_INTERVENTION, False):
|
||||||
# but sometimes for example we want to deactivate the gripper
|
|
||||||
action = info["action_intervention"]
|
|
||||||
episode_intervention = True
|
episode_intervention = True
|
||||||
# Increment intervention steps counter
|
|
||||||
episode_intervention_steps += 1
|
episode_intervention_steps += 1
|
||||||
|
|
||||||
|
complementary_info = {
|
||||||
|
"discrete_penalty": torch.tensor(
|
||||||
|
[new_transition[TransitionKey.COMPLEMENTARY_DATA].get("discrete_penalty", 0.0)]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
# Create transition for learner (convert to old format)
|
||||||
list_transition_to_send_to_learner.append(
|
list_transition_to_send_to_learner.append(
|
||||||
Transition(
|
Transition(
|
||||||
state=obs,
|
state=observation,
|
||||||
action=action,
|
action=executed_action,
|
||||||
reward=reward,
|
reward=reward,
|
||||||
next_state=next_obs,
|
next_state=next_observation,
|
||||||
done=done,
|
done=done,
|
||||||
truncated=truncated, # TODO: (azouitine) Handle truncation properly
|
truncated=truncated,
|
||||||
complementary_info=info,
|
complementary_info=complementary_info,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# assign obs to the next obs and continue the rollout
|
|
||||||
obs = next_obs
|
# Update transition for next iteration
|
||||||
|
transition = new_transition
|
||||||
|
|
||||||
if done or truncated:
|
if done or truncated:
|
||||||
logging.info(f"[ACTOR] Global step {interaction_step}: Episode reward: {sum_reward_episode}")
|
logging.info(f"[ACTOR] Global step {interaction_step}: Episode reward: {sum_reward_episode}")
|
||||||
@@ -347,12 +377,20 @@ def act_with_policy(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reset intervention counters
|
# Reset intervention counters and environment
|
||||||
sum_reward_episode = 0.0
|
sum_reward_episode = 0.0
|
||||||
episode_intervention = False
|
episode_intervention = False
|
||||||
episode_intervention_steps = 0
|
episode_intervention_steps = 0
|
||||||
episode_total_steps = 0
|
episode_total_steps = 0
|
||||||
|
|
||||||
|
# Reset environment and processors
|
||||||
obs, info = online_env.reset()
|
obs, info = online_env.reset()
|
||||||
|
env_processor.reset()
|
||||||
|
action_processor.reset()
|
||||||
|
|
||||||
|
# Process initial observation
|
||||||
|
transition = create_transition(observation=obs, info=info)
|
||||||
|
transition = env_processor(transition)
|
||||||
|
|
||||||
if cfg.env.fps is not None:
|
if cfg.env.fps is not None:
|
||||||
dt_time = time.perf_counter() - start_time
|
dt_time = time.perf_counter() - start_time
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -75,6 +75,7 @@ from lerobot.policies.sac.modeling_sac import SACPolicy
|
|||||||
from lerobot.robots import so100_follower # noqa: F401
|
from lerobot.robots import so100_follower # noqa: F401
|
||||||
from lerobot.scripts.rl import learner_service
|
from lerobot.scripts.rl import learner_service
|
||||||
from lerobot.teleoperators import gamepad, so101_leader # noqa: F401
|
from lerobot.teleoperators import gamepad, so101_leader # noqa: F401
|
||||||
|
from lerobot.teleoperators.utils import TeleopEvents
|
||||||
from lerobot.transport import services_pb2_grpc
|
from lerobot.transport import services_pb2_grpc
|
||||||
from lerobot.transport.utils import (
|
from lerobot.transport.utils import (
|
||||||
MAX_MESSAGE_SIZE,
|
MAX_MESSAGE_SIZE,
|
||||||
@@ -1174,7 +1175,7 @@ def process_transitions(
|
|||||||
|
|
||||||
# Add to offline buffer if it's an intervention
|
# Add to offline buffer if it's an intervention
|
||||||
if dataset_repo_id is not None and transition.get("complementary_info", {}).get(
|
if dataset_repo_id is not None and transition.get("complementary_info", {}).get(
|
||||||
"is_intervention"
|
TeleopEvents.IS_INTERVENTION
|
||||||
):
|
):
|
||||||
offline_replay_buffer.add(**transition)
|
offline_replay_buffer.add(**transition)
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
|
|
||||||
from .config import TeleoperatorConfig
|
from .config import TeleoperatorConfig
|
||||||
from .teleoperator import Teleoperator
|
from .teleoperator import Teleoperator
|
||||||
from .utils import make_teleoperator_from_config
|
from .utils import TeleopEvents, make_teleoperator_from_config
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from ..utils import TeleopEvents
|
||||||
|
|
||||||
|
|
||||||
class InputController:
|
class InputController:
|
||||||
"""Base class for input controllers that generate motion deltas."""
|
"""Base class for input controllers that generate motion deltas."""
|
||||||
@@ -134,10 +136,10 @@ class KeyboardController(InputController):
|
|||||||
return False
|
return False
|
||||||
elif key == keyboard.Key.enter:
|
elif key == keyboard.Key.enter:
|
||||||
self.key_states["success"] = True
|
self.key_states["success"] = True
|
||||||
self.episode_end_status = "success"
|
self.episode_end_status = TeleopEvents.SUCCESS
|
||||||
elif key == keyboard.Key.backspace:
|
elif key == keyboard.Key.backspace:
|
||||||
self.key_states["failure"] = True
|
self.key_states["failure"] = True
|
||||||
self.episode_end_status = "failure"
|
self.episode_end_status = TeleopEvents.FAILURE
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -255,13 +257,13 @@ class GamepadController(InputController):
|
|||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
if event.type == pygame.JOYBUTTONDOWN:
|
if event.type == pygame.JOYBUTTONDOWN:
|
||||||
if event.button == 3:
|
if event.button == 3:
|
||||||
self.episode_end_status = "success"
|
self.episode_end_status = TeleopEvents.SUCCESS
|
||||||
# A button (1) for failure
|
# A button (1) for failure
|
||||||
elif event.button == 1:
|
elif event.button == 1:
|
||||||
self.episode_end_status = "failure"
|
self.episode_end_status = TeleopEvents.FAILURE
|
||||||
# X button (0) for rerecord
|
# X button (0) for rerecord
|
||||||
elif event.button == 0:
|
elif event.button == 0:
|
||||||
self.episode_end_status = "rerecord_episode"
|
self.episode_end_status = TeleopEvents.RERECORD_EPISODE
|
||||||
|
|
||||||
# RB button (6) for closing gripper
|
# RB button (6) for closing gripper
|
||||||
elif event.button == 6:
|
elif event.button == 6:
|
||||||
@@ -451,11 +453,11 @@ class GamepadControllerHID(InputController):
|
|||||||
# Check if X/Square button (bit 5) is pressed for failure
|
# Check if X/Square button (bit 5) is pressed for failure
|
||||||
# Check if A/Cross button (bit 4) is pressed for rerecording
|
# Check if A/Cross button (bit 4) is pressed for rerecording
|
||||||
if buttons & 1 << 7:
|
if buttons & 1 << 7:
|
||||||
self.episode_end_status = "success"
|
self.episode_end_status = TeleopEvents.SUCCESS
|
||||||
elif buttons & 1 << 5:
|
elif buttons & 1 << 5:
|
||||||
self.episode_end_status = "failure"
|
self.episode_end_status = TeleopEvents.FAILURE
|
||||||
elif buttons & 1 << 4:
|
elif buttons & 1 << 4:
|
||||||
self.episode_end_status = "rerecord_episode"
|
self.episode_end_status = TeleopEvents.RERECORD_EPISODE
|
||||||
else:
|
else:
|
||||||
self.episode_end_status = None
|
self.episode_end_status = None
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from typing import Any
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from ..teleoperator import Teleoperator
|
from ..teleoperator import Teleoperator
|
||||||
|
from ..utils import TeleopEvents
|
||||||
from .configuration_gamepad import GamepadTeleopConfig
|
from .configuration_gamepad import GamepadTeleopConfig
|
||||||
|
|
||||||
|
|
||||||
@@ -93,9 +94,9 @@ class GamepadTeleop(Teleoperator):
|
|||||||
gamepad_action = np.array([delta_x, delta_y, delta_z], dtype=np.float32)
|
gamepad_action = np.array([delta_x, delta_y, delta_z], dtype=np.float32)
|
||||||
|
|
||||||
action_dict = {
|
action_dict = {
|
||||||
"delta_x": gamepad_action[0],
|
"action.delta_x": gamepad_action[0],
|
||||||
"delta_y": gamepad_action[1],
|
"action.delta_y": gamepad_action[1],
|
||||||
"delta_z": gamepad_action[2],
|
"action.delta_z": gamepad_action[2],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Default gripper action is to stay
|
# Default gripper action is to stay
|
||||||
@@ -107,6 +108,48 @@ class GamepadTeleop(Teleoperator):
|
|||||||
|
|
||||||
return action_dict
|
return action_dict
|
||||||
|
|
||||||
|
def get_teleop_events(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get extra control events from the gamepad such as intervention status,
|
||||||
|
episode termination, success indicators, etc.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- is_intervention: bool - Whether human is currently intervening
|
||||||
|
- terminate_episode: bool - Whether to terminate the current episode
|
||||||
|
- success: bool - Whether the episode was successful
|
||||||
|
- rerecord_episode: bool - Whether to rerecord the episode
|
||||||
|
"""
|
||||||
|
if self.gamepad is None:
|
||||||
|
return {
|
||||||
|
TeleopEvents.IS_INTERVENTION: False,
|
||||||
|
TeleopEvents.TERMINATE_EPISODE: False,
|
||||||
|
TeleopEvents.SUCCESS: False,
|
||||||
|
TeleopEvents.RERECORD_EPISODE: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update gamepad state to get fresh inputs
|
||||||
|
self.gamepad.update()
|
||||||
|
|
||||||
|
# Check if intervention is active
|
||||||
|
is_intervention = self.gamepad.should_intervene()
|
||||||
|
|
||||||
|
# Get episode end status
|
||||||
|
episode_end_status = self.gamepad.get_episode_end_status()
|
||||||
|
terminate_episode = episode_end_status in [
|
||||||
|
TeleopEvents.RERECORD_EPISODE,
|
||||||
|
TeleopEvents.FAILURE,
|
||||||
|
]
|
||||||
|
success = episode_end_status == TeleopEvents.SUCCESS
|
||||||
|
rerecord_episode = episode_end_status == TeleopEvents.RERECORD_EPISODE
|
||||||
|
|
||||||
|
return {
|
||||||
|
TeleopEvents.IS_INTERVENTION: is_intervention,
|
||||||
|
TeleopEvents.TERMINATE_EPISODE: terminate_episode,
|
||||||
|
TeleopEvents.SUCCESS: success,
|
||||||
|
TeleopEvents.RERECORD_EPISODE: rerecord_episode,
|
||||||
|
}
|
||||||
|
|
||||||
def disconnect(self) -> None:
|
def disconnect(self) -> None:
|
||||||
"""Disconnect from the gamepad."""
|
"""Disconnect from the gamepad."""
|
||||||
if self.gamepad is not None:
|
if self.gamepad is not None:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from typing import Any
|
|||||||
from lerobot.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
from lerobot.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
||||||
|
|
||||||
from ..teleoperator import Teleoperator
|
from ..teleoperator import Teleoperator
|
||||||
|
from ..utils import TeleopEvents
|
||||||
from .configuration_keyboard import KeyboardEndEffectorTeleopConfig, KeyboardTeleopConfig
|
from .configuration_keyboard import KeyboardEndEffectorTeleopConfig, KeyboardTeleopConfig
|
||||||
|
|
||||||
PYNPUT_AVAILABLE = True
|
PYNPUT_AVAILABLE = True
|
||||||
@@ -167,13 +168,13 @@ class KeyboardEndEffectorTeleop(KeyboardTeleop):
|
|||||||
return {
|
return {
|
||||||
"dtype": "float32",
|
"dtype": "float32",
|
||||||
"shape": (4,),
|
"shape": (4,),
|
||||||
"names": {"delta_x": 0, "delta_y": 1, "delta_z": 2, "gripper": 3},
|
"names": {"action.delta_x": 0, "action.delta_y": 1, "action.delta_z": 2, "action.gripper": 3},
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"dtype": "float32",
|
"dtype": "float32",
|
||||||
"shape": (3,),
|
"shape": (3,),
|
||||||
"names": {"delta_x": 0, "delta_y": 1, "delta_z": 2},
|
"names": {"action.delta_x": 0, "action.delta_y": 1, "action.delta_z": 2},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _on_press(self, key):
|
def _on_press(self, key):
|
||||||
@@ -226,12 +227,75 @@ class KeyboardEndEffectorTeleop(KeyboardTeleop):
|
|||||||
self.current_pressed.clear()
|
self.current_pressed.clear()
|
||||||
|
|
||||||
action_dict = {
|
action_dict = {
|
||||||
"delta_x": delta_x,
|
"action.delta_x": delta_x,
|
||||||
"delta_y": delta_y,
|
"action.delta_y": delta_y,
|
||||||
"delta_z": delta_z,
|
"action.delta_z": delta_z,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.config.use_gripper:
|
if self.config.use_gripper:
|
||||||
action_dict["gripper"] = gripper_action
|
action_dict["gripper"] = gripper_action
|
||||||
|
|
||||||
return action_dict
|
return action_dict
|
||||||
|
|
||||||
|
def get_teleop_events(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get extra control events from the keyboard such as intervention status,
|
||||||
|
episode termination, success indicators, etc.
|
||||||
|
|
||||||
|
Keyboard mappings:
|
||||||
|
- Any movement keys pressed = intervention active
|
||||||
|
- 's' key = success (terminate episode successfully)
|
||||||
|
- 'r' key = rerecord episode (terminate and rerecord)
|
||||||
|
- 'q' key = quit episode (terminate without success)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- is_intervention: bool - Whether human is currently intervening
|
||||||
|
- terminate_episode: bool - Whether to terminate the current episode
|
||||||
|
- success: bool - Whether the episode was successful
|
||||||
|
- rerecord_episode: bool - Whether to rerecord the episode
|
||||||
|
"""
|
||||||
|
if not self.is_connected:
|
||||||
|
return {
|
||||||
|
TeleopEvents.IS_INTERVENTION: False,
|
||||||
|
TeleopEvents.TERMINATE_EPISODE: False,
|
||||||
|
TeleopEvents.SUCCESS: False,
|
||||||
|
TeleopEvents.RERECORD_EPISODE: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if any movement keys are currently pressed (indicates intervention)
|
||||||
|
movement_keys = [
|
||||||
|
keyboard.Key.up,
|
||||||
|
keyboard.Key.down,
|
||||||
|
keyboard.Key.left,
|
||||||
|
keyboard.Key.right,
|
||||||
|
keyboard.Key.shift,
|
||||||
|
keyboard.Key.shift_r,
|
||||||
|
keyboard.Key.ctrl_r,
|
||||||
|
keyboard.Key.ctrl_l,
|
||||||
|
]
|
||||||
|
is_intervention = any(self.current_pressed.get(key, False) for key in movement_keys)
|
||||||
|
|
||||||
|
# Check for episode control commands from misc_keys_queue
|
||||||
|
terminate_episode = False
|
||||||
|
success = False
|
||||||
|
rerecord_episode = False
|
||||||
|
|
||||||
|
# Process any pending misc keys
|
||||||
|
while not self.misc_keys_queue.empty():
|
||||||
|
key = self.misc_keys_queue.get_nowait()
|
||||||
|
if key == "s":
|
||||||
|
success = True
|
||||||
|
elif key == "r":
|
||||||
|
terminate_episode = True
|
||||||
|
rerecord_episode = True
|
||||||
|
elif key == "q":
|
||||||
|
terminate_episode = True
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
TeleopEvents.IS_INTERVENTION: is_intervention,
|
||||||
|
TeleopEvents.TERMINATE_EPISODE: terminate_episode,
|
||||||
|
TeleopEvents.SUCCESS: success,
|
||||||
|
TeleopEvents.RERECORD_EPISODE: rerecord_episode,
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,22 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from .config import TeleoperatorConfig
|
from .config import TeleoperatorConfig
|
||||||
from .teleoperator import Teleoperator
|
from .teleoperator import Teleoperator
|
||||||
|
|
||||||
|
|
||||||
|
class TeleopEvents(Enum):
|
||||||
|
"""Shared constants for teleoperator events across teleoperators."""
|
||||||
|
|
||||||
|
SUCCESS = "success"
|
||||||
|
FAILURE = "failure"
|
||||||
|
RERECORD_EPISODE = "rerecord_episode"
|
||||||
|
IS_INTERVENTION = "is_intervention"
|
||||||
|
TERMINATE_EPISODE = "terminate_episode"
|
||||||
|
|
||||||
|
|
||||||
def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator:
|
def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator:
|
||||||
if config.type == "keyboard":
|
if config.type == "keyboard":
|
||||||
from .keyboard import KeyboardTeleop
|
from .keyboard import KeyboardTeleop
|
||||||
|
|||||||
Reference in New Issue
Block a user