exoskeleton/code/test/test_int_estimater.py

375 lines
13 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
import os, sys, numpy as np
import pinocchio as pin
from omegaconf import OmegaConf
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from core.interaction_estimater import InteractionEstimator
# —— 误差函数(更鲁棒的相对误差 + 绝对误差)——
def rel_err(a, b, eps=1e-9):
"""对称式相对误差‖a-b‖ / max(0.5(‖a‖+‖b‖), eps)"""
na, nb = np.linalg.norm(a), np.linalg.norm(b)
denom = max(0.5 * (na + nb), eps)
return np.linalg.norm(a - b) / denom
def abs_err(a, b):
"""绝对误差‖a-b‖"""
return np.linalg.norm(a - b)
# —— 固定范数的扳手采样(方向随机)——
def sample_wrench_fixed_norm(F_lin_norm=10.0, F_ang_norm=2.0, rng=None):
"""
在线性部分和角部分分别固定范数方向随机的 6D 扳手采样
"""
rng = np.random.default_rng() if rng is None else rng
v = rng.standard_normal(6)
f = v[:3]
m = v[3:]
f = f / (np.linalg.norm(f) + 1e-12) * F_lin_norm
m = m / (np.linalg.norm(m) + 1e-12) * F_ang_norm
return np.hstack([f, m])
def load_model(urdf_path: str) -> pin.Model:
assert os.path.exists(urdf_path), f"URDF not found: {urdf_path}"
model = pin.buildModelFromUrdf(urdf_path)
print(f"[URDF] model loaded: nq={model.nq}, nv={model.nv}, njoints={model.njoints}")
return model
def oracle_test(model,
chest_frame: str = 'slave_shoulder',
ee_frame: str = 'slave_ee',
n_trials: int = 300,
noise_tau_std: float = 0.0,
lambda_damp: float = 1e-6,
seed: int = 11,
sv_min_skip: float = 1e-6):
"""
使用 InteractionEstimator 理想/带噪声仿真
- 通过动力学模型生成 tau_model
- 叠加胸腔系扳手 CJ^T F_true 和测量噪声得到 tau_meas
- 调用 est.estimate 恢复 tau_int, F_est
- 统计相对/绝对误差的均值方差p90
"""
rng = np.random.default_rng(seed)
est = InteractionEstimator(model, chest_frame, ee_frame, lambda_damp=lambda_damp)
data = model.createData()
errs_rel, errs_abs = [], []
kept = 0
for k in range(n_trials):
# 随机状态
q = pin.randomConfiguration(model)
qd = np.random.randn(model.nv) * 0.1
qdd = np.random.randn(model.nv) * 0.2
# 动力学力矩 tau_model
M = pin.crba(model, data, q)
M = (M + M.T) - np.diag(M.diagonal())
nle = pin.nonLinearEffects(model, data, q, qd)
g = pin.computeGeneralizedGravity(model, data, q)
Cqd = nle - g
tau_model = M @ qdd + Cqd + g
# 胸腔系雅可比 CJ
CJ = est._chest_jacobian(q, qd) # 6 x nv
svals = np.linalg.svd(CJ, compute_uv=False)
if svals[-1] < sv_min_skip:
# 跳过奇异邻域样本
continue
# 固定范数扳手
CF_true = sample_wrench_fixed_norm(10.0, 2.0, rng=rng)
# 合成“测得力矩” tau_meas = tau_model + CJ^T F_true + noise
tau_meas = tau_model + CJ.T @ CF_true + np.random.randn(model.nv) * noise_tau_std
# 利用 estimator 做估计
tau_int, CF_est, CJ_check = est.estimate(q, qd, qdd, tau_meas)
# 误差统计
errs_rel.append(rel_err(CF_est, CF_true))
errs_abs.append(abs_err(CF_est, CF_true))
kept += 1
# 一致性:回投误差 & 功率一致性(抽样打印)
if k % 50 == 0:
back_err = rel_err(CJ_check.T @ CF_est, tau_int)
v_C = CJ @ qd
Pq = float(tau_int @ qd)
Pv = float(CF_true @ v_C)
print(
f"[{k:03d}] rel={errs_rel[-1]:.3e}, abs={errs_abs[-1]:.3e}, "
f"back={back_err:.3e}, power={abs(Pq - Pv):.3e}, sv_min={svals[-1]:.2e}"
)
errs_rel = np.array(errs_rel)
errs_abs = np.array(errs_abs)
if kept == 0:
print("[Oracle] all samples skipped by sv_min filter; try smaller sv_min_skip.")
# 返回空 summary
return dict(
errs_rel=errs_rel,
errs_abs=errs_abs,
kept=0,
mean_rel=np.nan,
std_rel=np.nan,
p90_rel=np.nan,
mean_abs=np.nan,
std_abs=np.nan,
p90_abs=np.nan,
)
mean_rel = float(errs_rel.mean())
std_rel = float(errs_rel.std())
p90_rel = float(np.percentile(errs_rel, 90))
mean_abs = float(errs_abs.mean())
std_abs = float(errs_abs.std())
p90_abs = float(np.percentile(errs_abs, 90))
print(
f"[Oracle REL] mean={mean_rel:.3e}, std={std_rel:.3e}, "
f"median={np.median(errs_rel):.3e}, p90={p90_rel:.3e}"
)
print(
f"[Oracle ABS] mean={mean_abs:.3e}, std={std_abs:.3e}, "
f"median={np.median(errs_abs):.3e}, p90={p90_abs:.3e}"
)
summary = dict(
errs_rel=errs_rel,
errs_abs=errs_abs,
kept=kept,
mean_rel=mean_rel,
std_rel=std_rel,
p90_rel=p90_rel,
mean_abs=mean_abs,
std_abs=std_abs,
p90_abs=p90_abs,
)
return summary
def oracle_test_baseline(model,
chest_frame: str = 'slave_shoulder',
ee_frame: str = 'slave_ee',
n_trials: int = 300,
noise_tau_std: float = 0.05,
seed: int = 17,
sv_min_skip: float = 1e-6):
"""
基线不调用 InteractionEstimator 的残差/阻尼公式
直接用未阻尼伪逆解胸腔扳手
tau_int_true = tau_meas - tau_model
F_est = (CJ CJ^T)^{-1} CJ tau_int_true
用来对比在同样噪声/配置下的数值稳定性
"""
rng = np.random.default_rng(seed)
est_tmp = InteractionEstimator(model, chest_frame, ee_frame, lambda_damp=0.0)
data = model.createData()
errs_rel, errs_abs = [], []
kept = 0
for k in range(n_trials):
# 随机状态
q = pin.randomConfiguration(model)
qd = np.random.randn(model.nv) * 0.1
qdd = np.random.randn(model.nv) * 0.2
# 动力学力矩 tau_model
M = pin.crba(model, data, q)
M = (M + M.T) - np.diag(M.diagonal())
nle = pin.nonLinearEffects(model, data, q, qd)
g = pin.computeGeneralizedGravity(model, data, q)
Cqd = nle - g
tau_model = M @ qdd + Cqd + g
# 胸腔雅可比 CJ
CJ = est_tmp._chest_jacobian(q, qd)
svals = np.linalg.svd(CJ, compute_uv=False)
if svals[-1] < sv_min_skip:
continue
# 固定范数扳手(与 oracle_test 相同范数)
CF_true = sample_wrench_fixed_norm(10.0, 2.0, rng=rng)
# 测得力矩(含噪声)
tau_meas = tau_model + CJ.T @ CF_true + np.random.randn(model.nv) * noise_tau_std
# “真实”关节残差力矩(在仿真中我们知道 tau_model
tau_int_true = tau_meas - tau_model
# 未阻尼伪逆 (CJ CJ^T)^{-1} CJ tau_int_true
JJt = CJ @ CJ.T
try:
F_est = np.linalg.solve(JJt, CJ @ tau_int_true)
except np.linalg.LinAlgError:
# 数值奇异,跳过该样本
continue
errs_rel.append(rel_err(F_est, CF_true))
errs_abs.append(abs_err(F_est, CF_true))
kept += 1
if k % 50 == 0:
back_err = rel_err(CJ.T @ F_est, tau_int_true)
print(
f"[BASE {k:03d}] rel={errs_rel[-1]:.3e}, abs={errs_abs[-1]:.3e}, "
f"back={back_err:.3e}, sv_min={svals[-1]:.2e}"
)
errs_rel = np.array(errs_rel)
errs_abs = np.array(errs_abs)
if kept == 0:
print("[Baseline] all samples skipped by sv_min filter; try smaller sv_min_skip.")
return dict(
errs_rel=errs_rel,
errs_abs=errs_abs,
kept=0,
mean_rel=np.nan,
std_rel=np.nan,
p90_rel=np.nan,
mean_abs=np.nan,
std_abs=np.nan,
p90_abs=np.nan,
)
mean_rel = float(errs_rel.mean())
std_rel = float(errs_rel.std())
p90_rel = float(np.percentile(errs_rel, 90))
mean_abs = float(errs_abs.mean())
std_abs = float(errs_abs.std())
p90_abs = float(np.percentile(errs_abs, 90))
print(
f"[BASELINE REL] mean={mean_rel:.3e}, std={std_rel:.3e}, "
f"median={np.median(errs_rel):.3e}, p90={p90_rel:.3e}"
)
print(
f"[BASELINE ABS] mean={mean_abs:.3e}, std={std_abs:.3e}, "
f"median={np.median(errs_abs):.3e}, p90={p90_abs:.3e}"
)
summary = dict(
errs_rel=errs_rel,
errs_abs=errs_abs,
kept=kept,
mean_rel=mean_rel,
std_rel=std_rel,
p90_rel=p90_rel,
mean_abs=mean_abs,
std_abs=std_abs,
p90_abs=p90_abs,
)
return summary
def consistency_sweep(model,
chest_frame: str = 'slave_shoulder',
ee_frame: str = 'slave_ee'):
"""
对不同阻尼系数 lambda 进行一个小 sweep查看
- 估计扳手范数
- 回投误差
- cond(JJ^T)
"""
est_ref = InteractionEstimator(model, chest_frame, ee_frame, lambda_damp=1e-3)
q = pin.randomConfiguration(model)
qd = np.random.randn(model.nv) * 0.05
qdd = np.zeros(model.nv)
CJ = est_ref._chest_jacobian(q, qd)
CF_true = np.array([5.0, -3.0, 8.0, 0.5, 0.2, -0.1])
M = pin.crba(model, est_ref.data, q)
M = (M + M.T) - np.diag(M.diagonal())
nle = pin.nonLinearEffects(model, est_ref.data, q, qd)
g = pin.computeGeneralizedGravity(model, est_ref.data, q)
Cqd = nle - g
tau_model = M @ qdd + Cqd + g
tau_meas = tau_model + CJ.T @ CF_true
lambdas = [1e-6, 3e-6, 1e-5, 3e-5, 1e-4, 3e-4, 1e-3, 3e-3, 1e-2]
print("\n[Lambda sweep] λ, ‖F_est‖, back_err, cond(JJᵀ)")
for lam in lambdas:
est = InteractionEstimator(model, chest_frame, ee_frame, lambda_damp=lam)
tau_int, CF_est, CJ_use = est.estimate(q, qd, qdd, tau_meas)
back_err = rel_err(CJ_use.T @ CF_est, tau_int)
JJt = CJ_use @ CJ_use.T
cond = np.linalg.cond(JJt) if np.linalg.matrix_rank(JJt) == 6 else np.inf
print(f"{lam:8.1e} {np.linalg.norm(CF_est):8.3f} {back_err:8.2e} {cond:8.2e}")
def main():
conf = OmegaConf.load("./config/config.yaml")
urdf_path = getattr(conf, "slave_urdf", None)
model = load_model(urdf_path)
# 注意chest_frame 在论文中记为 C 框架,这里保持一致
chest_frame = "slave_base" # 或 "slave_shoulder",视你的 URDF 定义而定
ee_frame = "slave_ee"
print("\n=== ORACLE (no noise, λ=1e-6) ===")
summary_ideal = oracle_test(
model, chest_frame, ee_frame,
n_trials=300, noise_tau_std=0.0, lambda_damp=1e-6,
seed=11, sv_min_skip=1e-6
)
print("\n=== PROPOSED (tau noise 0.05 N·m, λ=1e-3) ===")
summary_proposed = oracle_test(
model, chest_frame, ee_frame,
n_trials=300, noise_tau_std=0.05, lambda_damp=1e-3,
seed=13, sv_min_skip=1e-6
)
print("\n=== BASELINE (tau noise 0.05 N·m, undamped pseudoinverse) ===")
summary_baseline = oracle_test_baseline(
model, chest_frame, ee_frame,
n_trials=300, noise_tau_std=0.05,
seed=17, sv_min_skip=1e-6
)
# 打印一个 markdown 风格的小表格,方便直接贴到论文
def to_percent(x):
return 100.0 * x if x is not None and not np.isnan(x) else float("nan")
print("\n=== SUMMARY (relative wrench error e_F) ===")
print("| Method | mean e_F [%] | std e_F [%] | p90 e_F [%] | Kept |")
print("|------------------------|-------------:|------------:|------------:|-----:|")
print(
f"| Ideal (no noise) | {to_percent(summary_ideal['mean_rel']):11.2f} | "
f"{to_percent(summary_ideal['std_rel']):11.2f} | "
f"{to_percent(summary_ideal['p90_rel']):11.2f} | "
f"{summary_ideal['kept']:4d} |"
)
print(
f"| Proposed (damped) | {to_percent(summary_proposed['mean_rel']):11.2f} | "
f"{to_percent(summary_proposed['std_rel']):11.2f} | "
f"{to_percent(summary_proposed['p90_rel']):11.2f} | "
f"{summary_proposed['kept']:4d} |"
)
print(
f"| Baseline (PI, undamp) | {to_percent(summary_baseline['mean_rel']):11.2f} | "
f"{to_percent(summary_baseline['std_rel']):11.2f} | "
f"{to_percent(summary_baseline['p90_rel']):11.2f} | "
f"{summary_baseline['kept']:4d} |"
)
# 可选:扫 lambda看条件数/回投误差(和论文里 Fig. 6(a)(b) 对应)
consistency_sweep(model, chest_frame, ee_frame)
if __name__ == "__main__":
main()