exoskeleton/code/test/test_haptic_render.py

310 lines
9.9 KiB
Python
Raw Permalink 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.

# -*- coding: utf-8 -*-
"""
测试脚本:验证 InteractionEstimator + HapticRenderer 的正确性,
并绘制论文中使用的误差和能量罐曲线。
运行:
python test_haptic_render.py
依赖:
- pinocchio
- numpy
- matplotlib
- omegaconf
"""
import numpy as np
import pinocchio as pin
from omegaconf import OmegaConf
import matplotlib.pyplot as plt
import os, sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from core.interaction_estimater import InteractionEstimator
from core.haptic_render import HapticRenderer, TankParams
def moving_average(x, window: int = 7):
"""简单滑动平均,用于平滑 alpha 等曲线。"""
if window <= 1:
return x
kernel = np.ones(window, dtype=float) / float(window)
# 使用 same 保持长度一致
return np.convolve(x, kernel, mode="same")
def build_model_with_frames():
"""
备用:构造一个 6-DoF 示例机械臂,并添加:
- chest_link: 挂在关节1
- ee_link: 挂在末端关节
目前脚本直接从 URDF 加载,不一定用得到。
"""
model = pin.buildSampleModelManipulator()
chest_joint_id = 1
chest_frame_name = "chest_link"
model.addFrame(pin.Frame(
chest_frame_name,
chest_joint_id,
chest_joint_id,
pin.SE3.Identity(),
pin.FrameType.OP_FRAME
))
ee_joint_id = model.njoints - 1
ee_frame_name = "ee_link"
model.addFrame(pin.Frame(
ee_frame_name,
ee_joint_id,
ee_joint_id,
pin.SE3.Identity(),
pin.FrameType.OP_FRAME
))
return model, chest_frame_name, ee_frame_name
def main():
conf = OmegaConf.load("./config/config.yaml")
np.random.seed(0)
# 1) 构造主/从模型(这里用真实 URDF
master_model, _, _ = pin.buildModelsFromUrdf(str(conf.master_urdf))
slave_model, _, _ = pin.buildModelsFromUrdf(str(conf.slave_urdf))
# 从端交互估计器
est_slave = InteractionEstimator(
slave_model,
chest_frame_name=conf.s_base_frame,
ee_frame_name=conf.s_ee_frame,
lambda_damp=conf.interaction_est.lambda_damp,
)
# 主端渲染器
renderer = HapticRenderer(
master_model,
chest_frame_name=conf.m_base_frame,
ee_frame_name=conf.m_ee_frame,
feedback_strength=conf.haptic_render.feedback_strength,
E_init=conf.haptic_render.E_init,
E_max=conf.haptic_render.E_max,
alpha_floor=conf.haptic_render.alpha_floor,
alpha_ceil=conf.haptic_render.alpha_ceil,
E0=conf.haptic_render.E0,
)
nq_s, nv_s = slave_model.nq, slave_model.nv
nq_m, nv_m = master_model.nq, master_model.nv
dt = 0.002
n_steps = 500 # 足够画出平滑曲线
print("========== TEST START ==========")
print(f"slave nq={nq_s}, master nq={nq_m}, dt={dt}s, steps={n_steps}")
print("--------------------------------")
# -------- 统计量 / 曲线数据 --------
times = []
err_tau_int_hist = []
err_CF_hist = []
err_tau_fb_hist = []
E_hist = []
alpha_hist = []
max_err_tau_int = 0.0
max_err_CF = 0.0
max_err_tau_fb = 0.0
for k in range(n_steps):
t = k * dt
times.append(t)
# 2) 随机生成从端状态(可改成真实记录或特定轨迹)
q_s = 0.2 * (np.random.rand(nq_s) - 0.5) # [-0.1,0.1]
qd_s = 0.1 * (np.random.rand(nv_s) - 0.5)
qdd_s = np.zeros_like(qd_s)
# 从端模型扭矩M qdd + C qd + g
tau_model_s = est_slave._tau_model(q_s, qd_s, qdd_s, tau_ff_fric=None)
# 从端胸腔雅可比 C J_s
CJ_s = est_slave._chest_jacobian(q_s, qd_s) # 6 x nv_s
# 3) 人为构造“真实”交互扳手 C F_true
# 也可以改成随时间变化的模式,例如正弦。
CF_true = np.array([10.0, 0.0, 0.0, # 10 N 沿 Cx
0.0, 0.0, 0.0])
# 真 · 交互关节力矩
tau_int_true = CJ_s.T @ CF_true
# 构造测量力矩tau_meas = tau_model + tau_int_true
tau_meas_s = tau_model_s + tau_int_true
# 4) 调用 InteractionEstimator 估计
tau_int_est, CF_int_est, CJ_s_est = est_slave.estimate(
q_s, qd_s, qdd_s,
tau_meas=tau_meas_s,
tau_ff_fric=None
)
# 从端末端在 C 系的速度扭量(用于能量罐功率)
V_slave_C = CJ_s @ qd_s
# 5) 主端状态(此处用从端状态代替,仅做算法验证)
q_m = q_s.copy()
qd_m = qd_s.copy()
tau_fb_m_est, alpha = renderer.render_from_CF(
q_m, qd_m,
CF_int_slave_C=CF_int_est,
V_slave_C=V_slave_C,
dt=dt
)
# 主端胸腔雅可比,用于构造“真 · 反馈扭矩”(不经过能量罐)
CJ_m = renderer.CJ_master.chest_jacobian(q_m, qd_m)
tau_fb_m_true = CJ_m.T @ CF_true
# -------- 误差计算 --------
err_tau_int = np.linalg.norm(tau_int_est - tau_int_true)
err_CF = np.linalg.norm(CF_int_est - CF_true)
err_tau_fb = np.linalg.norm(tau_fb_m_est - tau_fb_m_true)
err_tau_int_hist.append(err_tau_int)
err_CF_hist.append(err_CF)
err_tau_fb_hist.append(err_tau_fb)
max_err_tau_int = max(max_err_tau_int, err_tau_int)
max_err_CF = max(max_err_CF, err_CF)
max_err_tau_fb = max(max_err_tau_fb, err_tau_fb)
# 能量罐状态
E_hist.append(renderer.tank.E)
alpha_hist.append(alpha)
# ---- 每隔若干步打印一次 ----
if k % 100 == 0:
print(f"\n--- Step {k} (t = {t:.3f} s) ---")
print(f"‣ ||tau_int_true|| = {np.linalg.norm(tau_int_true):.4e}")
print(f" ||tau_int_est - tau_int_true|| = {err_tau_int:.4e}")
print(f"‣ ||CF_true|| = {np.linalg.norm(CF_true):.4e}")
print(f" ||CF_int_est - CF_true|| = {err_CF:.4e}")
print(f"‣ ||tau_fb_m_true|| = {np.linalg.norm(tau_fb_m_true):.4e}")
print(f" ||tau_fb_m_est - tau_fb_m_true|| = {err_tau_fb:.4e}")
print(f" Energy tank: E = {renderer.tank.E:.4f}, alpha = {alpha:.4f}")
# ---------- 数值结果摘要 ----------
print("\n========== SUMMARY ==========")
print(f"max ||tau_int_est - tau_int_true|| = {max_err_tau_int:.4e}")
print(f"max ||CF_int_est - CF_true|| = {max_err_CF:.4e}")
print(f"max ||tau_fb_est - tau_fb_true|| = {max_err_tau_fb:.4e}")
tol_tau_int = 1e-3
tol_CF = 1e-3
tol_tau_fb = 1e-3
if max_err_tau_int < tol_tau_int and max_err_CF < tol_CF and max_err_tau_fb < tol_tau_fb:
print("[PASS] 所有误差均在容许范围内。")
else:
print("[WARN] 误差超出阈值,请检查算法或考虑调小阻尼 lambda_damp。")
# =====================================================
# 绘图部分(适合论文呈现)
# =====================================================
times = np.array(times)
err_tau_int_hist = np.array(err_tau_int_hist)
err_CF_hist = np.array(err_CF_hist)
err_tau_fb_hist = np.array(err_tau_fb_hist)
E_hist = np.array(E_hist)
alpha_hist = np.array(alpha_hist)
# 对 alpha 做轻微平滑(论文图更清晰)
alpha_smooth = moving_average(alpha_hist, window=7)
# ------------------ 误差子图3 个独立子图) ------------------
fig_err, (ax1, ax2, ax3) = plt.subplots(
3, 1, sharex=True, figsize=(6, 7)
)
# (a) Interaction torque estimation error
ax1.plot(
times,
err_tau_int_hist,
color="C0",
label=r"$\|\tau_{\mathrm{int}}^{\mathrm{est}}-\tau_{\mathrm{int}}^{\mathrm{true}}\|$",
)
ax1.set_title("(a) Interaction torque estimation error")
ax1.set_ylabel(r"Error norm $\|\cdot\|$")
ax1.set_yscale("log")
ax1.grid(True, linestyle="--", linewidth=0.5)
ax1.legend(loc="lower right")
# (b) Interaction wrench estimation error
ax2.plot(
times,
err_CF_hist,
color="C1",
label=r"$\|{}^{C}F_{\mathrm{int}}^{\mathrm{est}}-{}^{C}F_{\mathrm{true}}\|$",
)
ax2.set_title("(b) Interaction wrench estimation error")
ax2.set_ylabel(r"Error norm $\|\cdot\|$")
ax2.set_yscale("log")
ax2.grid(True, linestyle="--", linewidth=0.5)
ax2.legend(loc="lower right")
# (c) Feedback torque rendering error
ax3.plot(
times,
err_tau_fb_hist,
color="C2",
label=r"$\|\tau_{\mathrm{fb}}^{\mathrm{est}}-\tau_{\mathrm{fb}}^{\mathrm{true}}\|$",
)
ax3.set_title("(c) Haptic feedback torque error")
ax3.set_xlabel(r"Time $t$ [s]")
ax3.set_ylabel(r"Error norm $\|\cdot\|$")
ax3.set_yscale("log")
ax3.grid(True, linestyle="--", linewidth=0.5)
ax3.legend(loc="lower right")
fig_err.tight_layout()
# fig_err.savefig("figs/haptic_errors_3subplots.png", dpi=300, bbox_inches="tight")
# ------------------ 能量罐 + 缩放系数 ------------------
fig_tank, ax1_t = plt.subplots(figsize=(6, 4))
ax1_t.set_title("Energy tank dynamics and scaling factor")
l1 = ax1_t.plot(
times,
E_hist,
color="C0",
label=r"Tank energy $E[k]$",
)
ax1_t.set_xlabel(r"Time $t$ [s]")
ax1_t.set_ylabel(r"Energy $E$ [J]")
ax1_t.grid(True, linestyle="--", linewidth=0.5)
ax2_t = ax1_t.twinx()
l2 = ax2_t.plot(
times,
alpha_smooth,
color="C1",
linestyle="--",
label=r"Scaling $\alpha[k]$ (smoothed)",
)
ax2_t.set_ylabel(r"Scaling factor $\alpha$")
# Legend 合并并右下角防遮挡
lines = l1 + l2
labels = [l.get_label() for l in lines]
ax1_t.legend(lines, labels, loc="lower right")
fig_tank.tight_layout()
# fig_tank.savefig("figs/haptic_tank_smooth.png", dpi=300, bbox_inches="tight")
plt.show()
if __name__ == "__main__":
main()