File size: 13,589 Bytes
0037d53 | 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 | # Copyright (c) 2024-2026, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md).
# All Rights Reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""
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 # noqa: F401 # register gym envs
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
# ============================================================================
# Parameter application (same as sysid_ur5e_osc.py)
# ============================================================================
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)
# ============================================================================
# Closed-loop replay
# ============================================================================
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),
}
# ============================================================================
# Plotting
# ============================================================================
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)
# ============================================================================
# Main
# ============================================================================
def main():
args = args_cli
device_str = args.device
# Load checkpoint
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')}")
# Print best params
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")
# Load real data
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")
# Move to GPU
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()
# Create env (same as sysid_ur5e_osc.py)
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
# Apply best params
print(f"\nApplying best params (delay={delay})...")
apply_params(robot, best_params, arm_joint_ids, num_joints, device)
# Run closed-loop replay
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
# Compute per-joint RMSE
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}")
# Sysid-equivalent score for comparison with checkpoint
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)
# Plot
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()
|