| |
| |
| |
| |
|
|
| """ |
| Visualize system identification fit by replaying real waypoints through |
| the manager-based Sysid env (same RelCartesianOSCAction as sysid_ur5e_osc.py). |
| |
| Loads a CMA-ES checkpoint, applies best params to the sim, runs closed-loop |
| replay, and plots sim vs. real joint trajectories. |
| |
| Usage: |
| python scripts_v2/tools/sim2real/plot_sysid_fit.py --headless \ |
| --checkpoint logs/sysid/YYYYMMDD_HHMMSS/checkpoint_0200.pt \ |
| --real_data sysid_data_real.pt |
| """ |
|
|
| import argparse |
| import matplotlib |
| import numpy as np |
| import os |
| import torch |
|
|
| matplotlib.use("Agg") |
| import gymnasium as gym |
| import matplotlib.pyplot as plt |
|
|
| from isaaclab.app import AppLauncher |
|
|
| parser = argparse.ArgumentParser(description="Plot sysid fit") |
| parser.add_argument("--checkpoint", type=str, required=True, help="Path to sysid checkpoint .pt") |
| parser.add_argument("--real_data", type=str, required=True, help="Path to real data .pt") |
| parser.add_argument("--max_steps", type=int, default=None) |
|
|
| AppLauncher.add_app_launcher_args(parser) |
| args_cli = parser.parse_args() |
|
|
| app_launcher = AppLauncher(args_cli) |
| simulation_app = app_launcher.app |
|
|
| from isaaclab.actuators import DelayedPDActuatorCfg |
| from isaaclab.assets import Articulation |
| from isaaclab.utils.math import subtract_frame_transforms |
|
|
| from uwlab_assets.robots.ur5e_robotiq_gripper.kinematics import ARM_JOINT_NAMES, EE_BODY_NAME, NUM_ARM_JOINTS |
|
|
| import uwlab_tasks |
| from uwlab_tasks.manager_based.manipulation.omnireset.config.ur5e_robotiq_2f85.sysid_cfg import SysidEnvCfg |
| from uwlab_tasks.manager_based.manipulation.omnireset.mdp.utils import settle_robot, target_pose_to_action |
|
|
| |
| |
| |
|
|
|
|
| def apply_params(robot, params, arm_joint_ids, num_joints, device): |
| """Apply 25-element param vector (single env) to robot.""" |
| N = 1 |
| env_ids = torch.arange(N, device=device) |
| p = torch.tensor(params, device=device, dtype=torch.float32).unsqueeze(0) |
|
|
| armature_full = torch.zeros(N, num_joints, device=device) |
| static_friction_full = torch.zeros(N, num_joints, device=device) |
| dynamic_friction_full = torch.zeros(N, num_joints, device=device) |
| viscous_friction_full = torch.zeros(N, num_joints, device=device) |
| armature_full[:, arm_joint_ids] = p[:, 0:6] |
| static_fric = p[:, 6:12] |
| dynamic_ratio = p[:, 12:18] |
| static_friction_full[:, arm_joint_ids] = static_fric |
| dynamic_friction_full[:, arm_joint_ids] = dynamic_ratio * static_fric |
| viscous_friction_full[:, arm_joint_ids] = p[:, 18:24] |
| robot.write_joint_armature_to_sim(armature_full, env_ids=env_ids) |
| robot.write_joint_friction_coefficient_to_sim( |
| static_friction_full, |
| joint_dynamic_friction_coeff=dynamic_friction_full, |
| joint_viscous_friction_coeff=viscous_friction_full, |
| env_ids=env_ids, |
| ) |
|
|
| delay_int = int(round(float(p[0, 24]))) |
| arm_actuator = robot.actuators["arm"] |
| delay_tensor = torch.tensor([delay_int], device=device, dtype=torch.int) |
| arm_actuator.positions_delay_buffer.set_time_lag(delay_tensor) |
| arm_actuator.velocities_delay_buffer.set_time_lag(delay_tensor) |
| arm_actuator.efforts_delay_buffer.set_time_lag(delay_tensor) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def closed_loop_replay( |
| env, |
| wp_step_indices, |
| wp_target_pos, |
| wp_target_quat, |
| initial_joint_pos, |
| arm_joint_ids, |
| ee_frame_idx, |
| sim_dt, |
| T_steps, |
| headless=True, |
| ): |
| """Run closed-loop replay using env's RelCartesianOSC. Returns sim trajectory dict.""" |
| unwrapped = env.unwrapped |
| robot = unwrapped.scene["robot"] |
| sim = unwrapped.sim |
| device = unwrapped.device |
| action_dim = unwrapped.action_manager.total_action_dim |
| W = wp_step_indices.shape[0] |
|
|
| default_joint_pos = robot.data.default_joint_pos.clone() |
| default_joint_vel = robot.data.default_joint_vel.clone() |
| default_joint_pos[:, arm_joint_ids] = initial_joint_pos.unsqueeze(0) |
| default_joint_vel[:] = 0.0 |
| env.reset() |
| settle_robot(robot, sim, default_joint_pos, default_joint_vel, arm_joint_ids, sim_dt, headless=headless) |
|
|
| sim_positions, sim_velocities, sim_ee_positions = [], [], [] |
| wp_idx = 0 |
|
|
| for t in range(T_steps): |
| while wp_idx + 1 < W and t >= wp_step_indices[wp_idx + 1]: |
| wp_idx += 1 |
|
|
| ee_pos_w = robot.data.body_pos_w[:, ee_frame_idx] |
| ee_quat_w = robot.data.body_quat_w[:, ee_frame_idx] |
| ee_pos_b, ee_quat_b = subtract_frame_transforms( |
| robot.data.root_pos_w, robot.data.root_quat_w, ee_pos_w, ee_quat_w |
| ) |
| target_pos = wp_target_pos[wp_idx].unsqueeze(0) |
| target_quat = wp_target_quat[wp_idx].unsqueeze(0) |
|
|
| action_arm = target_pose_to_action(ee_pos_b, ee_quat_b, target_pos, target_quat) |
| action = torch.cat([action_arm, torch.zeros(1, action_dim - 6, device=device)], dim=-1) |
| env.step(action) |
|
|
| joint_pos = robot.data.joint_pos[:, arm_joint_ids] |
| joint_vel = robot.data.joint_vel[:, arm_joint_ids] |
| sim_positions.append(joint_pos[0].cpu().numpy().copy()) |
| sim_velocities.append(joint_vel[0].cpu().numpy().copy()) |
| sim_ee_positions.append(ee_pos_b[0].cpu().numpy().copy()) |
|
|
| if (t + 1) % max(1, T_steps // 20) == 0: |
| print(f" step {t+1}/{T_steps} ({100*(t+1)/T_steps:.0f}%)") |
|
|
| return { |
| "joint_positions": np.array(sim_positions), |
| "joint_velocities": np.array(sim_velocities), |
| "ee_positions": np.array(sim_ee_positions), |
| } |
|
|
|
|
| |
| |
| |
|
|
| JOINT_NAMES_SHORT = ["Shoulder Pan", "Shoulder Lift", "Elbow", "Wrist 1", "Wrist 2", "Wrist 3"] |
|
|
|
|
| def plot_overlay(real_joints, sim_joints, dt, save_path="sysid_fit.png"): |
| """Plot sim vs real joint positions.""" |
| T = real_joints.shape[0] |
| time_axis = np.arange(T) * dt |
|
|
| fig, axes = plt.subplots(3, 2, figsize=(16, 10), sharex=True) |
| axes = axes.flatten() |
|
|
| for j in range(NUM_ARM_JOINTS): |
| ax = axes[j] |
| ax.plot(time_axis, np.degrees(real_joints[:, j]), "b-", linewidth=1.0, label="Real", alpha=0.8) |
| ax.plot(time_axis, np.degrees(sim_joints[:, j]), "r-", linewidth=1.0, label="Sim", alpha=0.8) |
| ax.set_title(JOINT_NAMES_SHORT[j], fontsize=11) |
| ax.set_ylabel("deg") |
| ax.legend(loc="upper right", fontsize=8) |
| ax.grid(True, alpha=0.3) |
|
|
| axes[-2].set_xlabel("Time (s)") |
| axes[-1].set_xlabel("Time (s)") |
| fig.suptitle("Sysid Fit: Sim vs Real Joint Trajectories", fontsize=13) |
| fig.tight_layout() |
| fig.savefig(save_path, dpi=150) |
| print(f"Saved overlay plot: {save_path}") |
| plt.close(fig) |
|
|
|
|
| def plot_error(real_joints, sim_joints, dt, save_path="sysid_fit_error.png"): |
| """Plot per-joint error over time.""" |
| T = real_joints.shape[0] |
| time_axis = np.arange(T) * dt |
| error_deg = np.degrees(sim_joints - real_joints) |
|
|
| fig, axes = plt.subplots(3, 2, figsize=(16, 10), sharex=True) |
| axes = axes.flatten() |
|
|
| for j in range(NUM_ARM_JOINTS): |
| ax = axes[j] |
| ax.plot(time_axis, error_deg[:, j], "k-", linewidth=0.8, alpha=0.7) |
| ax.axhline(y=0, color="gray", linestyle="--", alpha=0.5) |
| rmse_j = np.sqrt(np.mean(error_deg[:, j] ** 2)) |
| ax.set_title(f"{JOINT_NAMES_SHORT[j]} (RMSE={rmse_j:.2f}°)", fontsize=11) |
| ax.set_ylabel("Error (deg)") |
| ax.grid(True, alpha=0.3) |
|
|
| axes[-2].set_xlabel("Time (s)") |
| axes[-1].set_xlabel("Time (s)") |
| fig.suptitle("Sysid Fit: Per-Joint Error", fontsize=13) |
| fig.tight_layout() |
| fig.savefig(save_path, dpi=150) |
| print(f"Saved error plot: {save_path}") |
| plt.close(fig) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def main(): |
| args = args_cli |
| device_str = args.device |
|
|
| |
| print(f"\nLoading checkpoint: {args.checkpoint}") |
| ckpt = torch.load(args.checkpoint, map_location="cpu", weights_only=False) |
| best_params = ckpt["best_params"] |
| best_score = ckpt["best_score"] |
| ckpt_args = ckpt.get("args", {}) |
| print(f" Score (MSE): {best_score:.6f} RMSE: {np.degrees(np.sqrt(best_score)):.4f}°") |
| print(f" Checkpoint args: sim_dt={ckpt_args.get('sim_dt', 'N/A')}") |
|
|
| |
| arm = best_params[:6] |
| sfric = best_params[6:12] |
| dratio = best_params[12:18] |
| vfric = best_params[18:24] |
| delay = round(float(best_params[24])) |
| print(f"\n {'Joint':<20s} {'Armature':>10s} {'SFric':>10s} {'DRatio':>10s} {'VFric':>10s}") |
| for i, name in enumerate(JOINT_NAMES_SHORT): |
| print(f" {name:<20s} {arm[i]:10.4f} {sfric[i]:10.4f} {dratio[i]:10.4f} {vfric[i]:10.4f}") |
| print(f" Motor delay: {delay} steps") |
|
|
| |
| print(f"\nLoading real data: {args.real_data}") |
| real_data = torch.load(args.real_data, map_location="cpu", weights_only=False) |
| real_joint_pos = real_data["joint_positions"] |
| initial_joint_pos = real_data["initial_joint_pos"] |
| wp_step_indices = real_data["waypoint_step_indices"] |
| wp_target_pos = real_data["waypoint_target_pos"] |
| wp_target_quat = real_data["waypoint_target_quat"] |
| dt = real_data["dt"] |
|
|
| T_steps = real_joint_pos.shape[0] |
| if args.max_steps is not None: |
| T_steps = min(T_steps, args.max_steps) |
|
|
| print(f" {T_steps} steps ({T_steps*dt:.2f}s), dt={dt*1000:.1f}ms") |
|
|
| |
| real_joint_pos_np = real_joint_pos[:T_steps].numpy() |
| initial_joint_pos_dev = initial_joint_pos.to(device_str).float() |
| wp_step_indices = wp_step_indices.to(device_str).long() |
| wp_target_pos = wp_target_pos.to(device_str).float() |
| wp_target_quat = wp_target_quat.to(device_str).float() |
|
|
| |
| env_cfg = SysidEnvCfg() |
| env_cfg.scene.num_envs = 1 |
| env_cfg.scene.env_spacing = 2.0 |
| delay_max = max(delay, 5) |
| _effort_lim = { |
| "shoulder_pan_joint": 150.0, |
| "shoulder_lift_joint": 150.0, |
| "elbow_joint": 150.0, |
| "wrist_1_joint": 28.0, |
| "wrist_2_joint": 28.0, |
| "wrist_3_joint": 28.0, |
| } |
| _vel_lim = { |
| "shoulder_pan_joint": 1.5708, |
| "shoulder_lift_joint": 1.5708, |
| "elbow_joint": 1.5708, |
| "wrist_1_joint": 3.1415, |
| "wrist_2_joint": 3.1415, |
| "wrist_3_joint": 3.1415, |
| } |
| env_cfg.scene.robot.actuators["arm"] = DelayedPDActuatorCfg( |
| joint_names_expr=["shoulder.*", "elbow.*", "wrist.*"], |
| stiffness=0.0, |
| damping=0.0, |
| effort_limit=_effort_lim, |
| velocity_limit=_vel_lim, |
| min_delay=0, |
| max_delay=delay_max, |
| ) |
| env = gym.make("OmniReset-Ur5eRobotiq2f85-Sysid-v0", cfg=env_cfg) |
| env.reset() |
|
|
| unwrapped = env.unwrapped |
| robot: Articulation = unwrapped.scene["robot"] |
| device = unwrapped.device |
| arm_joint_ids = robot.find_joints(ARM_JOINT_NAMES)[0] |
| ee_frame_idx = robot.find_bodies(EE_BODY_NAME)[0][0] |
| num_joints = robot.num_joints |
| sim_dt = env_cfg.sim.dt |
|
|
| |
| print(f"\nApplying best params (delay={delay})...") |
| apply_params(robot, best_params, arm_joint_ids, num_joints, device) |
|
|
| |
| print(f"\nRunning closed-loop replay ({T_steps} steps)...") |
| result = closed_loop_replay( |
| env, |
| wp_step_indices, |
| wp_target_pos, |
| wp_target_quat, |
| initial_joint_pos_dev, |
| arm_joint_ids, |
| ee_frame_idx, |
| sim_dt, |
| T_steps, |
| headless=args_cli.headless, |
| ) |
|
|
| sim_joints = result["joint_positions"] |
| real_joints = real_joint_pos_np |
|
|
| |
| error_deg = np.degrees(sim_joints - real_joints) |
| print(f"\n{'='*60}") |
| print("Per-joint RMSE (deg)") |
| print("=" * 60) |
| for j in range(NUM_ARM_JOINTS): |
| rmse_j = np.sqrt(np.mean(error_deg[:, j] ** 2)) |
| print(f" {JOINT_NAMES_SHORT[j]:<16s}: {rmse_j:.4f}") |
|
|
| rmse_total = np.sqrt(np.mean(error_deg**2)) |
| mae_total = np.mean(np.abs(error_deg)) |
| max_total = np.max(np.abs(error_deg)) |
| print(f" TOTAL : RMSE={rmse_total:.4f} MAE={mae_total:.4f} Max={max_total:.4f}") |
|
|
| |
| error_rad = sim_joints - real_joints |
| sysid_score = np.mean(np.sum(error_rad**2, axis=1)) |
| sysid_rmse_deg = np.degrees(np.sqrt(sysid_score)) |
| print(f"\n Sysid-equivalent metric: score={sysid_score:.6f} RMSE={sysid_rmse_deg:.4f}°") |
| print(f" Checkpoint metric: score={best_score:.6f} RMSE={np.degrees(np.sqrt(best_score)):.4f}°") |
| print("=" * 60) |
|
|
| |
| out_dir = os.path.dirname(args.checkpoint) if os.path.dirname(args.checkpoint) else "." |
| plot_overlay(real_joints, sim_joints, dt, save_path=os.path.join(out_dir, "sysid_fit.png")) |
| plot_error(real_joints, sim_joints, dt, save_path=os.path.join(out_dir, "sysid_fit_error.png")) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
| simulation_app.close() |
|
|