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()