375 lines
13 KiB
Python
375 lines
13 KiB
Python
|
|
# -*- 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()
|