| import os |
| import argparse |
| import gymnasium as gym |
| import torch |
| import numpy as np |
| from pathlib import Path |
| from typing import List, Dict, Any, Optional |
|
|
| from isaaclab.envs import DirectRLEnv |
| from isaaclab_tasks.utils import parse_env_cfg |
|
|
| from scripts.eval_policy import PolicyRegistry |
| from scripts.eval_policy.base_policy import BasePolicy |
|
|
| from scripts.utils.eval_utils import ( |
| convert_ee_pose_to_joints, |
| save_videos_from_observations, |
| calculate_and_print_metrics, |
| ) |
|
|
| from lehome.utils.record import ( |
| RateLimiter, |
| get_next_experiment_path_with_gap, |
| append_episode_initial_pose, |
| ) |
| from lerobot.datasets.lerobot_dataset import LeRobotDataset |
| from .common import stabilize_garment_after_reset |
| from lehome.utils.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| def run_evaluation_loop( |
| env: DirectRLEnv, |
| policy: BasePolicy, |
| args: argparse.Namespace, |
| ee_solver: Optional[Any] = None, |
| is_bimanual: bool = False, |
| garment_name: Optional[str] = None, |
| ) -> List[Dict[str, Any]]: |
| """ |
| Core evaluation loop. |
| Refactored to be agnostic of specific model implementations. |
| """ |
|
|
| |
| eval_dataset = None |
| json_path = None |
| episode_index = 0 |
| if args.save_datasets: |
| |
| |
| |
| |
| root_path = Path(args.eval_dataset_path) |
| eval_dataset = LeRobotDataset.create( |
| repo_id="lehome_eval", |
| fps=args.step_hz, |
| root=get_next_experiment_path_with_gap(root_path), |
| use_videos=True, |
| image_writer_threads=8, |
| image_writer_processes=0, |
| features=None, |
| ) |
| json_path = eval_dataset.root / "meta" / "garment_info.json" |
|
|
| all_episode_metrics = [] |
| logger.info(f"Starting evaluation: {args.num_episodes} episodes") |
| rate_limiter = RateLimiter(args.step_hz) |
|
|
| for i in range(args.num_episodes): |
| |
| env.reset() |
| policy.reset() |
| stabilize_garment_after_reset(env, args) |
|
|
| |
| object_initial_pose = env.get_all_pose() if args.save_datasets else None |
| observation_dict = env._get_observations() |
|
|
| |
| episode_frames = ( |
| {k: [] for k in observation_dict.keys() if "images" in k} |
| if args.save_video |
| else {} |
| ) |
|
|
| episode_return = 0.0 |
| episode_length = 0 |
| extra_steps = 0 |
| success_flag = False |
| success = torch.tensor(False) |
|
|
| for st in range(args.max_steps): |
| if rate_limiter: |
| rate_limiter.sleep(env) |
|
|
| |
| |
| action_np = policy.select_action(observation_dict) |
|
|
| |
| |
| action = torch.from_numpy(action_np).float().to(args.device).unsqueeze(0) |
|
|
| |
| |
| if args.use_ee_pose and ee_solver is not None: |
| current_joints = ( |
| torch.from_numpy(observation_dict["observation.state"]) |
| .float() |
| .to(args.device) |
| ) |
| action = convert_ee_pose_to_joints( |
| ee_pose_action=action.squeeze(0), |
| current_joints=current_joints, |
| solver=ee_solver, |
| is_bimanual=is_bimanual, |
| state_unit="rad", |
| device=args.device, |
| ).unsqueeze(0) |
|
|
| |
| env.step(action) |
|
|
| |
| if not success_flag: |
| success = env._get_success() |
| if success.item(): |
| success_flag = True |
| extra_steps = 50 |
|
|
| |
| reward_value = env._get_rewards() |
| if isinstance(reward_value, torch.Tensor): |
| reward = reward_value.item() |
| else: |
| reward = float(reward_value) |
|
|
| |
| episode_return += reward |
| |
| if not success_flag: |
| episode_length += 1 |
|
|
| |
| observation_dict = env._get_observations() |
|
|
| |
| if args.save_datasets: |
| frame = { |
| k: v |
| for k, v in observation_dict.items() |
| if k != "observation.top_depth" |
| } |
| frame["task"] = args.task_description |
| eval_dataset.add_frame(frame) |
|
|
| if args.save_video: |
| for key, val in observation_dict.items(): |
| if "images" in key: |
| episode_frames[key].append(val.copy()) |
|
|
| if success_flag: |
| extra_steps -= 1 |
| if extra_steps <= 0: |
| break |
|
|
| |
| is_success = success.item() if success_flag else False |
|
|
| |
| if args.save_datasets: |
| if success_flag: |
| eval_dataset.save_episode() |
| append_episode_initial_pose( |
| json_path, |
| episode_index, |
| object_initial_pose, |
| garment_name=garment_name, |
| ) |
| episode_index += 1 |
| else: |
| eval_dataset.clear_episode_buffer() |
|
|
| |
| if args.save_video: |
| save_videos_from_observations( |
| episode_frames, |
| success=success if success_flag else torch.tensor(False), |
| save_dir=args.video_dir, |
| episode_idx=i, |
| garment_name=garment_name, |
| ) |
|
|
| |
| all_episode_metrics.append( |
| {"return": episode_return, "length": episode_length, "success": is_success} |
| ) |
| logger.info( |
| f"Episode {i + 1}/{args.num_episodes}: Return={episode_return:.2f}, Length={episode_length}, Success={is_success}" |
| ) |
|
|
| return all_episode_metrics |
|
|
|
|
| def eval(args: argparse.Namespace, simulation_app: Any) -> None: |
| """ |
| Main entry point for evaluation logic. |
| """ |
| |
| env_cfg = parse_env_cfg(args.task, device=args.device) |
| env_cfg.sim.use_fabric = False |
| if args.use_random_seed: |
| env_cfg.use_random_seed = True |
| else: |
| env_cfg.use_random_seed = False |
| env_cfg.seed = args.seed |
| |
| if hasattr(env_cfg, "sim") and hasattr(env_cfg.sim, "seed"): |
| env_cfg.sim.seed = args.seed |
|
|
| env_cfg.garment_cfg_base_path = args.garment_cfg_base_path |
| env_cfg.particle_cfg_path = args.particle_cfg_path |
|
|
| |
| |
| logger.info(f"Initializing Policy Type: {args.policy_type}") |
|
|
| |
| if not PolicyRegistry.is_registered(args.policy_type): |
| available_policies = PolicyRegistry.list_policies() |
| raise ValueError( |
| f"Policy type '{args.policy_type}' not found in registry. " |
| f"Available policies: {', '.join(available_policies)}" |
| ) |
|
|
| device = "cuda" if torch.cuda.is_available() else "cpu" |
| is_bimanual = "Bi" in args.task or "bi" in args.task.lower() |
|
|
| |
| |
| policy_kwargs = { |
| "device": device, |
| } |
|
|
| if args.policy_type == "lerobot": |
| |
| if not args.policy_path: |
| raise ValueError("--policy_path is required for lerobot policy type") |
| if not args.dataset_root: |
| raise ValueError("--dataset_root is required for lerobot policy type") |
| policy_kwargs.update( |
| { |
| "policy_path": args.policy_path, |
| "dataset_root": args.dataset_root, |
| "task_description": args.task_description, |
| } |
| ) |
| else: |
| |
| if args.policy_path: |
| policy_kwargs["model_path"] = args.policy_path |
|
|
| |
| policy = PolicyRegistry.create(args.policy_type, **policy_kwargs) |
| logger.info(f"Policy '{args.policy_type}' loaded successfully") |
|
|
| |
| ee_solver = None |
| if args.use_ee_pose: |
| from lehome.utils import RobotKinematics |
|
|
| urdf_path = args.ee_urdf_path |
| joint_names = [ |
| "shoulder_pan", |
| "shoulder_lift", |
| "elbow_flex", |
| "wrist_flex", |
| "wrist_roll", |
| ] |
| ee_solver = RobotKinematics( |
| str(urdf_path), |
| target_frame_name="gripper_frame_link", |
| joint_names=joint_names, |
| ) |
| logger.info(f"IK solver loaded.") |
|
|
| |
| |
| eval_list = [] |
|
|
| |
| if args.garment_type == "custom": |
| |
| eval_list_path = os.path.join( |
| args.garment_cfg_base_path, "Release", "Release_test_list.txt" |
| ) |
| else: |
| |
| type_map = { |
| "top_long": "Top_Long", |
| "top_short": "Top_Short", |
| "pant_long": "Pant_Long", |
| "pant_short": "Pant_Short", |
| } |
| file_prefix = type_map.get(args.garment_type, "Top_Long") |
| |
| eval_list_path = os.path.join( |
| args.garment_cfg_base_path, "Release", file_prefix, f"{file_prefix}.txt" |
| ) |
|
|
| logger.info( |
| f"Loading evaluation list for category '{args.garment_type}' from: {eval_list_path}" |
| ) |
|
|
| if not os.path.exists(eval_list_path): |
| raise FileNotFoundError(f"Evaluation list not found: {eval_list_path}") |
|
|
| with open(eval_list_path, "r") as f: |
| names = [line.strip() for line in f.readlines() if line.strip()] |
| for name in names: |
| eval_list.append((name, "Release")) |
|
|
| logger.info(f"Loaded {len(eval_list)} garments for category: {args.garment_type}") |
|
|
| if not eval_list: |
| raise ValueError( |
| f"No garments found to evaluate for category '{args.garment_type}'." |
| ) |
|
|
| |
| all_garment_metrics = [] |
|
|
| |
| first_name, first_stage = eval_list[0] |
| env_cfg.garment_name = first_name |
| env_cfg.garment_version = first_stage |
| env = gym.make(args.task, cfg=env_cfg).unwrapped |
| env.initialize_obs() |
|
|
| try: |
| for garment_idx, (garment_name, garment_stage) in enumerate(eval_list): |
| logger.info( |
| f"Evaluating: {garment_name} ({garment_stage}) ({garment_idx+1}/{len(eval_list)})" |
| ) |
|
|
| |
| if garment_idx > 0: |
| if hasattr(env, "switch_garment"): |
| env.switch_garment(garment_name, garment_stage) |
| env.reset() |
| policy.reset() |
| else: |
| env.close() |
| env_cfg.garment_name = garment_name |
| env_cfg.garment_version = garment_stage |
| env = gym.make(args.task, cfg=env_cfg).unwrapped |
| env.initialize_obs() |
| policy.reset() |
|
|
| |
| metrics = run_evaluation_loop( |
| env=env, |
| policy=policy, |
| args=args, |
| ee_solver=ee_solver, |
| is_bimanual=is_bimanual, |
| garment_name=garment_name, |
| ) |
|
|
| all_garment_metrics.append( |
| {"garment_name": garment_name, "metrics": metrics} |
| ) |
|
|
| finally: |
| env.close() |
|
|
| |
| logger.info("=" * 60) |
| logger.info("Overall Summary") |
| logger.info("=" * 60) |
|
|
| if all_garment_metrics: |
| |
| all_episodes = [] |
| for garment_data in all_garment_metrics: |
| for episode_metric in garment_data["metrics"]: |
| episode_metric["garment_name"] = garment_data["garment_name"] |
| all_episodes.append(episode_metric) |
|
|
| |
| calculate_and_print_metrics(all_episodes) |
|
|
| |
| logger.info("=" * 60) |
| logger.info("Per-Garment Summary") |
| logger.info("=" * 60) |
| for garment_data in all_garment_metrics: |
| garment_name = garment_data["garment_name"] |
| metrics = garment_data["metrics"] |
| success_count = sum(1 for m in metrics if m["success"]) |
| success_rate = success_count / len(metrics) if metrics else 0.0 |
| avg_return = np.mean([m["return"] for m in metrics]) if metrics else 0.0 |
| logger.info( |
| f" {garment_name}: Success Rate = {success_rate:.2%}, Avg Return = {avg_return:.2f}" |
| ) |
| else: |
| logger.info("No metrics collected (all evaluations failed)") |
|
|
| logger.info("=" * 60) |
| logger.info("Evaluation completed successfully") |
| logger.info("=" * 60) |
|
|