exoskeleton/code/test/test_haptic_render.py

310 lines
9.9 KiB
Python
Raw Normal View History

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