File size: 14,691 Bytes
1cc903b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 | 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.
"""
# --- Dataset Recording Setup (Optional) ---
eval_dataset = None
json_path = None
episode_index = 0
if args.save_datasets:
# Note: We might need to handle 'features' argument if dataset saving is strictly required,
# but for simplicity in Challenge mode, we often skip strictly matching metadata features.
# Or you can expose policy.meta if available.
# Here we initialize vaguely to keep it simple.
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, # Let LeRobot infer or pass explicitly if needed
)
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):
# 1. Reset Environment & Policy
env.reset()
policy.reset()
stabilize_garment_after_reset(env, args)
# 2. Initial Observation (Numpy)
object_initial_pose = env.get_all_pose() if args.save_datasets else None
observation_dict = env._get_observations()
# Prepare for video recording
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)
# 3. Policy Inference (The core abstraction)
# Input: Numpy Dict -> Output: Numpy Array
action_np = policy.select_action(observation_dict)
# 4. Prepare Action for Environment (Tensor)
# Convert numpy action to tensor for Isaac Lab
action = torch.from_numpy(action_np).float().to(args.device).unsqueeze(0)
# 5. Inverse Kinematics (Optional Helper Logic)
# If policy outputs EE pose but env needs joints
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)
# 6. Step Environment
env.step(action)
# Check success first
if not success_flag:
success = env._get_success()
if success.item():
success_flag = True
extra_steps = 50 # Run a bit longer after success to settle
# Get reward from environment (Isaac Lab stores rewards internally)
reward_value = env._get_rewards()
if isinstance(reward_value, torch.Tensor):
reward = reward_value.item()
else:
reward = float(reward_value)
# Accumulate reward for all steps (including post-success steps)
episode_return += reward
# Only count length before success (for consistency with episode termination)
if not success_flag:
episode_length += 1
# Update Observation
observation_dict = env._get_observations()
# Recording
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
# --- End of Episode Handling ---
is_success = success.item() if success_flag else False
# Save Datasets
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()
# Save Videos (Using generic util)
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, #新增
)
# Log Metrics
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.
"""
# 1. Environment Configuration
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
# Propagate seed to sim config if structure exists
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
# 2. Initialize Policy (Using the Policy Registry)
# This replaces create_il_policy, make_pre_post_processors, etc.
logger.info(f"Initializing Policy Type: {args.policy_type}")
# Check if policy is registered
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()
# Create policy instance from registry with appropriate arguments
# Different policies may require different initialization arguments
policy_kwargs = {
"device": device,
}
if args.policy_type == "lerobot":
# LeRobot policy requires policy_path and dataset_root
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:
# For custom policies, pass policy_path as model_path if provided
if args.policy_path:
policy_kwargs["model_path"] = args.policy_path
# Create policy from registry
policy = PolicyRegistry.create(args.policy_type, **policy_kwargs)
logger.info(f"Policy '{args.policy_type}' loaded successfully")
# 3. Initialize IK Solver (If needed)
ee_solver = None
if args.use_ee_pose:
from lehome.utils import RobotKinematics
urdf_path = args.ee_urdf_path # Assuming path is handled or add check logic
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.")
# 4. Load Evaluation List
# Only loads from 'Release' directory based on garment_type
eval_list = [] # List of (name, stage)
# Evaluate a specific category based on garment_type
if args.garment_type == "custom":
# For 'custom' type, we load from the root Release_test_list.txt
eval_list_path = os.path.join(
args.garment_cfg_base_path, "Release", "Release_test_list.txt"
)
else:
# Map argument to specific sub-category directory
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")
# Path: Assets/objects/Challenge_Garment/Release/Top_Long/Top_Long.txt
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}'."
)
# 5. Main Evaluation Loops
all_garment_metrics = []
# Init Env with first garment
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)})"
)
# Switch Garment Logic
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()
# Run Loop
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()
# Print summary across all garments
logger.info("=" * 60)
logger.info("Overall Summary")
logger.info("=" * 60)
if all_garment_metrics:
# Aggregate all episode 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)
# Print overall metrics
calculate_and_print_metrics(all_episodes)
# Print per-garment summary
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)
|