Files
RC_WheelLeg/05_software/real/sim2real/tools/math_utils.py
T

109 lines
4.0 KiB
Python

"""数学工具 — 与 rc_mjlab/sim2sim/tools/math_utils.py 数值完全一致。"""
import numpy as np
def get_gravity_orientation(quat_wxyz: np.ndarray) -> np.ndarray:
qw, qx, qy, qz = quat_wxyz
gx = 2.0 * (-qz * qx + qw * qy)
gy = -2.0 * (qz * qy + qw * qx)
gz = 1.0 - 2.0 * (qw * qw + qz * qz)
return np.array([gx, gy, gz], dtype=np.float32)
def quat_rotate_inverse(quat_wxyz: np.ndarray, v: np.ndarray) -> np.ndarray:
q_w = quat_wxyz[0]
q_vec = quat_wxyz[1:]
a = v * (2.0 * q_w * q_w - 1.0)
b = np.cross(q_vec, v) * q_w * 2.0
c = q_vec * np.dot(q_vec, v) * 2.0
return a - b + c
def quat_from_accel(accel: np.ndarray) -> np.ndarray:
"""用静止重力方向初始化机身姿态四元数。
思想:仿真启动时 quat = [1,0,0,0] 隐含"机身完全水平",但真机摆在地面上
pitch/roll 通常各自有几度偏差,会让 projected_gravity 一开始就错。
用加速度计读数与 [0,0,-1] 的最短旋转作为初值,可以把首步重力误差
降到 IMU 噪声级。
"""
g_meas = accel / (np.linalg.norm(accel) + 1e-9)
g_ref = np.array([0.0, 0.0, 1.0], dtype=np.float32)
cross = np.cross(g_ref, g_meas)
dot = float(np.dot(g_ref, g_meas))
if dot < -0.999999:
return np.array([0.0, 1.0, 0.0, 0.0], dtype=np.float32)
s = float(np.sqrt((1.0 + dot) * 2.0))
q = np.array([s * 0.5, cross[0] / s, cross[1] / s, cross[2] / s], dtype=np.float32)
return q / (np.linalg.norm(q) + 1e-9)
class LowPassFilter:
"""一阶 IIR 低通,alpha 公式与训练侧 rc_mjlab/src/robot/mdp/lowpass_actions.py
`_lowpass_weights` 完全一致:
alpha = 1 - exp(-2π · cutoff_freq / control_freq)
= 1 - exp(-2π · cutoff_freq · dt)
注意:这与 rc_mjlab/sim2sim/interface/mujoco_io.py 用的近似公式
(dt / (dt + 1/(2π·fc))) 数值上不同,在 15Hz 截止时差约 30%。
我们以训练侧为准,因为策略是在那个滤波下学的。
"""
def __init__(self, cutoff_freq: float, dt: float, dim: int):
self.alpha = float(1.0 - np.exp(-2.0 * np.pi * cutoff_freq * dt))
self.y_prev = None
def filter(self, x: np.ndarray) -> np.ndarray:
if self.y_prev is None:
self.y_prev = x.copy()
y = self.alpha * x + (1.0 - self.alpha) * self.y_prev
self.y_prev = y.copy()
return y
def reset(self):
self.y_prev = None
class MahonyFilter:
"""互补滤波器:高频用陀螺仪积分,低频用加速度计修正。"""
def __init__(self, kp: float = 2.0, ki: float = 0.0, dt: float = 0.02):
self.kp = kp
self.ki = ki
self.dt = dt
self.q = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float32)
self.e_int = np.zeros(3, dtype=np.float32)
def reset_with_accel(self, accel: np.ndarray):
self.q = quat_from_accel(accel)
self.e_int.fill(0.0)
def update(self, accel: np.ndarray, gyro: np.ndarray) -> np.ndarray:
norm_a = float(np.linalg.norm(accel))
if norm_a > 1e-6:
a = accel / norm_a
q = self.q
v = np.array([
2.0 * (q[1] * q[3] - q[0] * q[2]),
2.0 * (q[0] * q[1] + q[2] * q[3]),
q[0] * q[0] - q[1] * q[1] - q[2] * q[2] + q[3] * q[3],
], dtype=np.float32)
e = np.cross(a, v)
if self.ki > 0.0:
self.e_int += e * self.dt
else:
self.e_int.fill(0.0)
gyro = gyro + self.kp * e + self.ki * self.e_int
q = self.q
q_dot = 0.5 * np.array([
-q[1] * gyro[0] - q[2] * gyro[1] - q[3] * gyro[2],
q[0] * gyro[0] + q[2] * gyro[2] - q[3] * gyro[1],
q[0] * gyro[1] - q[1] * gyro[2] + q[3] * gyro[0],
q[0] * gyro[2] + q[1] * gyro[1] - q[2] * gyro[0],
], dtype=np.float32)
self.q += q_dot * self.dt
self.q /= (np.linalg.norm(self.q) + 1e-9)
return self.q