Files
mujoco_linkerbot/tools/plot_real_sim_compare.py
T
2026-07-23 17:55:52 +08:00

159 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""真机 vs 仿真 q/v/力矩(或电流) 对比图。
时间原点 = 首次收到「握紧」控制指令 (cmd_u8: 255→0) 的时刻 1 s。
用法:
python3 tools/plot_real_sim_compare.py \\
reports/O6_middle_ros/ros_real_middle_left_20260716_172221.csv \\
reports/O6_middle_ros/ros_sim_middle_left_20260716_114515.csv
"""
from __future__ import annotations
import argparse
from pathlib import Path
import numpy as np
def load_csv(path: Path):
hdr = path.read_text().splitlines()[0].strip().split(",")
data = np.loadtxt(path, delimiter=",", skiprows=1)
col = {name: i for i, name in enumerate(hdr)}
return hdr, col, data
def first_close_cmd_time(t: np.ndarray, cmd_u8: np.ndarray) -> float:
"""首次 cmd 从 255 阶跃到 0(握紧)的时刻。"""
diff = np.diff(cmd_u8)
idx = np.where(diff < -100)[0]
if len(idx) == 0:
raise ValueError("未检测到 cmd 255→0 阶跃")
return float(t[idx[0] + 1])
def align_series(t, *arrays, t_cmd: float, t_pre: float = 1.0):
t0 = t_cmd - t_pre
t_rel = t - t0
return (t_rel, *arrays)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("real_csv", type=Path)
parser.add_argument("sim_csv", type=Path)
parser.add_argument("-o", "--out", type=Path, default=None)
parser.add_argument("--t-pre", type=float, default=1.0)
parser.add_argument("--t-max", type=float, default=8.0, help="相对原点后的最大时间(s)")
args = parser.parse_args()
_, cr, dr = load_csv(args.real_csv)
_, cs, ds = load_csv(args.sim_csv)
t_r, cmd_r = dr[:, cr["t_s"]], dr[:, cr["cmd_u8"]]
t_s, cmd_s = ds[:, cs["t_s"]], ds[:, cs["cmd_u8"]]
t_cmd_r = first_close_cmd_time(t_r, cmd_r)
t_cmd_s = first_close_cmd_time(t_s, cmd_s)
q_r = dr[:, cr["q_rad"]]
cmd_rad_r = dr[:, cr["cmd_rad"]]
v_r = dr[:, cr["v_rad_s"]]
q_s = ds[:, cs["q_rad"]]
cmd_rad_s = ds[:, cs["cmd_rad"]]
v_s = ds[:, cs["v_rad_s"]]
cur_r = dr[:, cr["current"]] if "current" in cr else np.full_like(q_r, np.nan)
tau_s = ds[:, cs["tau_Nm"]] if "tau_Nm" in cs else np.full_like(q_s, np.nan)
tr, qr, cmdr, vr, cur_r = align_series(
t_r, q_r, cmd_rad_r, v_r, cur_r, t_cmd=t_cmd_r, t_pre=args.t_pre
)
ts, qs, cmds, vs, tau_s = align_series(
t_s, q_s, cmd_rad_s, v_s, tau_s, t_cmd=t_cmd_s, t_pre=args.t_pre
)
mask_r = (tr >= -0.05) & (tr <= args.t_max)
mask_s = (ts >= -0.05) & (ts <= args.t_max)
out = args.out
if out is None:
out = args.real_csv.parent / "ros_real_vs_sim_middle_left_compare_qvt.png"
import os
root = Path(__file__).resolve().parents[1]
os.environ.setdefault("MPLCONFIGDIR", str(root / ".mplconfig"))
Path(os.environ["MPLCONFIGDIR"]).mkdir(parents=True, exist_ok=True)
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, axes = plt.subplots(3, 1, figsize=(11, 9), sharex=True)
fig.suptitle(
"O6 middle_mcp_pitch real vs sim\n"
f"t=0 = {args.t_pre:.0f}s before first close command (cmd_u8: 255->0)",
fontsize=12,
)
# --- angle ---
ax = axes[0]
# 控制指令:真机/仿真时序一致,只画一条黑色虚线
cmd_t = ts[mask_s] if np.any(mask_s) else tr[mask_r]
cmd_y = np.rad2deg(cmds[mask_s]) if np.any(mask_s) else np.rad2deg(cmdr[mask_r])
ax.plot(cmd_t, cmd_y, color="black", ls="--", lw=1.2, label="cmd")
ax.plot(ts[mask_s], np.rad2deg(qs[mask_s]), color="#1f77b4", lw=1.8, label="sim q")
ax.plot(tr[mask_r], np.rad2deg(qr[mask_r]), color="#ff7f0e", lw=1.8, label="real q")
ax.axvline(1.0, color="gray", ls=":", lw=1.0, alpha=0.8)
ax.set_ylabel("angle (deg)")
ax.legend(loc="best", fontsize=9)
ax.grid(True, alpha=0.3)
# --- velocity ---
ax = axes[1]
ax.plot(ts[mask_s], np.rad2deg(vs[mask_s]), color="#1f77b4", lw=1.8, label="sim v")
ax.plot(tr[mask_r], np.rad2deg(vr[mask_r]), color="#ff7f0e", lw=1.8, label="real v")
ax.axvline(1.0, color="gray", ls=":", lw=1.0, alpha=0.8)
ax.set_ylabel("velocity (deg/s)")
ax.legend(loc="best")
ax.grid(True, alpha=0.3)
# --- effort: sim tau vs real current ---
ax = axes[2]
ax.plot(ts[mask_s], tau_s[mask_s], color="#1f77b4", lw=1.8, label="sim τ (N·m)")
if np.any(np.isfinite(cur_r[mask_r])):
ax.plot(tr[mask_r], cur_r[mask_r], color="#ff7f0e", lw=1.8, label="real current")
ax.set_ylabel("τ (N·m) / current")
else:
ax.plot([], [], color="#ff7f0e", label="real current (no data)")
ax.set_ylabel("torque (N·m)")
ax.text(
0.5,
0.5,
"real current not recorded",
transform=ax.transAxes,
ha="center",
va="center",
fontsize=10,
color="#666",
)
ax.axvline(1.0, color="gray", ls=":", lw=1.0, alpha=0.8)
ax.set_xlabel("time (s) [t=0: 1s before first close cmd]")
ax.legend(loc="best")
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig(out, dpi=150)
plt.close(fig)
print(f"plot -> {out}")
print(f"real cmd↓ at abs t={t_cmd_r:.3f}s -> origin t0={t_cmd_r - args.t_pre:.3f}s")
print(f"sim cmd↓ at abs t={t_cmd_s:.3f}s -> origin t0={t_cmd_s - args.t_pre:.3f}s")
if __name__ == "__main__":
main()