619 lines
22 KiB
Python
619 lines
22 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Go1 RL Policy 推理 - 使用 ONNX Runtime + MuJoCo 可视化
|
||
训练模型: /opt/IsaacLab/logs/rsl_rl/unitree_go1_flat/2026-03-24_00-25-36/model_1999.pt
|
||
|
||
使用方法:
|
||
python3 /home/8x54zj-m/unitree_mujoco/go1_rl_inference.py
|
||
|
||
运行此脚本需要:
|
||
pip install mujoco onnxruntime numpy
|
||
|
||
键盘控制 (无需 root):
|
||
W/S: 前进/后退
|
||
A/D: 左转/右转
|
||
Q/E: 侧向左移/右移
|
||
空格: 停止
|
||
ESC 或 Ctrl+C: 退出查看器
|
||
"""
|
||
import numpy as np
|
||
import mujoco
|
||
from mujoco import viewer
|
||
import os
|
||
import sys
|
||
import tty
|
||
import termios
|
||
import signal
|
||
import threading
|
||
import time
|
||
|
||
# ============================================================
|
||
# 全局退出标志
|
||
# ============================================================
|
||
g_exit_requested = False
|
||
|
||
def signal_handler(signum, frame):
|
||
"""处理 Ctrl+C (SIGINT) 信号"""
|
||
global g_exit_requested
|
||
g_exit_requested = True
|
||
|
||
# 注册信号处理器
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
|
||
# ============================================================
|
||
# 配置
|
||
# ============================================================
|
||
ONNX_PATH = "/opt/IsaacLab/logs/rsl_rl/unitree_go1_flat/2026-03-24_00-25-36/exported/policy.onnx"
|
||
XML_PATH = "/home/8x54zj-m/unitree_mujoco/data/go1/xml/go1.xml"
|
||
MESH_PATH = "/home/8x54zj-m/unitree_mujoco/data/go1/meshes"
|
||
MLP_PATH = "/opt/MotrixLab/walk-these-ways/resources/actuator_nets/unitree_go1.pt"
|
||
|
||
# 观测/动作维度
|
||
OBS_DIM = 48
|
||
ACTION_DIM = 12
|
||
|
||
# 关节顺序映射 (policy输出 -> MuJoCo关节)
|
||
# MuJoCo 关节顺序: [FR_hip, FR_thigh, FR_calf, FL_hip, FL_thigh, FL_calf, RR_hip, RR_thigh, RR_calf, RL_hip, RL_thigh, RL_calf]
|
||
# Policy 输出顺序取决于 IsaacLab/USD 文件中的定义
|
||
# 腿部排列共有 4! = 24 种可能性
|
||
#
|
||
# 测试组 (腿部顺序映射):
|
||
# 0: FR-FL-RR-RL (MuJoCo默认顺序)
|
||
# 1: FR-FL-RL-RR
|
||
# 2: FR-RR-FL-RL
|
||
# 3: FR-RR-RL-FL
|
||
# 4: FR-RL-FL-RR
|
||
# 5: FR-RL-RR-FL
|
||
# 6: FL-FR-RR-RL
|
||
# 7: FL-FR-RL-RR
|
||
# 8: FL-RR-FR-RL
|
||
# 9: FL-RR-RL-FR
|
||
# 10: FL-RL-FR-RR
|
||
# 11: FL-RL-RR-FR
|
||
# 12: RR-FR-FL-RL
|
||
# 13: RR-FR-RL-FL 。。
|
||
# 14: RR-FL-FR-RL
|
||
# 15: RR-FL-RL-FR
|
||
# 16: RR-RL-FR-FL
|
||
# 17: RR-RL-FL-FR
|
||
# 18: RL-FR-FL-RR 。。
|
||
# 19: RL-FR-RR-FL 。。
|
||
# 20: RL-FL-FR-RR
|
||
# 21: RL-FL-RR-FR
|
||
# 22: RL-RR-FR-FL
|
||
# 23: RL-RR-FL-FR 。。
|
||
ACTION_MAPPING = 0 # 选择测试组 (0-23)
|
||
|
||
# 腿部顺序定义
|
||
LEG_MAPPING = [
|
||
[0, 1, 2, 3], # 0: FR-FL-RR-RL (MuJoCo顺序)
|
||
[0, 1, 3, 2], # 1: FR-FL-RL-RR
|
||
[0, 2, 1, 3], # 2: FR-RR-FL-RL
|
||
[0, 2, 3, 1], # 3: FR-RR-RL-FL
|
||
[0, 3, 1, 2], # 4: FR-RL-FL-RR
|
||
[0, 3, 2, 1], # 5: FR-RL-RR-FL
|
||
[1, 0, 2, 3], # 6: FL-FR-RR-RL
|
||
[1, 0, 3, 2], # 7: FL-FR-RL-RR
|
||
[1, 2, 0, 3], # 8: FL-RR-FR-RL
|
||
[1, 2, 3, 0], # 9: FL-RR-RL-FR
|
||
[1, 3, 0, 2], # 10: FL-RL-FR-RR
|
||
[1, 3, 2, 0], # 11: FL-RL-RR-FR
|
||
[2, 0, 1, 3], # 12: RR-FR-FL-RL
|
||
[2, 0, 3, 1], # 13: RR-FR-RL-FL
|
||
[2, 1, 0, 3], # 14: RR-FL-FR-RL
|
||
[2, 1, 3, 0], # 15: RR-FL-RL-FR
|
||
[2, 3, 0, 1], # 16: RR-RL-FR-FL
|
||
[2, 3, 1, 0], # 17: RR-RL-FL-FR
|
||
[3, 0, 1, 2], # 18: RL-FR-FL-RR
|
||
[3, 0, 2, 1], # 19: RL-FR-RR-FL
|
||
[3, 1, 0, 2], # 20: RL-FL-FR-RR
|
||
[3, 1, 2, 0], # 21: RL-FL-RR-FR
|
||
[3, 2, 0, 1], # 22: RL-RR-FR-FL
|
||
[3, 2, 1, 0], # 23: RL-RR-FL-FR
|
||
]
|
||
|
||
# 根据选择生成 DOF_MAPPING
|
||
# 每个腿有3个关节(hip, thigh, calf)
|
||
leg_order = LEG_MAPPING[ACTION_MAPPING]
|
||
DOF_MAPPING = np.array([
|
||
leg_order[0]*3 + 0, leg_order[0]*3 + 1, leg_order[0]*3 + 2, # 腿0
|
||
leg_order[1]*3 + 0, leg_order[1]*3 + 1, leg_order[1]*3 + 2, # 腿1
|
||
leg_order[2]*3 + 0, leg_order[2]*3 + 1, leg_order[2]*3 + 2, # 腿2
|
||
leg_order[3]*3 + 0, leg_order[3]*3 + 1, leg_order[3]*3 + 2, # 腿3
|
||
])
|
||
|
||
# 速度命令范围
|
||
MAX_LIN_VEL = 1.0 # 最大线速度 m/s
|
||
MAX_ANG_VEL = 1.0 # 最大角速度 rad/s
|
||
|
||
# ============================================================
|
||
# 缩放参数
|
||
# ============================================================
|
||
ACTION_SCALE = 0.15 # RL 输出动作的缩放 (映射到关节位置范围)
|
||
POS_SCALE = -1.0 # MLP 位置误差缩放
|
||
VEL_SCALE = 1.0 # MLP 速度缩放
|
||
TORQUE_SCALE = 1.0 # MLP torque 输出缩放
|
||
TORQUE_LIMIT = 23.7 # torque 限幅 (N·m)
|
||
|
||
# MuJoCo 摩擦系数配置
|
||
# friction = [滑动摩擦(X), 滑动摩擦(Y), 扭转摩擦]
|
||
# - friction[0]: X方向滑动摩擦系数
|
||
# - friction[1]: Y方向滑动摩擦系数 (各向异性材料用)
|
||
# - friction[2]: 扭转摩擦系数 (绕法线的旋转阻力)
|
||
#
|
||
# 常见地面摩擦参数参考:
|
||
# ┌──────────┬───────────────────┐
|
||
# │ 地面类型 │ FLOOR_FRICTION │
|
||
# ├──────────┼───────────────────┤
|
||
# │ 冰面 │ [0.05, 0.02, 0.01]│
|
||
# │ 木地板 │ [0.4, 0.3, 0.2 ]│
|
||
# │ 瓷砖 │ [0.6, 0.4, 0.3 ]│
|
||
# │ 橡胶垫 │ [1.5, 1.0, 0.5 ]│
|
||
# │ 粗糙地面 │ [2.0, 1.5, 1.0 ]│
|
||
# └──────────┴───────────────────┘
|
||
# 机器人 geom 摩擦系数
|
||
FRICTION = [0.6, 0.3, 0.3]
|
||
# 地面摩擦系数 (单独设置)
|
||
FLOOR_FRICTION = [2.0, 1.5, 1.0]
|
||
|
||
|
||
# ============================================================
|
||
# 键盘输入读取 (使用后台线程,无需 root 权限)
|
||
# ============================================================
|
||
class KeyboardReader:
|
||
"""使用后台线程读取键盘输入,不需要 root 权限"""
|
||
|
||
def __init__(self):
|
||
self.old_settings = None
|
||
self.keys_pressed = set()
|
||
self.running = True
|
||
self.thread = None
|
||
|
||
def init(self):
|
||
"""初始化终端并启动键盘读取线程"""
|
||
self.old_settings = termios.tcgetattr(sys.stdin)
|
||
tty.setcbreak(sys.stdin.fileno())
|
||
self.thread = threading.Thread(target=self._read_loop, daemon=True)
|
||
self.thread.start()
|
||
|
||
def _read_loop(self):
|
||
"""后台线程:持续读取键盘输入"""
|
||
try:
|
||
while self.running:
|
||
try:
|
||
import select
|
||
if select.select([sys.stdin], [], [], 0.1)[0]:
|
||
ch = sys.stdin.read(1)
|
||
if ch:
|
||
if ch == '\x1b': # ESC
|
||
self.keys_pressed.add('\x1b')
|
||
elif ch == ' ':
|
||
self.keys_pressed.add('space')
|
||
else:
|
||
self.keys_pressed.add(ch.lower())
|
||
except:
|
||
pass
|
||
except:
|
||
pass
|
||
|
||
def restore(self):
|
||
"""恢复终端设置"""
|
||
self.running = False
|
||
if self.thread:
|
||
self.thread.join(timeout=1.0)
|
||
if self.old_settings:
|
||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
|
||
|
||
def is_key_pressed(self, key):
|
||
"""检查按键是否被按下"""
|
||
if key == '\x1b':
|
||
return '\x1b' in self.keys_pressed
|
||
elif key == ' ':
|
||
return 'space' in self.keys_pressed
|
||
return key in self.keys_pressed
|
||
|
||
|
||
# ============================================================
|
||
# 辅助函数
|
||
# ============================================================
|
||
|
||
def get_joint_state(model, data, joint_name):
|
||
"""通过关节名获取关节位置"""
|
||
joint_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, joint_name)
|
||
if joint_id < 0:
|
||
return None
|
||
qposadr = model.jnt_qposadr[joint_id]
|
||
return data.qpos[qposadr]
|
||
|
||
|
||
def compute_observations(data, prev_action, command, default_joint_pos):
|
||
"""
|
||
计算 RL policy 观测向量 (48维)
|
||
|
||
对应 IsaacLab 观测配置:
|
||
[0:3] base_lin_vel - 躯干坐标系线速度
|
||
[3:6] base_ang_vel - 躯干坐标系角速度
|
||
[6:9] projected_gravity - 重力投影到躯干坐标系
|
||
[9:12] velocity_commands - 期望速度
|
||
[12:24] joint_pos_rel - 相对关节位置 (相对于默认位置)
|
||
[24:36] joint_vel_rel - 相对关节速度 (相对于默认速度)
|
||
[36:48] prev_actions - 上一步动作
|
||
"""
|
||
obs = np.zeros(OBS_DIM, dtype=np.float32)
|
||
|
||
# 获取躯干四元数 (MuJoCo 格式: qx, qy, qz, qw)
|
||
quat = data.qpos[3:7] # (x, y, z, w)
|
||
|
||
# 计算旋转矩阵 (从世界坐标系到躯干坐标系)
|
||
R = quaternion_to_rotation_matrix(quat).T # 转置得到逆变换 (世界->躯干)
|
||
|
||
# 1. 躯干线速度 - 转换到躯干坐标系
|
||
# IsaacLab: root_lin_vel_b = R @ root_lin_vel_w
|
||
global_lin_vel = data.qvel[0:3]
|
||
obs[0:3] = R @ global_lin_vel
|
||
|
||
# 2. 躯干角速度 - 转换到躯干坐标系
|
||
# IsaacLab: root_ang_vel_b = R @ root_ang_vel_w
|
||
global_ang_vel = data.qvel[3:6]
|
||
obs[3:6] = R @ global_ang_vel
|
||
|
||
# 3. 重力投影到躯干坐标系
|
||
# IsaacLab: projected_gravity_b = R @ gravity_world
|
||
# 重力向量 (0, 0, -9.81) 在世界坐标系中
|
||
gravity_world = np.array([0.0, 0.0, -9.81])
|
||
obs[6:9] = R @ gravity_world
|
||
|
||
# 4. 速度命令
|
||
obs[9:12] = command
|
||
|
||
# 5. 相对关节位置 (joint_pos - default_joint_pos)
|
||
# IsaacLab: joint_pos_rel = joint_pos - default_joint_pos
|
||
current_joint_pos = data.qpos[7:19]
|
||
obs[12:24] = current_joint_pos - default_joint_pos
|
||
|
||
# 6. 相对关节速度
|
||
# IsaacLab: joint_vel_rel = joint_vel - default_joint_vel (default = 0)
|
||
default_joint_vel = np.zeros(12) # IsaacLab 默认关节速度为 0
|
||
obs[24:36] = data.qvel[6:18] - default_joint_vel
|
||
|
||
# 7. 上一步动作
|
||
obs[36:48] = prev_action
|
||
|
||
return obs
|
||
|
||
|
||
def quaternion_to_rotation_matrix(q):
|
||
"""四元数转旋转矩阵 (MuJoCo 格式: qx, qy, qz, qw)
|
||
|
||
返回的矩阵将世界坐标系中的向量转换到以该四元数表示的局部坐标系中
|
||
"""
|
||
qx, qy, qz, qw = q
|
||
# 归一化确保数值稳定性
|
||
norm = np.sqrt(qx**2 + qy**2 + qz**2 + qw**2)
|
||
qx, qy, qz, qw = qx/norm, qy/norm, qz/norm, qw/norm
|
||
return np.array([
|
||
[1-2*(qy**2+qz**2), 2*(qx*qy-qz*qw), 2*(qx*qz+qy*qw)],
|
||
[2*(qx*qy+qz*qw), 1-2*(qx**2+qz**2), 2*(qy*qz-qx*qw)],
|
||
[2*(qx*qz-qy*qw), 2*(qy*qz+qx*qw), 1-2*(qx**2+qy**2)]
|
||
])
|
||
|
||
|
||
# ============================================================
|
||
# MLP Actuator Model (从 IsaacLab 迁移)
|
||
# ============================================================
|
||
class ActuatorMLP:
|
||
"""ActuatorNetMLP 模型 - 用于将 position target 转换为 torque
|
||
|
||
参考: IsaacLab source/isaaclab/isaaclab/actuators/actuator_net.py
|
||
使用批量处理,所有 12 个关节一次前向传播
|
||
"""
|
||
def __init__(self, model_path, num_joints=12):
|
||
import torch
|
||
|
||
self.num_joints = num_joints
|
||
|
||
# 强制使用 CPU 避免 CUDA 版本问题
|
||
self.network = torch.jit.load(model_path, map_location='cpu')
|
||
self.network.eval()
|
||
|
||
# GO1 MLP 配置 (来自 GO1_ACTUATOR_CFG)
|
||
self.pos_scale = POS_SCALE
|
||
self.vel_scale = VEL_SCALE
|
||
self.torque_scale = TORQUE_SCALE
|
||
self.input_idx = [0, 1, 2] # 3步历史
|
||
self.input_order = "pos_vel"
|
||
|
||
# 历史缓冲区 (每个关节独立)
|
||
history_len = max(self.input_idx) + 1 # = 3
|
||
self.pos_error_history = np.zeros((history_len, num_joints), dtype=np.float32)
|
||
self.vel_history = np.zeros((history_len, num_joints), dtype=np.float32)
|
||
|
||
# Torque 限幅 (来自 URDF 和 GO1_ACTUATOR_CFG)
|
||
self.effort_limit = TORQUE_LIMIT # N·m
|
||
|
||
def reset(self):
|
||
"""重置历史缓冲区"""
|
||
self.pos_error_history.fill(0.0)
|
||
self.vel_history.fill(0.0)
|
||
|
||
def compute_torque(self, pos_target, current_pos, current_vel):
|
||
"""计算 torque (使用批量处理,匹配 IsaacLab 实现)
|
||
|
||
Args:
|
||
pos_target: 目标位置 (12,)
|
||
current_pos: 当前关节位置 (12,)
|
||
current_vel: 当前关节速度 (12,)
|
||
|
||
Returns:
|
||
torque: 力矩命令 (12,)
|
||
"""
|
||
import torch
|
||
|
||
# 1. 计算 position error
|
||
pos_error = pos_target - current_pos # (12,)
|
||
|
||
# 2. 更新历史 (移动队列)
|
||
self.pos_error_history = np.roll(self.pos_error_history, 1, axis=0)
|
||
self.pos_error_history[0] = pos_error
|
||
|
||
self.vel_history = np.roll(self.vel_history, 1, axis=0)
|
||
self.vel_history[0] = current_vel
|
||
|
||
# 3. 批量计算 torque (IsaacLab 风格,一次性处理所有关节)
|
||
# pos_error_history: (history_len, num_joints) = (3, 12)
|
||
# vel_history: (history_len, num_joints) = (3, 12)
|
||
#
|
||
# IsaacLab 构建输入的方式:
|
||
# pos_input = torch.cat([history[:, i].unsqueeze(2) for i in input_idx], dim=2) # -> (num_envs, num_joints, 3)
|
||
# pos_input = pos_input.view(num_envs * num_joints, -1) # -> (12, 3)
|
||
# 然后 concat pos_input 和 vel_input 得到 (12, 6)
|
||
|
||
with torch.inference_mode():
|
||
# 构建 pos_input: (num_joints, 3) = (12, 3)
|
||
# 从 history 矩阵中提取 input_idx 指定的行,然后转置
|
||
# pos_error_history[self.input_idx, :] shape = (3, 12)
|
||
# 转置后变成 (12, 3),每行是一个关节的 3 步位置误差历史
|
||
pos_input = self.pos_error_history[self.input_idx, :].T * self.pos_scale # (12, 3)
|
||
|
||
# 构建 vel_input: (num_joints, 3) = (12, 3)
|
||
vel_input = self.vel_history[self.input_idx, :].T * self.vel_scale # (12, 3)
|
||
|
||
# 合并为网络输入: (12, 6)
|
||
if self.input_order == "pos_vel":
|
||
network_input = np.concatenate([pos_input, vel_input], axis=1)
|
||
else:
|
||
network_input = np.concatenate([vel_input, pos_input], axis=1)
|
||
|
||
# 批量推理 (12, 6) -> (12, 1)
|
||
input_tensor = torch.from_numpy(network_input).float() # (12, 6)
|
||
torque_output = self.network(input_tensor).numpy().flatten() # (12,)
|
||
|
||
# 限幅
|
||
torques = np.clip(torque_output * self.torque_scale,
|
||
-self.effort_limit, self.effort_limit)
|
||
|
||
return torques.astype(np.float32)
|
||
|
||
|
||
# ============================================================
|
||
# 主程序
|
||
# ============================================================
|
||
|
||
def main():
|
||
# 1. 加载 MuJoCo 模型
|
||
os.chdir(MESH_PATH)
|
||
with open(XML_PATH, 'r') as f:
|
||
xml_content = f.read()
|
||
xml_content = xml_content.replace('meshdir="../meshes/"', f'meshdir="{MESH_PATH}"')
|
||
xml_content = xml_content.replace('<default>\n=', '<default>\n')
|
||
|
||
model = mujoco.MjModel.from_xml_string(xml_content)
|
||
data = mujoco.MjData(model)
|
||
|
||
print(f"[INFO] MuJoCo Model: {model.nbody} bodies, {model.nq} DoF, {model.nu} actuators")
|
||
print(f"[INFO] 关节顺序: {[mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, i) for i in range(1, 13)]}")
|
||
|
||
# 2. 设置摩擦系数
|
||
floor_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "floor")
|
||
if floor_id >= 0:
|
||
model.geom_friction[floor_id] = FLOOR_FRICTION
|
||
print(f"[INFO] 地面摩擦系数设置为: {FLOOR_FRICTION}")
|
||
else:
|
||
print(f"[WARNING] 未找到 floor geom,使用默认摩擦系数")
|
||
for i in range(model.ngeom):
|
||
if i != floor_id:
|
||
model.geom_friction[i] = FRICTION
|
||
print(f"[INFO] 机器人摩擦系数设置为: {FRICTION}")
|
||
|
||
# 3. 加载 ONNX 策略
|
||
import onnxruntime as ort
|
||
sess = ort.InferenceSession(ONNX_PATH, providers=['CPUExecutionProvider'])
|
||
print(f"[INFO] ONNX Policy loaded from: {ONNX_PATH}")
|
||
print(f"[INFO] 观测维度: {OBS_DIM}, 动作维度: {ACTION_DIM}")
|
||
|
||
# 3. 加载 MLP Actuator 模型 (IsaacLab 风格)
|
||
try:
|
||
actuator_mlp = ActuatorMLP(MLP_PATH, num_joints=ACTION_DIM)
|
||
print(f"[INFO] ActuatorMLP loaded from: {MLP_PATH}")
|
||
use_mlp = True
|
||
except Exception as e:
|
||
print(f"[WARNING] Failed to load ActuatorMLP: {e}")
|
||
print(f"[INFO] Falling back to PD control")
|
||
use_mlp = False
|
||
|
||
# 4. 初始化
|
||
obs = np.zeros(OBS_DIM, dtype=np.float32)
|
||
prev_action = np.zeros(ACTION_DIM, dtype=np.float32)
|
||
|
||
# IsaacLab UNITREE_GO1_CFG 默认关节位置 (用于计算相对关节位置)
|
||
# 关节顺序: FR_hip, FR_thigh, FR_calf, FL_hip, FL_thigh, FL_calf, RR_hip, RR_thigh, RR_calf, RL_hip, RL_thigh, RL_calf
|
||
default_joint_pos = np.array([
|
||
-0.1, # FR_hip
|
||
0.8, # FR_thigh
|
||
-1.5, # FR_calf
|
||
0.1, # FL_hip
|
||
0.8, # FL_thigh
|
||
-1.5, # FL_calf
|
||
-0.1, # RR_hip
|
||
1.0, # RR_thigh
|
||
-1.5, # RR_calf
|
||
0.1, # RL_hip
|
||
1.0, # RL_thigh
|
||
-1.5, # RL_calf
|
||
])
|
||
|
||
# 重置机器人关节位置 (与 default_joint_pos 一致)
|
||
# 注意: MuJoCo 关节角度单位是 rad
|
||
# URDF 关节范围:
|
||
# Hip: -0.803 ~ 0.803 rad
|
||
# Thigh: -1.047 ~ 4.189 rad
|
||
# Calf: -2.697 ~ -0.916 rad
|
||
crouch_pos = np.array([
|
||
-0.1, # FR_hip (URDF: -0.803 ~ 0.803)
|
||
0.8, # FR_thigh (URDF: -1.047 ~ 4.189)
|
||
-1.5, # FR_calf (URDF: -2.697 ~ -0.916)
|
||
0.1, # FL_hip
|
||
0.8, # FL_thigh
|
||
-1.5, # FL_calf
|
||
-0.1, # RR_hip
|
||
1.0, # RR_thigh
|
||
-1.5, # RR_calf
|
||
0.1, # RL_hip
|
||
1.0, # RL_thigh
|
||
-1.5, # RL_calf
|
||
])
|
||
data.qpos[7:19] = crouch_pos
|
||
# 重置 qvel 为零,避免初始速度导致不稳定
|
||
data.qvel[:] = 0.0
|
||
# 将躯干抬高一点,避免初始碰撞
|
||
data.qpos[2] = 0.35
|
||
mujoco.mj_step(model, data) # 执行一步让状态更新
|
||
|
||
# 重置 MLP actuator 历史
|
||
if use_mlp:
|
||
actuator_mlp.reset()
|
||
|
||
print("[INFO] 机器人初始化完成,开始 RL 策略推理...", flush=True)
|
||
|
||
# 初始化键盘控制
|
||
keyboard_reader = KeyboardReader()
|
||
keyboard_reader.init()
|
||
print("[INFO] 键盘控制已启用!", flush=True)
|
||
print(" W/S: 前进/后退", flush=True)
|
||
print(" A/D: 左转/右转", flush=True)
|
||
print(" Q/E: 侧向左移/右移", flush=True)
|
||
print(" 空格: 停止", flush=True)
|
||
print(" ESC: 退出", flush=True)
|
||
|
||
# 4. 交互式可视化
|
||
import mujoco.viewer as mv
|
||
view = mv.launch_passive(model, data)
|
||
print("[INFO] 交互式查看器已启动!", flush=True)
|
||
print(" 鼠标拖拽旋转视角, 滚轮缩放, ESC退出", flush=True)
|
||
|
||
step_count = 0
|
||
last_inference_time = time.time()
|
||
inference_interval = 0.02 # 50Hz = 20ms
|
||
torque = np.zeros(ACTION_DIM, dtype=np.float32) # 初始化 torque
|
||
import sys
|
||
sys.stderr.write(f"[DEBUG] Starting loop\n")
|
||
sys.stderr.flush()
|
||
|
||
# 命令速度 (使用 numpy array 以便在循环中修改)
|
||
command = np.array([1.0, 0.0, 0.0], dtype=np.float32) # vx, vy, yaw_rate
|
||
|
||
while view.is_running() and not g_exit_requested:
|
||
current_time = time.time()
|
||
|
||
# W/S: 前进/后退 (vx)
|
||
if keyboard_reader.is_key_pressed('w'):
|
||
command[0] = MAX_LIN_VEL
|
||
elif keyboard_reader.is_key_pressed('s'):
|
||
command[0] = -MAX_LIN_VEL
|
||
else:
|
||
command[0] = 0.0
|
||
|
||
# Q/E: 侧向移动 (vy)
|
||
if keyboard_reader.is_key_pressed('q'):
|
||
command[1] = MAX_LIN_VEL
|
||
elif keyboard_reader.is_key_pressed('e'):
|
||
command[1] = -MAX_LIN_VEL
|
||
else:
|
||
command[1] = 0.0
|
||
|
||
# A/D: 左转/右转 (yaw_rate)
|
||
if keyboard_reader.is_key_pressed('a'):
|
||
command[2] = MAX_ANG_VEL
|
||
elif keyboard_reader.is_key_pressed('d'):
|
||
command[2] = -MAX_ANG_VEL
|
||
else:
|
||
command[2] = 0.0
|
||
|
||
# 空格: 停止
|
||
if keyboard_reader.is_key_pressed(' '):
|
||
command[:] = 0.0
|
||
|
||
# ESC: 退出
|
||
if keyboard_reader.is_key_pressed('\x1b'):
|
||
sys.stderr.write("[INFO] ESC pressed, exiting...\n")
|
||
break
|
||
|
||
# 50Hz 频率限制推理
|
||
if current_time - last_inference_time >= inference_interval:
|
||
# 计算观测
|
||
obs = compute_observations(data, prev_action, command, default_joint_pos)
|
||
|
||
# 推理获取动作
|
||
action = sess.run(None, {'obs': obs.reshape(1, -1)})[0][0]
|
||
|
||
# 动作缩放 (RL 输出是位置目标, 范围 [-1, 1], 映射到关节范围)
|
||
# IsaacLab 配置: actions.joint_pos.scale = 0.25
|
||
action_scaled = action * ACTION_SCALE
|
||
|
||
# 应用关节顺序映射 (policy输出 -> MuJoCo关节)
|
||
action_scaled_mapped = action_scaled[DOF_MAPPING]
|
||
|
||
# 获取当前关节状态
|
||
current_pos = data.qpos[7:19]
|
||
current_vel = data.qvel[6:18]
|
||
|
||
# 使用 MLP 计算 torque (与 IsaacLab 一致)
|
||
if use_mlp:
|
||
torque = actuator_mlp.compute_torque(
|
||
action_scaled_mapped, current_pos, current_vel
|
||
)
|
||
else:
|
||
# Fallback PD 控制
|
||
kp = 10.0
|
||
kd = 0.0
|
||
torque = kp * (action_scaled_mapped - current_pos) - kd * current_vel
|
||
|
||
# 保存上一步动作
|
||
prev_action = action.copy()
|
||
last_inference_time = current_time
|
||
|
||
# 应用控制 (力矩控制) - 每步都应用
|
||
data.ctrl[:] = torque
|
||
|
||
# 执行仿真
|
||
mujoco.mj_step(model, data)
|
||
view.sync()
|
||
|
||
step_count += 1
|
||
trunk_z = data.qpos[2]
|
||
lin_vel = np.linalg.norm(data.qvel[0:3])
|
||
ang_vel = np.linalg.norm(data.qvel[3:6])
|
||
sys.stderr.write(f" Step {step_count}: cmd=[{command[0]:.2f}, {command[1]:.2f}, {command[2]:.2f}] | "
|
||
f"trunk_z={trunk_z:.3f}m, vel={lin_vel:.3f}m/s\n")
|
||
sys.stderr.flush()
|
||
|
||
# 恢复终端设置
|
||
if g_exit_requested:
|
||
sys.stderr.write("[INFO] Ctrl+C pressed, exiting...\n")
|
||
keyboard_reader.restore()
|
||
view.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|