Files
lerobot/additional_functions/test_full_pipeline.py
T
2025-11-21 14:13:05 +01:00

276 lines
9.5 KiB
Python

#!/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.
"""
Test script to measure each component of the teleoperation pipeline separately.
Example:
```shell
python test_full_pipeline.py \
--robot.type=unitree_g1 \
--teleop.type=custom \
--fps=60 \
--duration=10.0
```
"""
import logging
import time
from dataclasses import asdict, dataclass
from pprint import pformat
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig # noqa: F401
from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraConfig # noqa: F401
from lerobot.cameras.zmq.configuration_zmq import ZMQCameraConfig # noqa: F401
from lerobot.configs import parser
from lerobot.processor import (
make_default_processors,
)
from lerobot.robots import ( # noqa: F401
Robot,
RobotConfig,
bi_so100_follower,
hope_jr,
koch_follower,
make_robot_from_config,
so100_follower,
so101_follower,
unitree_g1,
)
from lerobot.teleoperators import ( # noqa: F401
Teleoperator,
TeleoperatorConfig,
bi_so100_leader,
gamepad,
homunculus,
koch_leader,
make_teleoperator_from_config,
so100_leader,
so101_leader,
custom
)
from lerobot.utils.import_utils import register_third_party_devices
from lerobot.utils.robot_utils import busy_wait
from lerobot.utils.utils import init_logging, move_cursor_up
@dataclass
class TestPipelineConfig:
teleop: TeleoperatorConfig
robot: RobotConfig
# Limit the maximum frames per second.
fps: int = 60
# Test duration in seconds
duration: float = 10.0
def test_pipeline_loop(
teleop: Teleoperator,
robot: Robot,
fps: int,
duration: float,
):
"""
Test loop that measures timing of each pipeline component separately.
Args:
teleop: The teleoperator device instance.
robot: The robot instance.
fps: The target frequency for the control loop in frames per second.
duration: The duration of the test in seconds.
"""
teleop_action_processor, robot_action_processor, robot_observation_processor = make_default_processors()
start = time.perf_counter()
iteration = 0
# Timing accumulators
total_obs_time = 0
total_teleop_read_time = 0
total_teleop_process_time = 0
total_robot_process_time = 0
total_robot_send_time = 0
min_obs_time = float('inf')
max_obs_time = 0
min_teleop_read_time = float('inf')
max_teleop_read_time = 0
min_teleop_process_time = float('inf')
max_teleop_process_time = 0
min_robot_process_time = float('inf')
max_robot_process_time = 0
min_robot_send_time = float('inf')
max_robot_send_time = 0
print(f"\nStarting full pipeline test for {duration}s at {fps} FPS target...")
print(f"Robot: {robot.__class__.__name__}")
print(f"Teleoperator: {teleop.__class__.__name__}")
print("-" * 80)
while True:
loop_start = time.perf_counter()
# 1. Get robot observation
t1 = time.perf_counter()
obs = robot.get_observation()
obs_time = time.perf_counter() - t1
# 2. Get teleop action
t2 = time.perf_counter()
raw_action = teleop.get_action()
teleop_read_time = time.perf_counter() - t2
# 3. Process teleop action
t3 = time.perf_counter()
teleop_action = teleop_action_processor((raw_action, obs))
teleop_process_time = time.perf_counter() - t3
# 4. Process action for robot
t4 = time.perf_counter()
robot_action_to_send = robot_action_processor((teleop_action, obs))
robot_process_time = time.perf_counter() - t4
# 5. Send action to robot
t5 = time.perf_counter()
_ = robot.send_action(robot_action_to_send)
robot_send_time = time.perf_counter() - t5
# Track statistics
total_obs_time += obs_time
total_teleop_read_time += teleop_read_time
total_teleop_process_time += teleop_process_time
total_robot_process_time += robot_process_time
total_robot_send_time += robot_send_time
min_obs_time = min(min_obs_time, obs_time)
max_obs_time = max(max_obs_time, obs_time)
min_teleop_read_time = min(min_teleop_read_time, teleop_read_time)
max_teleop_read_time = max(max_teleop_read_time, teleop_read_time)
min_teleop_process_time = min(min_teleop_process_time, teleop_process_time)
max_teleop_process_time = max(max_teleop_process_time, teleop_process_time)
min_robot_process_time = min(min_robot_process_time, robot_process_time)
max_robot_process_time = max(max_robot_process_time, robot_process_time)
min_robot_send_time = min(min_robot_send_time, robot_send_time)
max_robot_send_time = max(max_robot_send_time, robot_send_time)
iteration += 1
# Display timing breakdown
total_component_time = obs_time + teleop_read_time + teleop_process_time + robot_process_time + robot_send_time
print(f"\nIteration: {iteration}")
print(f"1. robot.get_observation(): {obs_time * 1e3:>7.2f}ms")
print(f"2. teleop.get_action(): {teleop_read_time * 1e3:>7.2f}ms")
print(f"3. teleop_action_processor: {teleop_process_time * 1e3:>7.2f}ms")
print(f"4. robot_action_processor: {robot_process_time * 1e3:>7.2f}ms")
print(f"5. robot.send_action(): {robot_send_time * 1e3:>7.2f}ms")
print(f" Total component time: {total_component_time * 1e3:>7.2f}ms")
# Show wrist roll values
print(f"\nkLeftWristRoll.pos: {robot_action_to_send.get('kLeftWristRoll.pos', 'N/A')}")
print(f"kRightWristRoll.pos: {robot_action_to_send.get('kRightWristRoll.pos', 'N/A')}")
move_cursor_up(11)
# Wait to maintain target FPS
dt_s = time.perf_counter() - loop_start
busy_wait(1 / fps - dt_s)
loop_time = time.perf_counter() - loop_start
# Check if duration reached
elapsed = time.perf_counter() - start
if elapsed >= duration:
break
# Print final statistics
print("\n" + "=" * 80)
print("FINAL TIMING BREAKDOWN")
print("=" * 80)
print(f"Total iterations: {iteration}")
print(f"Total time: {elapsed:.2f}s")
print(f"Actual FPS: {iteration / elapsed:.2f}")
print(f"Target FPS: {fps}")
print("\n" + "-" * 80)
print(f"{'Component':<35} {'Avg (ms)':>10} {'Min (ms)':>10} {'Max (ms)':>10} {'% of time':>10}")
print("-" * 80)
avg_obs = total_obs_time / iteration * 1e3
avg_teleop_read = total_teleop_read_time / iteration * 1e3
avg_teleop_proc = total_teleop_process_time / iteration * 1e3
avg_robot_proc = total_robot_process_time / iteration * 1e3
avg_robot_send = total_robot_send_time / iteration * 1e3
total_avg = avg_obs + avg_teleop_read + avg_teleop_proc + avg_robot_proc + avg_robot_send
print(f"{'1. robot.get_observation()':<35} {avg_obs:>10.2f} {min_obs_time*1e3:>10.2f} {max_obs_time*1e3:>10.2f} {avg_obs/total_avg*100:>9.1f}%")
print(f"{'2. teleop.get_action()':<35} {avg_teleop_read:>10.2f} {min_teleop_read_time*1e3:>10.2f} {max_teleop_read_time*1e3:>10.2f} {avg_teleop_read/total_avg*100:>9.1f}%")
print(f"{'3. teleop_action_processor':<35} {avg_teleop_proc:>10.2f} {min_teleop_process_time*1e3:>10.2f} {max_teleop_process_time*1e3:>10.2f} {avg_teleop_proc/total_avg*100:>9.1f}%")
print(f"{'4. robot_action_processor':<35} {avg_robot_proc:>10.2f} {min_robot_process_time*1e3:>10.2f} {max_robot_process_time*1e3:>10.2f} {avg_robot_proc/total_avg*100:>9.1f}%")
print(f"{'5. robot.send_action()':<35} {avg_robot_send:>10.2f} {min_robot_send_time*1e3:>10.2f} {max_robot_send_time*1e3:>10.2f} {avg_robot_send/total_avg*100:>9.1f}%")
print("-" * 80)
print(f"{'TOTAL':<35} {total_avg:>10.2f}")
print("=" * 80)
@parser.wrap()
def test_pipeline(cfg: TestPipelineConfig):
init_logging()
logging.info(pformat(asdict(cfg)))
print("\nInitializing teleoperator...")
teleop = make_teleoperator_from_config(cfg.teleop)
print("Initializing robot...")
robot = make_robot_from_config(cfg.robot)
print("Connecting teleoperator...")
teleop.connect()
print("Connecting robot...")
robot.connect()
print(f"\nTeleoperator connected: {teleop.is_connected}")
print(f"Teleoperator calibrated: {teleop.is_calibrated}")
print(f"Robot connected: {robot.is_connected}")
try:
test_pipeline_loop(
teleop=teleop,
robot=robot,
fps=cfg.fps,
duration=cfg.duration,
)
except KeyboardInterrupt:
print("\nTest interrupted by user")
finally:
print("\nDisconnecting...")
teleop.disconnect()
robot.disconnect()
def main():
register_third_party_devices()
test_pipeline()
if __name__ == "__main__":
main()