1647241649
Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
5.3 KiB
Python
159 lines
5.3 KiB
Python
#!/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()
|