| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from copy import deepcopy |
| from dataclasses import dataclass, field |
| import logging |
| from pathlib import Path |
| import re |
| from typing import Any |
| import warnings |
|
|
| from gr00t.data.dataset.lerobot_episode_loader import LeRobotEpisodeLoader |
| from gr00t.data.dataset.sharded_single_step_dataset import extract_step_data |
| from gr00t.data.embodiment_tags import EmbodimentTag |
| from gr00t.data.utils import parse_observation_gr00t |
| from gr00t.eval._horizon_contract import PolicyHorizonSpec, migrate_deprecated_action_horizon_argv |
| from gr00t.policy import BasePolicy |
| from gr00t.policy.gr00t_policy import Gr00tPolicy |
| from gr00t.policy.server_client import PolicyClient |
| from matplotlib import pyplot as plt |
| import numpy as np |
| import pandas as pd |
| import tyro |
|
|
|
|
| warnings.simplefilter("ignore", category=FutureWarning) |
|
|
| """ |
| Example commands: |
| |
| NOTE: provide --model_path to load up the model checkpoint in this script, |
| else it will use the default host and port via RobotInferenceClient |
| |
| """ |
|
|
|
|
| def plot_trajectory_results( |
| state_joints_across_time: np.ndarray, |
| gt_action_across_time: np.ndarray, |
| pred_action_across_time: np.ndarray, |
| traj_id: int, |
| state_keys: list[str], |
| action_keys: list[str], |
| execution_horizon: int, |
| save_plot_path: str, |
| ) -> None: |
| """ |
| Plot and save trajectory results comparing ground truth and predicted actions. |
| |
| Args: |
| state_joints_across_time: Array of state joints over time |
| gt_action_across_time: Ground truth actions over time |
| pred_action_across_time: Predicted actions over time |
| traj_id: Trajectory ID |
| state_keys: List of state modality keys |
| action_keys: List of action modality keys |
| execution_horizon: Number of predicted-chunk steps executed per inference |
| save_plot_path: Path to save the plot |
| """ |
| actual_steps = len(gt_action_across_time) |
| action_dim = gt_action_across_time.shape[1] |
|
|
| indices_to_plot = list(range(action_dim)) |
|
|
| num_plots = len(indices_to_plot) |
| if num_plots == 0: |
| logging.warning("No valid indices to plot") |
| return |
|
|
| |
| fig, axes = plt.subplots(nrows=num_plots, ncols=1, figsize=(8, 4 * num_plots)) |
|
|
| |
| if num_plots == 1: |
| axes = [axes] |
|
|
| |
| fig.suptitle( |
| f"Trajectory {traj_id} - State: {', '.join(state_keys)} | Action: {', '.join(action_keys)}", |
| fontsize=16, |
| color="blue", |
| ) |
|
|
| for plot_idx, action_idx in enumerate(indices_to_plot): |
| ax = axes[plot_idx] |
|
|
| |
| |
| |
| if state_joints_across_time.shape == gt_action_across_time.shape: |
| ax.plot(state_joints_across_time[:, action_idx], label="state joints") |
| ax.plot(gt_action_across_time[:, action_idx], label="gt action") |
| ax.plot(pred_action_across_time[:, action_idx], label="pred action") |
|
|
| |
| for j in range(0, actual_steps, execution_horizon): |
| if j == 0: |
| ax.plot( |
| j, |
| gt_action_across_time[j, action_idx], |
| "ro", |
| label="inference point", |
| ) |
| else: |
| ax.plot(j, gt_action_across_time[j, action_idx], "ro") |
|
|
| ax.set_title(f"Action {action_idx}") |
| ax.legend() |
|
|
| plt.tight_layout() |
|
|
| |
| Path(save_plot_path).parent.mkdir(parents=True, exist_ok=True) |
| plt.savefig(save_plot_path) |
|
|
| plt.close() |
|
|
|
|
| def parse_action_gr00t(action: dict[str, Any]) -> dict[str, Any]: |
| |
| return {f"action.{key}": action[key][0] for key in action} |
|
|
|
|
| def evaluate_single_trajectory( |
| policy: BasePolicy, |
| loader: LeRobotEpisodeLoader, |
| traj_id: int, |
| embodiment_tag: EmbodimentTag, |
| modality_keys: list[str] | None = None, |
| steps=300, |
| execution_horizon=16, |
| save_plot_path=None, |
| ): |
| |
| traj = loader[traj_id] |
| traj_length = len(traj) |
| actual_steps = min(steps, traj_length) |
| logging.info( |
| f"Using {actual_steps} steps (requested: {steps}, trajectory length: {traj_length})" |
| ) |
|
|
| pred_action_across_time = [] |
|
|
| |
| state_keys = loader.modality_configs["state"].modality_keys |
| action_keys = ( |
| loader.modality_configs["action"].modality_keys if modality_keys is None else modality_keys |
| ) |
|
|
| |
| |
| |
| PolicyHorizonSpec.from_modality_config( |
| loader.modality_configs, n_action_steps=execution_horizon |
| ) |
|
|
| modality_configs = deepcopy(loader.modality_configs) |
| modality_configs.pop("action") |
| for step_count in range(0, actual_steps, execution_horizon): |
| data_point = extract_step_data(traj, step_count, modality_configs, embodiment_tag) |
| logging.info(f"inferencing at step: {step_count}") |
| obs = {} |
| for k, v in data_point.states.items(): |
| obs[f"state.{k}"] = v |
| for k, v in data_point.images.items(): |
| obs[f"video.{k}"] = np.array(v) |
| for language_key in loader.modality_configs["language"].modality_keys: |
| obs[language_key] = data_point.text |
| parsed_obs = parse_observation_gr00t(obs, loader.modality_configs) |
| _action_chunk, _ = policy.get_action(parsed_obs) |
| action_chunk = parse_action_gr00t(_action_chunk) |
| for j in range(execution_horizon): |
| |
| |
| concat_pred_action = np.concatenate( |
| [ |
| np.atleast_1d(np.atleast_1d(action_chunk[f"action.{key}"])[j]) |
| for key in action_keys |
| ], |
| axis=0, |
| ) |
| pred_action_across_time.append(concat_pred_action) |
|
|
| def extract_state_joints(traj: pd.DataFrame, columns: list[str]): |
| np_dict = {} |
| for column in columns: |
| np_dict[column] = np.vstack([arr for arr in traj[column]]) |
| return np.concatenate([np_dict[column] for column in columns], axis=-1) |
|
|
| |
| state_joints_across_time = extract_state_joints(traj, [f"state.{key}" for key in state_keys]) |
| gt_action_across_time = extract_state_joints(traj, [f"action.{key}" for key in action_keys])[ |
| :actual_steps |
| ] |
| pred_action_across_time = np.array(pred_action_across_time)[:actual_steps] |
| assert gt_action_across_time.shape == pred_action_across_time.shape, ( |
| f"gt_action: {gt_action_across_time.shape}, pred_action: {pred_action_across_time.shape}" |
| ) |
|
|
| |
| mse = np.mean((gt_action_across_time - pred_action_across_time) ** 2) |
| mae = np.mean(np.abs(gt_action_across_time - pred_action_across_time)) |
| logging.info(f"Unnormalized Action MSE across single traj: {mse}") |
| logging.info(f"Unnormalized Action MAE across single traj: {mae}") |
|
|
| logging.info(f"state_joints vs time {state_joints_across_time.shape}") |
| logging.info(f"gt_action_joints vs time {gt_action_across_time.shape}") |
| logging.info(f"pred_action_joints vs time {pred_action_across_time.shape}") |
|
|
| |
| plot_trajectory_results( |
| state_joints_across_time=state_joints_across_time, |
| gt_action_across_time=gt_action_across_time, |
| pred_action_across_time=pred_action_across_time, |
| traj_id=traj_id, |
| state_keys=state_keys, |
| action_keys=action_keys, |
| execution_horizon=execution_horizon, |
| save_plot_path=save_plot_path or f"/tmp/open_loop_eval/traj_{traj_id}.jpeg", |
| ) |
|
|
| return mse, mae |
|
|
|
|
| @dataclass |
| class ArgsConfig: |
| """Configuration for evaluating a policy.""" |
|
|
| host: str = "127.0.0.1" |
| """Host to connect to.""" |
|
|
| port: int = 5555 |
| """Port to connect to.""" |
|
|
| steps: int = 200 |
| """Maximum number of steps to evaluate (will be capped by trajectory length).""" |
|
|
| traj_ids: list[int] = field(default_factory=lambda: [0]) |
| """List of trajectory IDs to evaluate.""" |
|
|
| execution_horizon: int = 16 |
| """How many steps of each predicted action chunk to execute before re-planning |
| (must be <= the model's predicted chunk length).""" |
|
|
| dataset_path: str = "demo_data/cube_to_bowl_5/" |
| """Path to the dataset.""" |
|
|
| embodiment_tag: str = "new_embodiment" |
| """Embodiment tag (name or value, case-insensitive). Run with --help to see known tags.""" |
|
|
| model_path: str | None = None |
| """Path to the model checkpoint.""" |
|
|
| denoising_steps: int = 4 |
| """Number of denoising steps to use.""" |
|
|
| save_plot_path: str | None = None |
| """Path to save the plot to.""" |
|
|
| modality_keys: list[str] | None = None |
| """List of modality keys to plot. If None, plot all keys.""" |
|
|
|
|
| def main(args: ArgsConfig): |
| args.embodiment_tag = EmbodimentTag.resolve(args.embodiment_tag) |
| |
| logging.basicConfig(level=logging.INFO) |
|
|
| |
| local_model_path = args.model_path |
|
|
| |
| global_step = None |
| if local_model_path: |
| |
| match = re.search(r"checkpoint-(\d+)", local_model_path) |
| if match: |
| try: |
| global_step = int(match.group(1)) |
| logging.info(f"Extracted global_step {global_step} from checkpoint path") |
| except ValueError: |
| logging.warning( |
| f"Could not parse step number from checkpoint path: {local_model_path}" |
| ) |
| else: |
| logging.warning(f"Could not find checkpoint-<step> pattern in path: {local_model_path}") |
|
|
| if local_model_path is not None: |
| import torch |
|
|
| policy = Gr00tPolicy( |
| embodiment_tag=args.embodiment_tag, |
| model_path=local_model_path, |
| device="cuda" if torch.cuda.is_available() else "cpu", |
| ) |
| |
| |
| policy.model.action_head.num_inference_timesteps = args.denoising_steps |
| logging.info(f"Using {args.denoising_steps} denoising steps") |
| else: |
| policy = PolicyClient(host=args.host, port=args.port) |
| if args.denoising_steps != ArgsConfig.denoising_steps: |
| logging.warning( |
| "--denoising-steps=%d is ignored when running against a remote " |
| "policy server; set the denoising steps on the server " |
| "(run_gr00t_server.py) instead.", |
| args.denoising_steps, |
| ) |
|
|
| |
| modality = policy.get_modality_config() |
| logging.info(f"Current modality config: \n{modality}") |
|
|
| |
| dataset = LeRobotEpisodeLoader( |
| dataset_path=args.dataset_path, |
| modality_configs=modality, |
| ) |
|
|
| logging.info(f"Dataset length: {len(dataset)}") |
| logging.info(f"Running evaluation on trajectories: {args.traj_ids}") |
|
|
| all_mse = [] |
| all_mae = [] |
|
|
| for traj_id in args.traj_ids: |
| if traj_id >= len(dataset): |
| logging.warning(f"Trajectory ID {traj_id} is out of range. Skipping.") |
| continue |
|
|
| logging.info(f"Running trajectory: {traj_id}") |
| mse, mae = evaluate_single_trajectory( |
| policy, |
| dataset, |
| traj_id, |
| args.embodiment_tag, |
| args.modality_keys, |
| steps=args.steps, |
| execution_horizon=args.execution_horizon, |
| save_plot_path=args.save_plot_path, |
| ) |
| logging.info(f"MSE for trajectory {traj_id}: {mse}, MAE: {mae}") |
| all_mse.append(mse) |
| all_mae.append(mae) |
|
|
| if all_mse: |
| avg_mse = np.mean(np.array(all_mse)) |
| avg_mae = np.mean(np.array(all_mae)) |
| logging.info(f"Average MSE across all trajs: {avg_mse}") |
| logging.info(f"Average MAE across all trajs: {avg_mae}") |
| else: |
| logging.info("No valid trajectories were evaluated.") |
| logging.info("Done") |
|
|
|
|
| if __name__ == "__main__": |
| if migrate_deprecated_action_horizon_argv(): |
| logging.warning("--action-horizon is deprecated; use --execution-horizon.") |
| |
| config = tyro.cli(ArgsConfig) |
| main(config) |
|
|