Backup DreamWaQ rslrl stability fixes

This commit is contained in:
8x54zj-m
2026-07-22 02:17:01 +08:00
parent c664e7422f
commit 3648551043
17 changed files with 1068 additions and 96 deletions

Binary file not shown.

View File

@@ -121,18 +121,20 @@ class DreamWaQCfg(Go1WalkNpEnvCfg):
"tracking_lin_vel": 1.5, "tracking_lin_vel": 1.5,
"tracking_ang_vel": 1.0, "tracking_ang_vel": 1.0,
"lin_vel_z": -2.0, "lin_vel_z": -2.0,
"ang_vel_xy": -0.05, "ang_vel_xy": -0.10, # 抑制晃动
"orientation": -0.2, "orientation": -0.5, # 强制平稳姿态
"dof_acc": -2.5e-7, "dof_acc": -2.5e-7,
"base_height": -1.0, "base_height": -10.0, # 强惩罚身高偏差,避免蹲伏或踮脚
"feet_air_time": 0.1, "feet_air_time": 0.1,
"action_rate": -0.01, "action_rate": -0.01,
"joint_power": -2e-5, "joint_power": -2e-5,
"smoothness": -0.01, "smoothness": -0.02,
"power_distribution": -10e-6, "power_distribution": -10e-6,
# 注意:stand_still 在上游被注释掉 "stand_still": -0.5,
"dof_pos_limits": -5.0, # 关节限位软约束(参考 M20 修改版)
"collision": -1.0, # 惩罚身体碰撞
}) })
self.reward_config.only_positive_rewards = True self.reward_config.only_positive_rewards = False # 让坏行为负反馈直达策略
self.reward_config.tracking_sigma = 0.25 self.reward_config.tracking_sigma = 0.25
@@ -181,12 +183,25 @@ class DreamWaQTask(Go1WalkTask):
# ── 动作裁剪 + 力矩计算 ── # ── 动作裁剪 + 力矩计算 ──
def apply_action(self, actions, state): def apply_action(self, actions, state):
"""裁剪动作防止奖励计算溢出(上游 clip_actions=100)。""" """裁剪动作 + 随机延迟(模拟真实部署延迟,提高 sim-to-real 泛化)。"""
actions = np.clip(actions, -100.0, 100.0) actions = np.clip(actions, -4.0, 4.0)
return super().apply_action(actions, state) # 随机动作延迟0-3 个控制步0-60ms
if not hasattr(self, '_action_buffer'):
self._action_buffer = np.zeros((self._num_envs, 3, self._num_action), dtype=np.float32)
self._latency_steps = np.random.randint(0, 4, size=self._num_envs)
# 滑动缓冲区,新动作插入末尾
self._action_buffer = np.concatenate([
self._action_buffer[:, 1:, :], actions[:, np.newaxis, :]
], axis=1)
# 根据每个 env 的延迟取对应历史动作
delayed_actions = np.zeros_like(actions)
for i in range(self._num_envs):
lat = self._latency_steps[i]
delayed_actions[i] = self._action_buffer[i, 2 - lat] # buffer[-1] 是最新
return super().apply_action(delayed_actions, state)
def _compute_torques(self, actions, data): def _compute_torques(self, actions, data):
"""PD 控制器 + 域随机化。力矩裁剪防溢出。""" """PD 控制器 + 域随机化 + 关节限位。力矩裁剪防溢出。"""
state = getattr(self, '_state', None) state = getattr(self, '_state', None)
if state is not None: if state is not None:
motor_strength = state.info.get("motor_strength", motor_strength = state.info.get("motor_strength",
@@ -201,8 +216,13 @@ class DreamWaQTask(Go1WalkTask):
kd_factor = np.ones(1, dtype=np.float32) kd_factor = np.ones(1, dtype=np.float32)
actions_scaled = actions * self.cfg.control_config.action_scale * motor_strength[:, np.newaxis] actions_scaled = actions * self.cfg.control_config.action_scale * motor_strength[:, np.newaxis]
# 目标角度限制在关节范围内(与 MuJoCo 硬限位对齐)
target = actions_scaled + self.default_angles
lo = self._model.joint_limits[0][np.newaxis, :] # (1, 12)
hi = self._model.joint_limits[1][np.newaxis, :]
target = np.clip(target, lo, hi)
torques = (self.kps * kp_factor[:, np.newaxis]) * ( torques = (self.kps * kp_factor[:, np.newaxis]) * (
actions_scaled + self.default_angles - self.get_dof_pos(data) target - self.get_dof_pos(data)
) - (self.kds * kd_factor[:, np.newaxis]) * self.get_dof_vel(data) ) - (self.kds * kd_factor[:, np.newaxis]) * self.get_dof_vel(data)
return np.clip(torques, -80.0, 80.0) return np.clip(torques, -80.0, 80.0)
@@ -326,14 +346,31 @@ class DreamWaQTask(Go1WalkTask):
def update_observation(self, state): def update_observation(self, state):
data = state.data data = state.data
# 清理物理崩溃残留的 NaN/Inf(仅在检测到时才修改,避免无谓开销) # 清理物理崩溃残留的 NaN/Inf
dv = data.dof_vel dv = data.dof_vel
if np.any(~np.isfinite(dv)): if np.any(~np.isfinite(dv)):
data.set_dof_vel(np.nan_to_num(np.array(dv), nan=0.0, posinf=0.0, neginf=0.0)) dv_clean = np.array(dv)
dv_clean[:, 6:] = np.nan_to_num(dv_clean[:, 6:], nan=0.0, posinf=0.0, neginf=0.0)
data.set_dof_vel(dv_clean)
dp = data.dof_pos dp = data.dof_pos
if np.any(~np.isfinite(dp)): if np.any(~np.isfinite(dp)):
data.set_dof_pos(np.nan_to_num(np.array(dp), nan=0.0, posinf=0.0, neginf=0.0), self._model) dp_clean = np.array(dp)
# 四元数 NaN → 整行替换为单位四元数 [0,0,0,1]
quat_nan_row = np.any(~np.isfinite(dp_clean[:, 3:7]), axis=1)
if np.any(quat_nan_row):
dp_clean[quat_nan_row, 3:7] = [0.0, 0.0, 0.0, 1.0]
# 关节位置 NaN → 0
dp_clean[:, 7:] = np.nan_to_num(dp_clean[:, 7:], nan=0.0, posinf=0.0, neginf=0.0)
data.set_dof_pos(dp_clean, self._model)
else:
quat_norm = np.linalg.norm(dp[:, 3:7], axis=1)
bad_quat = quat_norm < 1e-6
if np.any(bad_quat):
dp_clean = np.array(dp)
dp_clean[bad_quat, 3:7] = [0.0, 0.0, 0.0, 1.0]
data.set_dof_pos(dp_clean, self._model)
obs = self._get_obs(data, state.info) obs = self._get_obs(data, state.info)
obs = np.nan_to_num(obs, nan=0.0, posinf=0.0, neginf=0.0)
# 更新历史缓冲区CENet 用不含当前帧的历史预测下一帧(论文 LVAE = MSE(õ_{t+1}, o_{t+1}) # 更新历史缓冲区CENet 用不含当前帧的历史预测下一帧(论文 LVAE = MSE(õ_{t+1}, o_{t+1})
full_hist = state.info.get("obs_history_full", full_hist = state.info.get("obs_history_full",
@@ -360,8 +397,10 @@ class DreamWaQTask(Go1WalkTask):
if do_rand.any(): if do_rand.any():
n_rand = int(do_rand.sum()) n_rand = int(do_rand.sum())
state.info["motor_strength"][do_rand] = np.random.uniform(0.9, 1.1, size=n_rand).astype(np.float32) state.info["motor_strength"][do_rand] = np.random.uniform(0.9, 1.1, size=n_rand).astype(np.float32)
state.info["kp_factor"][do_rand] = np.random.uniform(0.9, 1.1, size=n_rand).astype(np.float32) state.info["kp_factor"][do_rand] = np.random.uniform(0.85, 1.15, size=n_rand).astype(np.float32)
state.info["kd_factor"][do_rand] = np.random.uniform(0.9, 1.1, size=n_rand).astype(np.float32) state.info["kd_factor"][do_rand] = np.random.uniform(0.85, 1.15, size=n_rand).astype(np.float32)
# 动作延迟随机化(每个 rand 周期重新分配延迟步数)
self._latency_steps[do_rand] = np.random.randint(0, 4, size=n_rand)
state.info["friction_coeff"][do_rand] = np.random.uniform(0.2, 1.25, size=n_rand).astype(np.float32) state.info["friction_coeff"][do_rand] = np.random.uniform(0.2, 1.25, size=n_rand).astype(np.float32)
state.info["added_mass"][do_rand] = np.random.uniform(-1.0, 2.0, size=n_rand).astype(np.float32) state.info["added_mass"][do_rand] = np.random.uniform(-1.0, 2.0, size=n_rand).astype(np.float32)
state.info["com_displacement"][do_rand] = np.random.uniform(-0.05, 0.05, size=(n_rand, 3)).astype(np.float32) state.info["com_displacement"][do_rand] = np.random.uniform(-0.05, 0.05, size=(n_rand, 3)).astype(np.float32)
@@ -369,6 +408,8 @@ class DreamWaQTask(Go1WalkTask):
# 特权观测 # 特权观测
state.info["privileged_obs"] = self._get_privileged_obs(data, obs) state.info["privileged_obs"] = self._get_privileged_obs(data, obs)
state.info["privileged_obs"] = np.nan_to_num(
state.info["privileged_obs"], nan=0.0, posinf=0.0, neginf=0.0)
state.info["base_vel"] = self.get_local_linvel(data) state.info["base_vel"] = self.get_local_linvel(data)
# 足部接触 # 足部接触
@@ -429,7 +470,9 @@ class DreamWaQTask(Go1WalkTask):
cy = half_y - self._border - row * self._cell_size - self._cell_size / 2 cy = half_y - self._border - row * self._cell_size - self._cell_size / 2
all_origins[row, col] = [cx, cy] all_origins[row, col] = [cx, cy]
self._terrain_origins = all_origins self._terrain_origins = all_origins
self._max_init_level = 0 # 上游原值 # 跟踪课程进度:动态 max_init_level,避免将已学会的机器人拉回平地
self._max_init_level = 0
self._level_history = []
if num_reset > 0 and self._init_done and state is not None and hasattr(state, 'info'): if num_reset > 0 and self._init_done and state is not None and hasattr(state, 'info'):
old_info = state.info old_info = state.info
@@ -439,16 +482,117 @@ class DreamWaQTask(Go1WalkTask):
base_pose = self._body.get_pose(state.data) base_pose = self._body.get_pose(state.data)
base_pos = base_pose[done, :2] base_pos = base_pose[done, :2]
distance = np.linalg.norm(base_pos - old_origins, axis=1) distance = np.linalg.norm(base_pos - old_origins, axis=1)
move_up = distance > 3.0 # ── 多条件 Curriculum 升级 ──
cmd_speed = np.linalg.norm(old_commands[:, :2], axis=1) # 1) 速度跟踪: exp(-L2²/σ), σ=0.25, >0.5 表示等效恒定 L2 误差约 0.42m/s
required_dist = cmd_speed * (self.cfg.max_episode_steps * self.cfg.ctrl_dt) * 0.5 ep_tracking = old_info.get("ep_tracking_sum", np.zeros(self._num_envs, dtype=np.float32))[done]
move_down = (distance < required_dist) & ~move_up ep_steps = old_info.get("ep_steps", np.zeros(self._num_envs, dtype=np.int32))[done]
mask = ep_steps > 0
avg_tracking = np.zeros(num_reset, dtype=np.float32)
avg_tracking[mask] = ep_tracking[mask] / ep_steps[mask].astype(np.float32)
# 调试:打印 avg_tracking 分布(单次)
if not hasattr(self, '_dbg_tracking_printed'):
self._dbg_tracking_printed = True
valid = avg_tracking[mask]
if len(valid) > 0:
print(f"[Curric-DBG] avg_tracking: mean={np.mean(valid):.4f} "
f"min={np.min(valid):.4f} p10={np.percentile(valid,10):.4f} "
f"p50={np.percentile(valid,50):.4f} p90={np.percentile(valid,90):.4f} "
f"max={np.max(valid):.4f} >0.5={100*np.mean(valid>0.5):.0f}%")
# 2) 存活率: 必须自然结束(truncated), 而非摔倒终止(terminated)
term = state.terminated[done] if hasattr(state, 'terminated') else np.zeros(num_reset, dtype=bool)
not_fallen = ~term # True=活到 episode 结束
# 3) 姿态: avg_orient 保留 dt-0.0005 对应等效倾角约 13 deg
ep_orient = old_info.get("ep_orientation", np.zeros(self._num_envs, dtype=np.float32))[done]
avg_orient = np.zeros(num_reset, dtype=np.float32)
avg_orient[mask] = ep_orient[mask] / ep_steps[mask].astype(np.float32)
# 4) 身高: avg_base_height 保留 dt-0.001 对应 RMS 高度误差约 7cm
ep_bh = old_info.get("ep_base_height", np.zeros(self._num_envs, dtype=np.float32))[done]
avg_bh = np.zeros(num_reset, dtype=np.float32)
avg_bh[mask] = ep_bh[mask] / ep_steps[mask].astype(np.float32)
# orient: -0.5*dt*sin²(theta); bh: -10*dt*z_err²
move_up_raw = (
(avg_tracking > 0.5)
& not_fallen
& (avg_orient > -0.0005)
& (avg_bh > -0.001)
)
# ── 渐进升级:连续通过 3 次 + 冷却期 + 每次只升 1 级 ──
if not hasattr(self, '_consecutive_pass_count'):
self._consecutive_pass_count = np.zeros(self._num_envs, dtype=np.int32)
self._pending_upgrade = np.zeros(self._num_envs, dtype=bool)
self._upgrade_cooldown = np.zeros(self._num_envs, dtype=np.int32)
# 每 env: 连续通过 +1, 失败重置(仅更新被 reset 的 env
self._consecutive_pass_count[done_idx] = np.where(
move_up_raw,
self._consecutive_pass_count[done_idx] + 1, 0)
# 冷却期以该 env 自己完成的 episode 为单位,而不是全局 reset 批次数。
self._upgrade_cooldown[done_idx] = np.maximum(0, self._upgrade_cooldown[done_idx] - 1)
streak_ready = self._consecutive_pass_count[done_idx] >= 3
cooldown_blocked = streak_ready & (self._upgrade_cooldown[done_idx] > 0)
# 连续 3 次通过 且 不在冷却期 → 升级(取被 reset env 的当前状态)
can_upgrade = streak_ready & (self._upgrade_cooldown[done_idx] == 0)
# 升级后设置 5 episode 冷却期,并从新等级重新累计连续通过次数。
self._upgrade_cooldown[done_idx] = np.where(
can_upgrade, 5, self._upgrade_cooldown[done_idx])
self._consecutive_pass_count[done_idx] = np.where(
can_upgrade, 0, self._consecutive_pass_count[done_idx])
move_up = can_upgrade
move_down_raw = (
(~not_fallen)
| (avg_tracking < 0.35)
| (avg_orient <= -0.001)
| (avg_bh <= -0.002)
)
move_down = move_down_raw & ~move_up
new_levels = np.where(move_up, old_levels + 1, old_levels) new_levels = np.where(move_up, old_levels + 1, old_levels)
new_levels = np.where(move_down, new_levels - 1, new_levels) new_levels = np.where(move_down, new_levels - 1, new_levels)
at_max = new_levels >= self._num_rows
if at_max.any():
new_levels[at_max] = np.random.randint(0, self._num_rows, size=int(at_max.sum()))
new_levels = np.clip(new_levels, 0, self._num_rows - 1) new_levels = np.clip(new_levels, 0, self._num_rows - 1)
# 跟踪 level 分布,动态更新 max_init_levelP90 - 2
self._level_history.extend(new_levels.tolist())
if len(self._level_history) > 500:
self._level_history = self._level_history[-500:]
if len(self._level_history) >= 10:
sorted_lv = sorted(self._level_history)
self._max_init_level = max(0, sorted_lv[int(len(sorted_lv) * 0.9)] - 2)
# ── 调试日志: curriculum 各条件通过率 ──
if not hasattr(self, '_curric_log_counter'):
self._curric_log_counter = 0
self._curric_track_ok = 0; self._curric_survive_ok = 0
self._curric_orient_ok = 0; self._curric_bh_ok = 0
self._curric_raw_all_ok = 0; self._curric_streak_ok = 0
self._curric_upgrade_ok = 0; self._curric_cooldown_block = 0
self._curric_down_ok = 0; self._curric_down_track = 0
self._curric_down_stability = 0; self._curric_total = 0
self._curric_log_counter += 1
self._curric_track_ok += int((avg_tracking > 0.5).sum())
self._curric_survive_ok += int(not_fallen.sum())
self._curric_orient_ok += int((avg_orient > -0.0005).sum())
self._curric_bh_ok += int((avg_bh > -0.001).sum())
self._curric_raw_all_ok += int(move_up_raw.sum())
self._curric_streak_ok += int(streak_ready.sum())
self._curric_upgrade_ok += int(move_up.sum())
self._curric_cooldown_block += int(cooldown_blocked.sum())
self._curric_down_ok += int(move_down.sum())
self._curric_down_track += int((avg_tracking < 0.35).sum())
self._curric_down_stability += int((~not_fallen | (avg_orient <= -0.001) | (avg_bh <= -0.002)).sum())
self._curric_total += num_reset
if self._curric_log_counter % 50 == 0:
def pct(n): return 100*n/max(self._curric_total,1)
print(f"[Curric] n={self._curric_total:5d} "
f"track={pct(self._curric_track_ok):.0f}% "
f"survive={pct(self._curric_survive_ok):.0f}% "
f"orient={pct(self._curric_orient_ok):.0f}% "
f"bh={pct(self._curric_bh_ok):.0f}% "
f"rawALL={pct(self._curric_raw_all_ok):.0f}% "
f"streak3={pct(self._curric_streak_ok):.0f}% "
f"up={pct(self._curric_upgrade_ok):.0f}% "
f"cool={pct(self._curric_cooldown_block):.0f}% "
f"down={pct(self._curric_down_ok):.0f}% "
f"downT={pct(self._curric_down_track):.0f}% "
f"downS={pct(self._curric_down_stability):.0f}% "
f"max_init={self._max_init_level}")
else: else:
new_levels = np.random.randint(0, self._max_init_level + 1, size=num_reset, dtype=np.int32) new_levels = np.random.randint(0, self._max_init_level + 1, size=num_reset, dtype=np.int32)
self._init_done = True self._init_done = True
@@ -545,12 +689,32 @@ class DreamWaQTask(Go1WalkTask):
return obs, info return obs, info
# ── 新增奖励(参考 M20 修改版)──
def _reward_dof_pos_limits(self, data):
"""关节软限位惩罚——接近关节极限时扣分。"""
lo = self._model.joint_limits[0]
hi = self._model.joint_limits[1]
margin = 0.1 # rad 软边界
pos = self.get_dof_pos(data)
lower = np.clip(lo + margin - pos, 0, None)
upper = np.clip(pos - (hi - margin), 0, None)
return np.sum(lower + upper, axis=1)
def _reward_collision(self, data):
"""身体碰撞惩罚——膝/肩触地扣分。"""
cquerys = self._model.get_contact_query(data)
penal = cquerys.is_colliding(self.termination_check)
return np.any(penal.reshape(self._num_envs, -1), axis=1).astype(np.float32)
def update_terminated(self, state): def update_terminated(self, state):
"""基类接触终止 + hfield 边界终止(防止走出地形导致 NaN""" """基类接触终止 + hfield 边界终止(防止走出地形导致 NaN"""
state = super().update_terminated(state) state = super().update_terminated(state)
if self._model.num_hfields == 0:
return state # flat 场景无 hfield
hf = self._model.get_hfield(0)
pose = self._body.get_pose(state.data) pose = self._body.get_pose(state.data)
base_xy = pose[:, :2] base_xy = pose[:, :2]
hf = self._model.get_hfield(0)
b = hf.bound b = hf.bound
out_of_bounds = ( out_of_bounds = (
(base_xy[:, 0] < b[0]) | (base_xy[:, 0] > b[3]) | (base_xy[:, 0] < b[0]) | (base_xy[:, 0] > b[3]) |
@@ -577,7 +741,9 @@ class DreamWaQTask(Go1WalkTask):
"joint_power": self._reward_joint_power(data), "joint_power": self._reward_joint_power(data),
"smoothness": self._reward_smoothness(info), "smoothness": self._reward_smoothness(info),
"power_distribution": self._reward_power_distribution(data), "power_distribution": self._reward_power_distribution(data),
# stand_still 在上游被注释掉 "stand_still": self._reward_stand_still(data, commands),
"dof_pos_limits": self._reward_dof_pos_limits(data),
"collision": self._reward_collision(data),
} }
def update_feet_air_time(self, info: dict): def update_feet_air_time(self, info: dict):
@@ -606,6 +772,13 @@ class DreamWaQTask(Go1WalkTask):
rew *= np.linalg.norm(commands[:, :2], axis=1) > 0.1 rew *= np.linalg.norm(commands[:, :2], axis=1) > 0.1
return rew return rew
def _reward_stand_still(self, data, commands):
"""惩罚有命令但速度接近零的'卡住'行为。"""
cmd_norm = np.linalg.norm(commands[:, :2], axis=1)
vel_norm = np.linalg.norm(self.get_local_linvel(data)[:, :2], axis=1)
stifled = (cmd_norm > 0.2) & (vel_norm < 0.1) # 有命令但基本不动
return stifled.astype(np.float32)
def update_reward(self, state): def update_reward(self, state):
"""存储各项奖励到 TensorBoard + 更新 state.reward。 """存储各项奖励到 TensorBoard + 更新 state.reward。

View File

@@ -49,19 +49,10 @@
<geom size="0.046 0.02" quat="1 1 0 0" type="cylinder"/> <geom size="0.046 0.02" quat="1 1 0 0" type="cylinder"/>
</default> </default>
<default class="thigh1"> <default class="thigh1">
<geom size="0.015" fromto="-0.02 0 0 -0.02 0 -0.16"/> <geom type="capsule" size="0.02" fromto="0 0 0 0 0 -0.213"/>
</default>
<default class="thigh2">
<geom size="0.015" fromto="0 0 0 -0.02 0 -0.1"/>
</default>
<default class="thigh3">
<geom size="0.015" fromto="-0.02 0 -0.16 0 0 -0.2"/>
</default> </default>
<default class="calf1"> <default class="calf1">
<geom size="0.01" fromto="0 0 0 0.02 0 -0.13"/> <geom type="capsule" size="0.015" fromto="0 0 0 0 0 -0.213"/>
</default>
<default class="calf2">
<geom size="0.01" fromto="0.02 0 -0.13 0 0 -0.2"/>
</default> </default>
<default class="foot"> <default class="foot">
<geom type="sphere" size="0.023" pos="0 0 -0.213" solimp="0.9 .99 0.001" priority="10" condim="3"/> <geom type="sphere" size="0.023" pos="0 0 -0.213" solimp="0.9 .99 0.001" priority="10" condim="3"/>
@@ -97,20 +88,20 @@
<inertial pos="-0.0049166 0.00762615 -8.865e-05" quat="0.507341 0.514169 0.495027 0.482891" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/> <inertial pos="-0.0049166 0.00762615 -8.865e-05" quat="0.507341 0.514169 0.495027 0.482891" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/>
<joint class="abduction" name="FR_hip_joint"/> <joint class="abduction" name="FR_hip_joint"/>
<geom class="visual" mesh="hip" quat="1 0 0 0"/> <geom class="visual" mesh="hip" quat="1 0 0 0"/>
<!-- <geom name="fr_hip" class="hip_right1"/> --> <geom name="fr_hip" class="hip_right1"/>
<body name="FR_thigh" pos="0 -0.08 0"> <body name="FR_thigh" pos="0 -0.08 0">
<inertial pos="-0.00304722 0.019315 -0.0305004" quat="0.65243 -0.0272313 0.0775126 0.753383" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/> <inertial pos="-0.00304722 0.019315 -0.0305004" quat="0.65243 -0.0272313 0.0775126 0.753383" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/>
<joint class="hip" name="FR_thigh_joint"/> <joint class="hip" name="FR_thigh_joint"/>
<geom class="visual" mesh="thigh_mirror"/> <geom class="visual" mesh="thigh_mirror"/>
<!-- <geom name="fr_thigh1" class="thigh1"/> --> <geom name="fr_thigh1" class="thigh1"/>
<!-- <geom name="fr_thigh2" class="thigh2"/> --> <!-- removed: <geom name="fr_thigh2" class="thigh2"/> (simplified to single capsule) -->
<!-- <geom name="fr_thigh3" class="thigh3"/> --> <!-- removed: <geom name="fr_thigh3" class="thigh3"/> (simplified to single capsule) -->
<body name="FR_calf" pos="0 0 -0.213"> <body name="FR_calf" pos="0 0 -0.213">
<inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/> <inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/>
<joint class="knee" name="FR_calf_joint"/> <joint class="knee" name="FR_calf_joint"/>
<geom class="visual" mesh="calf"/> <geom class="visual" mesh="calf"/>
<!-- <geom name="fr_calf1" class="calf1"/> --> <geom name="fr_calf1" class="calf1"/>
<!-- <geom name="fr_calf2" class="calf2"/> --> <!-- removed: <geom name="fr_calf2" class="calf2"/> (simplified to single capsule) -->
<geom name="FR_foot" class="foot"/> <geom name="FR_foot" class="foot"/>
<site name="FR" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/> <site name="FR" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/>
</body> </body>
@@ -120,20 +111,20 @@
<inertial pos="-0.0049166 -0.00762615 -8.865e-05" quat="0.482891 0.495027 0.514169 0.507341" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/> <inertial pos="-0.0049166 -0.00762615 -8.865e-05" quat="0.482891 0.495027 0.514169 0.507341" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/>
<joint class="abduction" name="FL_hip_joint"/> <joint class="abduction" name="FL_hip_joint"/>
<geom class="visual" mesh="hip"/> <geom class="visual" mesh="hip"/>
<!-- <geom name="fl_hip" class="hip_left1"/> --> <geom name="fl_hip" class="hip_left1"/>
<body name="FL_thigh" pos="0 0.08 0"> <body name="FL_thigh" pos="0 0.08 0">
<inertial pos="-0.00304722 -0.019315 -0.0305004" quat="0.753383 0.0775126 -0.0272313 0.65243" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/> <inertial pos="-0.00304722 -0.019315 -0.0305004" quat="0.753383 0.0775126 -0.0272313 0.65243" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/>
<joint class="hip" name="FL_thigh_joint"/> <joint class="hip" name="FL_thigh_joint"/>
<geom class="visual" mesh="thigh"/> <geom class="visual" mesh="thigh"/>
<!-- <geom name="fl_thigh1" class="thigh1"/> --> <geom name="fl_thigh1" class="thigh1"/>
<!-- <geom name="fl_thigh2" class="thigh2"/> --> <!-- removed: <geom name="fl_thigh2" class="thigh2"/> (simplified to single capsule) -->
<!-- <geom name="fl_thigh3" class="thigh3"/> --> <!-- removed: <geom name="fl_thigh3" class="thigh3"/> (simplified to single capsule) -->
<body name="FL_calf" pos="0 0 -0.213"> <body name="FL_calf" pos="0 0 -0.213">
<inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/> <inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/>
<joint class="knee" name="FL_calf_joint"/> <joint class="knee" name="FL_calf_joint"/>
<geom class="visual" mesh="calf"/> <geom class="visual" mesh="calf"/>
<!-- <geom name="fl_calf1" class="calf1"/> --> <geom name="fl_calf1" class="calf1"/>
<!-- <geom name="fl_calf2" class="calf2"/> --> <!-- removed: <geom name="fl_calf2" class="calf2"/> (simplified to single capsule) -->
<geom name="FL_foot" class="foot"/> <geom name="FL_foot" class="foot"/>
<site name="FL" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/> <site name="FL" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/>
</body> </body>
@@ -143,20 +134,20 @@
<inertial pos="0.0049166 0.00762615 -8.865e-05" quat="0.495027 0.482891 0.507341 0.514169" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/> <inertial pos="0.0049166 0.00762615 -8.865e-05" quat="0.495027 0.482891 0.507341 0.514169" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/>
<joint class="abduction" name="RR_hip_joint"/> <joint class="abduction" name="RR_hip_joint"/>
<geom class="visual" quat="0 0 0 -1" mesh="hip"/> <geom class="visual" quat="0 0 0 -1" mesh="hip"/>
<!-- <geom name="rr_hip" class="hip_right1"/> --> <geom name="rr_hip" class="hip_right1"/>
<body name="RR_thigh" pos="0 -0.08 0"> <body name="RR_thigh" pos="0 -0.08 0">
<inertial pos="-0.00304722 0.019315 -0.0305004" quat="0.65243 -0.0272313 0.0775126 0.753383" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/> <inertial pos="-0.00304722 0.019315 -0.0305004" quat="0.65243 -0.0272313 0.0775126 0.753383" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/>
<joint class="hip" name="RR_thigh_joint"/> <joint class="hip" name="RR_thigh_joint"/>
<geom class="visual" mesh="thigh_mirror"/> <geom class="visual" mesh="thigh_mirror"/>
<!-- <geom name="rr_thigh1" class="thigh1"/> <geom name="rr_thigh1" class="thigh1"/>
<geom name="rr_thigh2" class="thigh2"/> <!-- removed: <geom name="rr_thigh2" class="thigh2"/> (simplified to single capsule) -->
<geom name="rr_thigh3" class="thigh3"/> --> <!-- removed: <geom name="rr_thigh3" class="thigh3"/> (simplified to single capsule) -->
<body name="RR_calf" pos="0 0 -0.213"> <body name="RR_calf" pos="0 0 -0.213">
<inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/> <inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/>
<joint class="knee" name="RR_calf_joint"/> <joint class="knee" name="RR_calf_joint"/>
<geom class="visual" mesh="calf"/> <geom class="visual" mesh="calf"/>
<!-- <geom name="rr_calf1" class="calf1"/> <geom name="rr_calf1" class="calf1"/>
<geom name="rr_calf2" class="calf2"/> --> <!-- removed: <geom name="rr_calf2" class="calf2"/> (simplified to single capsule) -->
<geom name="RR_foot" class="foot"/> <geom name="RR_foot" class="foot"/>
<site name="RR" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/> <site name="RR" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/>
</body> </body>
@@ -166,20 +157,20 @@
<inertial pos="0.0049166 -0.00762615 -8.865e-05" quat="0.514169 0.507341 0.482891 0.495027" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/> <inertial pos="0.0049166 -0.00762615 -8.865e-05" quat="0.514169 0.507341 0.482891 0.495027" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/>
<joint class="abduction" name="RL_hip_joint"/> <joint class="abduction" name="RL_hip_joint"/>
<geom class="visual" quat="0 0 1 0" mesh="hip"/> <geom class="visual" quat="0 0 1 0" mesh="hip"/>
<!-- <geom name="rl_hip" class="hip_left1"/> --> <geom name="rl_hip" class="hip_left1"/>
<body name="RL_thigh" pos="0 0.08 0"> <body name="RL_thigh" pos="0 0.08 0">
<inertial pos="-0.00304722 -0.019315 -0.0305004" quat="0.753383 0.0775126 -0.0272313 0.65243" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/> <inertial pos="-0.00304722 -0.019315 -0.0305004" quat="0.753383 0.0775126 -0.0272313 0.65243" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/>
<joint class="hip" name="RL_thigh_joint"/> <joint class="hip" name="RL_thigh_joint"/>
<geom class="visual" mesh="thigh"/> <geom class="visual" mesh="thigh"/>
<!-- <geom name="rl_thigh1" class="thigh1"/> <geom name="rl_thigh1" class="thigh1"/>
<geom name="rl_thigh2" class="thigh2"/> <!-- removed: <geom name="rl_thigh2" class="thigh2"/> (simplified to single capsule) -->
<geom name="rl_thigh3" class="thigh3"/> --> <!-- removed: <geom name="rl_thigh3" class="thigh3"/> (simplified to single capsule) -->
<body name="RL_calf" pos="0 0 -0.213"> <body name="RL_calf" pos="0 0 -0.213">
<inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/> <inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/>
<joint class="knee" name="RL_calf_joint"/> <joint class="knee" name="RL_calf_joint"/>
<geom class="visual" mesh="calf"/> <geom class="visual" mesh="calf"/>
<!-- <geom name="rl_calf1" class="calf1"/> <geom name="rl_calf1" class="calf1"/>
<geom name="rl_calf2" class="calf2"/> --> <!-- removed: <geom name="rl_calf2" class="calf2"/> (simplified to single capsule) -->
<geom name="RL_foot" class="foot"/> <geom name="RL_foot" class="foot"/>
<site name="RL" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/> <site name="RL" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/>
</body> </body>

View File

@@ -182,17 +182,19 @@ class NpEnv(ABEnv):
pass pass
def physics_step(self): def physics_step(self):
self._physics_crashed_this_step = False
# motrixsim.SceneModel.step only supports single step, so we loop # motrixsim.SceneModel.step only supports single step, so we loop
try: try:
for _ in range(self._cfg.sim_substeps): for _ in range(self._cfg.sim_substeps):
self._model.step(self._state.data) self._model.step(self._state.data)
except Exception as e: except BaseException as e:
# Rust panic / 物理崩溃 → 标记终止 + 清除腐蚀数据 # Rust panic / 物理崩溃 → 标记终止 + 清除腐蚀数据
n = self._state.data.shape[0] n = self._state.data.shape[0]
self._state.terminated[:] = True self._state.terminated[:] = True
self._state.reward[:] = 0.0 self._state.reward[:] = 0.0
# 强制重置物理状态,防止 NaN 传播到 observations/rewards # 强制重置物理状态,防止 NaN 传播到 observations/rewards
self._state.data.reset(self._model) self._state.data.reset(self._model)
self._physics_crashed_this_step = True
if not hasattr(self, '_physics_crash_count'): if not hasattr(self, '_physics_crash_count'):
self._physics_crash_count = 0 self._physics_crash_count = 0
self._physics_crash_count += 1 self._physics_crash_count += 1
@@ -213,6 +215,9 @@ class NpEnv(ABEnv):
self._state = self.apply_action(actions, self._state) self._state = self.apply_action(actions, self._state)
assert self._state is not None, "apply_action must return a valid NpEnvState" assert self._state is not None, "apply_action must return a valid NpEnvState"
self.physics_step() self.physics_step()
if getattr(self, "_physics_crashed_this_step", False):
self._reset_done_envs()
return self._state
self._state = self.update_state(self._state) self._state = self.update_state(self._state)
self._state.info["steps"] += 1 self._state.info["steps"] += 1
self._update_truncate() self._update_truncate()

View File

@@ -4,6 +4,43 @@ import torch
import torch.nn as nn import torch.nn as nn
from torch.distributions import Normal from torch.distributions import Normal
class RunningStats(nn.Module):
"""Running mean/variance tracker for actor observation normalization.
Matches SKRL RunningStandardScaler behavior. Without it, actor sees
unscaled inputs at inference → garbage actions → robot collapses.
"""
def __init__(self, num_features: int, eps: float = 1e-8, clip: float = 5.0):
super().__init__()
self.eps = eps
self.clip = clip
self.register_buffer("count", torch.zeros(1, dtype=torch.int64))
self.register_buffer("mean", torch.zeros(num_features))
self.register_buffer("var", torch.ones(num_features))
@torch.no_grad()
def update(self, x):
if x.dim() != 2:
return
n = x.shape[0]
batch_mean = x.mean(dim=0)
batch_var = x.var(dim=0, unbiased=False)
batch_count = torch.tensor(n, dtype=torch.int64, device=x.device)
delta = batch_mean - self.mean
total = self.count + batch_count
self.mean.add_(delta * batch_count.float() / total.float())
m_a = self.var * self.count.float()
m_b = batch_var * batch_count.float()
m2 = m_a + m_b + delta.pow(2) * self.count.float() * batch_count.float() / total.float()
self.var.copy_(m2 / total.float())
self.count.add_(batch_count)
def forward(self, x):
return torch.clamp(
(x - self.mean) / (self.var.sqrt() + self.eps), -self.clip, self.clip)
class ActorCritic_DWAQ(nn.Module): class ActorCritic_DWAQ(nn.Module):
def __init__(self, num_actor_obs, num_critic_obs, num_actions, cenet_in_dim, cenet_out_dim, activation="elu", init_noise_std=1.0,): def __init__(self, num_actor_obs, num_critic_obs, num_actions, cenet_in_dim, cenet_out_dim, activation="elu", init_noise_std=1.0,):
super().__init__() super().__init__()
@@ -52,6 +89,7 @@ class ActorCritic_DWAQ(nn.Module):
) )
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions)) self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
self.actor_normalizer = RunningStats(actor_input_dim, eps=1e-8, clip=5.0)
self.distribution = None self.distribution = None
# disable args validation for speedup # disable args validation for speedup
Normal.set_default_validate_args = False Normal.set_default_validate_args = False
@@ -89,7 +127,7 @@ class ActorCritic_DWAQ(nn.Module):
# code = mean_latent + var*code_temp # code = mean_latent + var*code_temp
# print("latent : ",code[0]) # print("latent : ",code[0])
mean_vel = self.encode_mean_vel(distribution) mean_vel = self.encode_mean_vel(distribution)
logvar_vel = self.encode_mean_vel(distribution) logvar_vel = self.encode_logvar_vel(distribution) # FIXED: was encode_mean_vel
code_latent = self.reparameterise(mean_latent,logvar_latent) code_latent = self.reparameterise(mean_latent,logvar_latent)
code_vel = self.reparameterise(mean_vel,logvar_vel) code_vel = self.reparameterise(mean_vel,logvar_vel)
code = torch.cat((code_vel,code_latent),dim=-1) code = torch.cat((code_vel,code_latent),dim=-1)
@@ -117,17 +155,23 @@ class ActorCritic_DWAQ(nn.Module):
self.distribution = Normal(mean, mean * 0.0 + self.std) self.distribution = Normal(mean, mean * 0.0 + self.std)
def act(self, observations, obs_history, **kwargs): def act(self, observations, obs_history, **kwargs):
code,_,decode,_,_,_,_ = self.cenet_forward(obs_history) code, _, decode, _, _, _, _ = self.cenet_forward(obs_history)
observations = torch.cat((code,observations),dim=-1) observations = torch.cat((code, observations), dim=-1)
if self.training:
self.actor_normalizer.update(observations)
if self.actor_normalizer.count > 10:
observations = self.actor_normalizer(observations)
self.update_distribution(observations) self.update_distribution(observations)
return self.distribution.sample() return self.distribution.sample()
def get_actions_log_prob(self, actions): def get_actions_log_prob(self, actions):
return self.distribution.log_prob(actions).sum(dim=-1) return self.distribution.log_prob(actions).sum(dim=-1)
def act_inference(self, observations,obs_history): def act_inference(self, observations, obs_history):
code,_,decode,_,_,_,_ = self.cenet_forward(obs_history) code, _, decode, _, _, _, _ = self.cenet_forward(obs_history)
observations = torch.cat((code,observations),dim=-1) observations = torch.cat((code, observations), dim=-1)
if self.actor_normalizer.count > 10:
observations = self.actor_normalizer(observations)
actions_mean = self.actor(observations) actions_mean = self.actor(observations)
return actions_mean return actions_mean

View File

@@ -20,7 +20,7 @@ import torch
class DwaqVecEnv: class DwaqVecEnv:
"""Wraps a MotrixLab DreamWaQ NpEnv for the upstream DreamWaQ rsl_rl runner.""" """Wraps a MotrixLab DreamWaQ NpEnv for the upstream DreamWaQ rsl_rl runner."""
def __init__(self, env, device, num_obs=45, num_privileged_obs=235, def __init__(self, env, device, num_obs=45, num_privileged_obs=247,
num_obs_hist=5, num_actions=12, clip_actions=100.0): num_obs_hist=5, num_actions=12, clip_actions=100.0):
self._env = env self._env = env
self.device = device self.device = device

View File

@@ -158,12 +158,19 @@ class OnPolicyRunner:
f"rew={rew:.2f} eplen={elen:.0f}") f"rew={rew:.2f} eplen={elen:.0f}")
def save(self, path, infos=None): def save(self, path, infos=None):
torch.save({ save_dict = {
"model_state_dict": self.alg.actor_critic.state_dict(), "model_state_dict": self.alg.actor_critic.state_dict(),
"optimizer_state_dict": self.alg.optimizer.state_dict(), "optimizer_state_dict": self.alg.optimizer.state_dict(),
"iter": self.current_learning_iteration, "iter": self.current_learning_iteration,
"infos": infos, "infos": infos,
}, path) }
# Persist RunningStats normalizer state for inference
if hasattr(self.alg.actor_critic, 'actor_normalizer'):
n = self.alg.actor_critic.actor_normalizer
save_dict["normalizer_count"] = n.count
save_dict["normalizer_mean"] = n.mean
save_dict["normalizer_var"] = n.var
torch.save(save_dict, path)
def load(self, path, load_optimizer=True): def load(self, path, load_optimizer=True):
loaded_dict = torch.load(path, map_location=self.device) loaded_dict = torch.load(path, map_location=self.device)
@@ -171,6 +178,12 @@ class OnPolicyRunner:
if load_optimizer: if load_optimizer:
self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"]) self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"])
self.current_learning_iteration = loaded_dict["iter"] self.current_learning_iteration = loaded_dict["iter"]
# Restore RunningStats normalizer state
if "normalizer_count" in loaded_dict and hasattr(self.alg.actor_critic, 'actor_normalizer'):
n = self.alg.actor_critic.actor_normalizer
n.count.copy_(loaded_dict["normalizer_count"])
n.mean.copy_(loaded_dict["normalizer_mean"])
n.var.copy_(loaded_dict["normalizer_var"])
return loaded_dict["infos"] return loaded_dict["infos"]
def get_inference_policy(self, device=None): def get_inference_policy(self, device=None):

View File

@@ -14,6 +14,7 @@
import torch import torch
import torch.nn as nn import torch.nn as nn
from torch.distributions import Normal
from tensordict import TensorDict from tensordict import TensorDict
from rsl_rl.models.mlp_model import MLPModel from rsl_rl.models.mlp_model import MLPModel
@@ -113,23 +114,43 @@ class CENetActorModel(MLPModel):
# 在 nn.Module.__init__ 之后创建 VAE 子模块 # 在 nn.Module.__init__ 之后创建 VAE 子模块
self.vae = CENetVAE(cenet_in_dim, cenet_out_dim, activation) self.vae = CENetVAE(cenet_in_dim, cenet_out_dim, activation)
self.action_clip = 4.0
self.std_clip = 0.6
self._last_cenet_output = None self._last_cenet_output = None
# AdaBoot: 自适应 bootstrapping论文 Section II-C # AdaBoot: 自适应 bootstrapping论文 Section II-C
self._adaboot_cv_buffer = [] # 速度估计误差的 CV 历史 self._adaboot_cv_buffer = [] # 速度估计误差的 CV 历史
self._adaboot_prob = 1.0 # 当前 bootstrap 概率1.0 = 完全信任 GT self._adaboot_prob = 1.0 # 当前 bootstrap 概率1.0 = 完全信任 GT
def _update_distribution(self, obs: torch.Tensor) -> None: def _update_distribution(self, obs: torch.Tensor) -> None:
"""覆盖父类 — 强制 std > 0 再创建 Normal 分布(防止 NaN""" """覆盖父类 — 限制动作分布,避免采样动作进入 PD/关节限位饱和区"""
# 先 clamp std再调父类创建分布 mean = torch.clamp(self.mlp(obs), -self.action_clip, self.action_clip)
if self.stochastic and not self.state_dependent_std: if self.stochastic and not self.state_dependent_std:
with torch.no_grad(): with torch.no_grad():
if self.noise_std_type == "scalar": if self.noise_std_type == "scalar":
self.std.nan_to_num_(nan=0.5, posinf=1.0, neginf=1.0) self.std.nan_to_num_(nan=0.5, posinf=1.0, neginf=1.0)
self.std.clamp_(min=1e-6) self.std.clamp_(min=1e-6, max=self.std_clip)
elif self.noise_std_type == "log": elif self.noise_std_type == "log":
self.log_std.nan_to_num_(nan=0.0, posinf=5.0, neginf=-5.0) self.log_std.nan_to_num_(nan=0.0, posinf=5.0, neginf=-5.0)
self.log_std.clamp_(min=-20.0, max=10.0) self.log_std.clamp_(min=-20.0, max=torch.log(torch.tensor(self.std_clip)).item())
super()._update_distribution(obs) if self.noise_std_type == "scalar":
std = self.std.expand_as(mean)
elif self.noise_std_type == "log":
std = torch.exp(self.log_std).expand_as(mean)
else:
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}.")
else:
std = torch.full_like(mean, self.std_clip)
self.distribution = Normal(mean, std)
def forward(
self,
obs: TensorDict,
masks: torch.Tensor | None = None,
hidden_state: HiddenState = None,
stochastic_output: bool = False,
) -> torch.Tensor:
actions = super().forward(obs, masks, hidden_state, stochastic_output)
return torch.clamp(actions, -self.action_clip, self.action_clip)
def _get_latent_dim(self) -> int: def _get_latent_dim(self) -> int:
"""Actor 实际输入code(19) + policy(45) = 64。 """Actor 实际输入code(19) + policy(45) = 64。
@@ -164,10 +185,10 @@ class CENetActorModel(MLPModel):
code = torch.cat([code_vel, code[:, 3:]], dim=-1) code = torch.cat([code_vel, code[:, 3:]], dim=-1)
# 防止 VAE NaN 传播到下游 # 防止 VAE NaN 传播到下游
if torch.isnan(code).any(): if not torch.isfinite(code).all():
code = torch.nan_to_num(code, nan=0.0) code = torch.nan_to_num(code, nan=0.0, posinf=0.0, neginf=0.0)
if torch.isnan(policy_obs).any(): if not torch.isfinite(policy_obs).all():
policy_obs = torch.nan_to_num(policy_obs, nan=0.0) policy_obs = torch.nan_to_num(policy_obs, nan=0.0, posinf=0.0, neginf=0.0)
latent = torch.cat([code, policy_obs], dim=-1) # (N, 64) latent = torch.cat([code, policy_obs], dim=-1) # (N, 64)
return latent return latent

View File

@@ -81,8 +81,17 @@ class DreamWaQPPO(PPO):
returns_batch, old_actions_log_prob_batch, old_mu_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch,
old_sigma_batch, hid_states_batch, masks_batch, old_sigma_batch, hid_states_batch, masks_batch,
) in generator: ) in generator:
# NaN 检测 — 数据有 NaN 就跳过这个 batch # 多层有限值检测 — 数据异常就跳过这个 batch,避免污染网络参数。
if torch.isnan(obs_batch["policy"]).any() or torch.isnan(obs_batch["obs_history"]).any(): if (not torch.isfinite(obs_batch["policy"]).all()
or not torch.isfinite(obs_batch["obs_history"]).all()
or ("privileged_obs" in obs_batch and not torch.isfinite(obs_batch["privileged_obs"]).all())
or not torch.isfinite(actions_batch).all()
or not torch.isfinite(target_values_batch).all()
or not torch.isfinite(advantages_batch).all()
or not torch.isfinite(returns_batch).all()
or not torch.isfinite(old_actions_log_prob_batch).all()
or not torch.isfinite(old_mu_batch).all()
or not torch.isfinite(old_sigma_batch).all()):
continue continue
# ── 标准 PPO 前向(各调用一次,复用结果)── # ── 标准 PPO 前向(各调用一次,复用结果)──
self.actor(obs_batch, masks=masks_batch, stochastic_output=True) self.actor(obs_batch, masks=masks_batch, stochastic_output=True)
@@ -106,6 +115,11 @@ class DreamWaQPPO(PPO):
actions_batch, actions_log_prob_batch, actions_batch, actions_log_prob_batch,
old_actions_log_prob_batch, advantages_batch) old_actions_log_prob_batch, advantages_batch)
# NaN guard: 损失值异常 → 跳过此 batch保护模型权重
if (not torch.isfinite(surrogate_loss)
or not torch.isfinite(value_loss)):
continue
# ── KL 自适应 schedule ── # ── KL 自适应 schedule ──
if self.desired_kl is not None and self.schedule == 'adaptive': if self.desired_kl is not None and self.schedule == 'adaptive':
with torch.inference_mode(): with torch.inference_mode():
@@ -136,20 +150,41 @@ class DreamWaQPPO(PPO):
- self.entropy_coef * entropy_loss - self.entropy_coef * entropy_loss
+ autoenc_loss + autoenc_loss
) )
if not torch.isfinite(loss):
continue
# ── 梯度更新 ── # ── 梯度更新 ──
self.optimizer.zero_grad() self.optimizer.zero_grad()
loss.backward() loss.backward()
actor_grads_ok = all(
p.grad is None or torch.isfinite(p.grad).all()
for p in self.actor.parameters()
)
critic_grads_ok = all(
p.grad is None or torch.isfinite(p.grad).all()
for p in self.critic.parameters()
)
if not (actor_grads_ok and critic_grads_ok):
self.optimizer.zero_grad(set_to_none=True)
continue
if self.max_grad_norm is not None: if self.max_grad_norm is not None:
nn.utils.clip_grad_norm_(self.actor.parameters(), self.max_grad_norm) try:
nn.utils.clip_grad_norm_(self.critic.parameters(), self.max_grad_norm) nn.utils.clip_grad_norm_(
self.actor.parameters(), self.max_grad_norm, error_if_nonfinite=True)
nn.utils.clip_grad_norm_(
self.critic.parameters(), self.max_grad_norm, error_if_nonfinite=True)
except RuntimeError:
self.optimizer.zero_grad(set_to_none=True)
continue
nn.utils.clip_grad_value_(self.actor.parameters(), 10.0) nn.utils.clip_grad_value_(self.actor.parameters(), 10.0)
self.optimizer.step() self.optimizer.step()
# 每次更新后强制 std > 0防止数值异常导致 NaN # 每次更新后强制 std > 0防止数值异常导致 NaN
if hasattr(self.actor, 'std') and self.actor.stochastic: if hasattr(self.actor, 'std') and self.actor.stochastic:
with torch.no_grad(): with torch.no_grad():
self.actor.std.nan_to_num_(nan=0.5, posinf=1.0, neginf=1.0) if not torch.isfinite(self.actor.std).all():
self.actor.std.clamp_(min=1e-6) self.actor.std.data.fill_(0.5)
std_clip = getattr(self.actor, "std_clip", 0.6)
self.actor.std.data.clamp_(min=1e-6, max=std_clip)
# ── 累计日志 ── # ── 累计日志 ──
mean_value_loss += value_loss.item() mean_value_loss += value_loss.item()

View File

@@ -4,7 +4,7 @@
CENet (VAE): history(225) → [128,64] → latent(16) + vel_est(3) = code(19) CENet (VAE): history(225) → [128,64] → latent(16) + vel_est(3) = code(19)
Decoder: code(19) → [64,128] → next_obs(45) Decoder: code(19) → [64,128] → next_obs(45)
Actor: code(19) + obs(45) = 64 → [512,256,128] → action(12) Actor: code(19) + obs(45) = 64 → [512,256,128] → action(12)
Critic: privileged_obs(235) → [512,256,128] → value(1) Critic: privileged_obs(247) → [512,256,128] → value(1)
VAE Loss: reconstruction_MSE + velocity_MSE + beta * KL VAE Loss: reconstruction_MSE + velocity_MSE + beta * KL
""" """
@@ -189,7 +189,7 @@ class DreamWaQWrapper:
@property @property
def privileged_obs(self): def privileged_obs(self):
return self._env._state.info.get("privileged_obs", return self._env._state.info.get("privileged_obs",
np.zeros((self._num_envs, 235), dtype=np.float32)) np.zeros((self._num_envs, 247), dtype=np.float32))
@property @property
def obs_history(self): def obs_history(self):
@@ -398,7 +398,7 @@ class DreamWaQTrainer:
def __call__(self, inputs, role): def __call__(self, inputs, role):
kernel_init = nn.initializers.orthogonal(jnp.sqrt(2)) kernel_init = nn.initializers.orthogonal(jnp.sqrt(2))
x = inputs["states"] x = inputs["states"]
# Critic: obs(45) + base_vel(3) + heights(187) = 235 # Critic: obs(45) + base_vel(3) + heights(187) = 247
# Layout: [code(19) | obs(45) | base_vel(3) | heights(187)] # Layout: [code(19) | obs(45) | base_vel(3) | heights(187)]
x_c = jnp.concatenate([x[:, 19:64], x[:, 64:254]], axis=-1) x_c = jnp.concatenate([x[:, 19:64], x[:, 64:254]], axis=-1)
for d in value_cfg.hiddens: for d in value_cfg.hiddens:

View File

@@ -112,7 +112,7 @@ class rslrl:
# Runner 设置(严格对齐上游 LeggedRobotCfgPPO + Go1RoughCfgPPO # Runner 设置(严格对齐上游 LeggedRobotCfgPPO + Go1RoughCfgPPO
runner.seed = 5 # 上游 seed=5 runner.seed = 5 # 上游 seed=5
runner.max_iterations = 3000 # 续训到 3000 runner.max_iterations = 5000
runner.num_steps_per_env = 24 runner.num_steps_per_env = 24
runner.experiment_name = "go1_dreamwaq_walk" runner.experiment_name = "go1_dreamwaq_walk"
runner.save_interval = 50 runner.save_interval = 50

View File

@@ -0,0 +1,458 @@
#!/usr/bin/env python3
"""地形碰撞诊断工具 — 系统性检测 hfield PNG 地形的物理交互质量。
检测项:
1. 脚部穿透foot z < terrain z
2. 接触力异常(为 0 但应该接触 / 过大)
3. NaN/Inf 传播
4. 物理爆炸(关节速度飙升)
5. 机器人卡住(速度接近 0 但有命令)
6. 躯干沉入地形base z < terrain z
用法:
uv run scripts/diag_terrain_collision.py # 全量检测
uv run scripts/diag_terrain_collision.py --level 5 # 仅 level 5
uv run scripts/diag_terrain_collision.py --type stairs # 仅楼梯地形
uv run scripts/diag_terrain_collision.py --render # 渲染可视化
"""
import argparse, os, sys, time
import numpy as np
os.environ.setdefault("XLA_PYTHON_CLIENT_PREALLOCATE", "false")
os.environ.setdefault("JAX_PLATFORMS", "cpu")
os.environ.setdefault("DREAMWAQ_TERRAIN", "pyramid")
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import motrix_envs.locomotion.go1.dreamwaq # noqa: F401
from motrix_envs import registry as env_registry
# ═══════════════════════════════════════════════════════════════════════
# 配置
# ═══════════════════════════════════════════════════════════════════════
NUM_ROWS = 10
NUM_COLS = 20
CELL_M = 8.0
BORDER_M = 5.0
SETTLE_STEPS = 100 # 自由落体稳定步数200Hz
TEST_STEPS = 400 # 测试步数
STEPS_PER_CELL = 5 # 每个 cell 测试的 spawn 位置数
# 地形类型名(与 gen_dreamwaq_terrain.py PROPORTIONS 对应)
TYPE_NAMES = ["平滑斜坡", "粗糙斜坡", "下行楼梯", "上行楼梯", "离散障碍"]
CUM = [0.1, 0.2, 0.55, 0.9, 1.0]
def _cell_origin(row, col):
half_x = BORDER_M + NUM_COLS * CELL_M / 2.0
half_y = BORDER_M + NUM_ROWS * CELL_M / 2.0
cx = -half_x + BORDER_M + col * CELL_M + CELL_M / 2
cy = half_y - BORDER_M - row * CELL_M - CELL_M / 2
return cx, cy
def _terrain_type(col):
choice = col / NUM_COLS + 0.001
for i, c in enumerate(CUM):
if choice < c:
return i
return len(TYPE_NAMES) - 1
# ═══════════════════════════════════════════════════════════════════════
# 诊断核心
# ═══════════════════════════════════════════════════════════════════════
class CollisionDiag:
def __init__(self, env, render=False):
self.env = env
self.render = render
self.renderer = None
if render:
from motrix_envs.np.renderer import NpRenderer
self.renderer = NpRenderer(env)
# 统计数据
self.stats = {
"penetrations": [], # (row, col, ttype, foot_z - terrain_z)
"contact_anomalies": [], # (row, col, ttype, max_contact_force)
"nan_events": [], # (row, col, ttype, step)
"explosions": [], # (row, col, ttype, max_dof_vel)
"stuck_events": [], # (row, col, ttype, speed)
"base_sink": [], # (row, col, ttype, base_z - terrain_z)
"ok_count": 0,
"total_tested": 0,
}
def _get_terrain_z(self, x, y):
"""查询 (x, y) 处的地形高度。"""
hm = self.env._hm_cache
if hm is None:
return 0.0
nr, nc = hm.shape
xmin = self.env._h_xmin
ymax = self.env._h_ymax
w = self.env._h_xmax - xmin
h = ymax - self.env._h_ymin
z_base = self.env._h_zbase
col = int(np.clip((x - xmin) / max(w, 1e-6) * (nc - 1), 0, nc - 1))
row = int(np.clip((ymax - y) / max(h, 1e-6) * (nr - 1), 0, nr - 1))
return float(hm[row, col]) + z_base
def _get_foot_positions(self, state):
"""获取所有足部在世界坐标系中的位置 (N, 4, 3)。"""
data = state.data
n = data.shape[0]
feet = np.zeros((n, 4, 3), dtype=np.float32)
foot_names = ["FR_foot", "FL_foot", "RR_foot", "RL_foot"]
for i, name in enumerate(foot_names):
gidx = self.env._model.get_geom_index(name)
if gidx is not None:
pose = self.env._model.geoms[gidx].get_pose(data)
feet[:, i, :] = pose[:, :3]
return feet
def test_cell(self, row, col, level):
"""在单个 cell 上测试多个 spawn 位置。"""
cx, cy = _cell_origin(row, col)
ttype = _terrain_type(col)
tname = TYPE_NAMES[ttype]
# 在 cell 内取几个 spawn 点(中央 + 四角附近)
offsets = [
(0, 0),
(CELL_M * 0.3, 0), (-CELL_M * 0.3, 0),
(0, CELL_M * 0.3), (0, -CELL_M * 0.3),
][:STEPS_PER_CELL]
for ox, oy in offsets:
sx, sy = cx + ox, cy + oy
terrain_z = float(self.env._sample_terrain_height(
np.array([[sx, sy]], dtype=np.float32), radius=0.35)[0])
self.stats["total_tested"] += 1
self._test_single(sx, sy, terrain_z, row, col, level, ttype, tname)
def _test_single(self, sx, sy, terrain_z, row, col, level, ttype, tname):
"""单个 spawn 点的完整测试。"""
env = self.env
data = env._state.data
n = 1
# Spawn
init_pos = env._init_dof_pos.copy().reshape(1, -1)
init_pos[0, 0] = sx
init_pos[0, 1] = sy
spawn_z = terrain_z + 0.45 # 标准 clearance
init_pos[0, 2] = spawn_z
yaw = np.random.uniform(-np.pi, np.pi)
init_pos[0, 3:7] = [0, 0, np.sin(yaw / 2), np.cos(yaw / 2)]
data.reset(env._model)
data.set_dof_pos(init_pos, env._model)
env._model.forward_kinematic(data)
# 给一个随机动作防止完全静止(小幅度 PD 控制)
action = 0.1 * (np.random.randn(n, 12).astype(np.float32))
has_nan = False
max_dof_vel = 0.0
max_contact_force = 0.0
min_foot_z = spawn_z
min_base_z = spawn_z
total_speed = 0.0
for step in range(SETTLE_STEPS + TEST_STEPS):
# Apply PD
actions_scaled = action * env.cfg.control_config.action_scale
target = actions_scaled + env.default_angles.reshape(1, -1)
lo = env._model.joint_limits[0].reshape(1, -1)
hi = env._model.joint_limits[1].reshape(1, -1)
target = np.clip(target, lo, hi)
torques = env.kps * (target - env.get_dof_pos(data)) - env.kds * env.get_dof_vel(data)
data.actuator_ctrls = np.clip(torques, -80.0, 80.0)
env._model.step(data)
# ── 检测 ──
# 1. NaN 检测
dv = np.array(data.dof_vel)
dp = np.array(data.dof_pos)
if np.any(~np.isfinite(dv)) or np.any(~np.isfinite(dp)):
self.stats["nan_events"].append((row, col, ttype, step))
has_nan = True
break
# 2. 速度爆炸
cur_max_vel = float(np.max(np.abs(dv[:, 6:])))
max_dof_vel = max(max_dof_vel, cur_max_vel)
if cur_max_vel > 100.0:
self.stats["explosions"].append((row, col, ttype, cur_max_vel))
break
# 3. 接触力
# Read contact forces via MotrixSim sensor API
cf_vals = []
for foot in ["FR", "FL", "RR", "RL"]:
try:
v = env._model.get_sensor_value(f"{foot}_foot_contact", data)
cf_vals.append(v[0])
except Exception:
cf_vals.append(np.zeros(3, dtype=np.float32))
cf_raw = np.concatenate(cf_vals)
cur_cf = float(np.max(np.abs(cf_raw)))
max_contact_force = max(max_contact_force, cur_cf)
# 4. 足部穿透settle 之后测)
if step >= SETTLE_STEPS:
feet = self._get_foot_positions(env._state)
for fi in range(4):
fz = float(feet[0, fi, 2])
tz = self._get_terrain_z(float(feet[0, fi, 0]), float(feet[0, fi, 1]))
penetration = tz - fz # 正 = 穿透
min_foot_z = min(min_foot_z, fz - tz) # 相对地形的高度
if penetration > 0.02: # > 2cm 穿透
self.stats["penetrations"].append(
(row, col, ttype, float(penetration)))
# 5. 躯干沉入
base_pose = env._body.get_pose(data)
bz = float(base_pose[0, 2])
tz = self._get_terrain_z(float(base_pose[0, 0]), float(base_pose[0, 1]))
min_base_z = min(min_base_z, bz - tz)
if bz < tz + 0.10 and step >= SETTLE_STEPS: # 躯干离地面 < 10cm
self.stats["base_sink"].append((row, col, ttype, float(bz - tz)))
# 6. 速度(卡住检测)
if step >= SETTLE_STEPS:
linvel = env.get_local_linvel(data)[0]
total_speed += float(np.linalg.norm(linvel[:2]))
if self.renderer and step % 20 == 0:
self.renderer.render()
time.sleep(0.005)
# ── 统计 ──
if not has_nan and max_dof_vel < 100.0:
avg_speed = total_speed / max(TEST_STEPS, 1)
if avg_speed < 0.01 and step == SETTLE_STEPS + TEST_STEPS - 1:
self.stats["stuck_events"].append((row, col, ttype, avg_speed))
if not has_nan and max_dof_vel < 50.0 and min_base_z > 0.05:
self.stats["ok_count"] += 1
# 记录接触力异常(完全无接触力但足部应该着地)
if max_contact_force < 1e-3 and step == SETTLE_STEPS + TEST_STEPS - 1:
feet = self._get_foot_positions(env._state)
for fi in range(4):
fz = float(feet[0, fi, 2])
tz = self._get_terrain_z(float(feet[0, fi, 0]), float(feet[0, fi, 1]))
if abs(fz - tz) < 0.05: # 足部接近地面
self.stats["contact_anomalies"].append(
(row, col, ttype, 0.0)) # 0 = 无接触力
break
def run(self, target_level=None, target_type=None):
"""遍历所有地形 cell 测试。"""
total = 0
start_t = time.time()
for row in range(NUM_ROWS):
if target_level is not None and row != target_level:
continue
for col in range(NUM_COLS):
ttype = _terrain_type(col)
if target_type is not None and ttype != target_type:
continue
total += 1
print(f"[Diag] 测试 {NUM_ROWS}×{NUM_COLS} 网格, "
f"每 cell {STEPS_PER_CELL} 个 spawn 点 = {total * STEPS_PER_CELL}")
print(f"[Diag] Settle={SETTLE_STEPS}步 Test={TEST_STEPS}")
count = 0
for row in range(NUM_ROWS):
if target_level is not None and row != target_level:
continue
for col in range(NUM_COLS):
ttype = _terrain_type(col)
if target_type is not None and ttype != target_type:
continue
self.test_cell(row, col, row)
count += 1
if count % 10 == 0:
elapsed = time.time() - start_t
print(f" [{count}/{total}] {elapsed:.1f}s "
f"OK={self.stats['ok_count']}/{self.stats['total_tested']} "
f"穿透={len(self.stats['penetrations'])} "
f"NaN={len(self.stats['nan_events'])} "
f"爆炸={len(self.stats['explosions'])}")
elapsed = time.time() - start_t
self._report(elapsed)
def _report(self, elapsed):
s = self.stats
t = s["total_tested"]
print("\n" + "=" * 70)
print(f"地形碰撞诊断报告 ({elapsed:.1f}s, {t} 次测试)")
print("=" * 70)
# 总体统计
ok_rate = s["ok_count"] / max(t, 1) * 100
print(f"\n ✅ 通过: {s['ok_count']}/{t} ({ok_rate:.1f}%)")
# 穿透
if s["penetrations"]:
pens = s["penetrations"]
pvals = [p[3] for p in pens]
print(f"\n 🔴 足部穿透: {len(pens)}")
print(f" 最大穿透: {max(pvals):.3f}m 平均: {np.mean(pvals):.3f}m")
# 按地形类型分组
by_type = {}
for p in pens:
tn = TYPE_NAMES[p[2]]
by_type.setdefault(tn, []).append(p[3])
for tn, vals in sorted(by_type.items()):
print(f" {tn}: {len(vals)}次 max={max(vals):.3f}m avg={np.mean(vals):.3f}m")
# 按 level 分组
by_level = {}
for p in pens:
by_level.setdefault(p[0], []).append(p[3])
print(f" 按 level: ", end="")
for lv in sorted(by_level.keys()):
vals = by_level[lv]
print(f"L{lv}({len(vals)}x max={max(vals):.3f}) ", end="")
print()
else:
print(f"\n 🟢 足部穿透: 0 次 ✓")
# NaN
if s["nan_events"]:
print(f"\n 🔴 NaN/Inf: {len(s['nan_events'])}")
for n in s["nan_events"][:5]:
print(f" L{n[0]} C{n[1]:02d} {TYPE_NAMES[n[2]]} @step {n[3]}")
else:
print(f" 🟢 NaN/Inf: 0 次 ✓")
# 物理爆炸
if s["explosions"]:
print(f"\n 🟡 速度爆炸 (>{100}rad/s): {len(s['explosions'])}")
for e in s["explosions"][:5]:
print(f" L{e[0]} C{e[1]:02d} {TYPE_NAMES[e[2]]} max_vel={e[3]:.1f}")
else:
print(f" 🟢 速度爆炸: 0 次 ✓")
# 接触力异常
if s["contact_anomalies"]:
print(f"\n 🟡 接触力异常 (无接触但足部近地): {len(s['contact_anomalies'])}")
else:
print(f" 🟢 接触力异常: 0 次 ✓")
# 卡住
if s["stuck_events"]:
print(f"\n 🟡 卡住 (速度<0.01m/s): {len(s['stuck_events'])}")
by_type = {}
for e in s["stuck_events"]:
tn = TYPE_NAMES[e[2]]
by_type.setdefault(tn, []).append(1)
for tn, vals in sorted(by_type.items()):
print(f" {tn}: {len(vals)}")
else:
print(f" 🟢 卡住: 0 次 ✓")
# 躯干沉入
if s["base_sink"]:
sinks = s["base_sink"]
print(f"\n 🟡 躯干贴地 (<10cm): {len(sinks)}")
vals = [s[3] for s in sinks]
print(f" 最差: {min(vals):.3f}m 平均: {np.mean(vals):.3f}m")
else:
print(f" 🟢 躯干贴地: 0 次 ✓")
# 按地形类型汇总
print(f"\n── 按地形类型汇总 ──")
for ti, tn in enumerate(TYPE_NAMES):
pens_t = sum(1 for p in s["penetrations"] if p[2] == ti)
nans_t = sum(1 for n in s["nan_events"] if n[2] == ti)
exps_t = sum(1 for e in s["explosions"] if e[2] == ti)
icon = "🔴" if (pens_t + nans_t + exps_t) > 0 else "🟢"
print(f" {icon} {tn}: 穿透{pens_t} NaN{nans_t} 爆炸{exps_t}")
# 结论
print(f"\n── 全局结论 ──")
if ok_rate > 95 and len(s["nan_events"]) == 0 and len(s["penetrations"]) < 5:
print(f" ✅ 仿真环境正常 — hfield 地形碰撞基本可靠")
elif ok_rate > 80:
print(f" ⚠ 仿真环境存在一些问题 — 部分地形类型/level 有问题")
else:
print(f" 🔴 仿真环境问题严重 — 需要排查 MotrixSim hfield 实现")
if len(s["penetrations"]) > 10:
print(f" 💡 穿透较多 → hfield 碰撞面可能偏软或 contact margin 过大")
print(f" 可尝试增大 geom margin 或检查 MotrixSim 碰撞参数")
# ═══════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════
def main():
p = argparse.ArgumentParser(description="DreamWaQ 地形碰撞诊断")
p.add_argument("--level", type=int, default=None, help="仅测试指定 level (0-9)")
p.add_argument("--type", dest="ttype", default=None,
choices=["slope", "rough", "stairs_down", "stairs_up", "obstacles"],
help="仅测试指定地形类型")
p.add_argument("--render", action="store_true", help="渲染可视化")
p.add_argument("--settle", type=int, default=SETTLE_STEPS)
p.add_argument("--test", type=int, default=TEST_STEPS)
args = p.parse_args()
type_map = {"slope": 0, "rough": 1, "stairs_down": 2, "stairs_up": 3, "obstacles": 4}
target_type = type_map.get(args.ttype)
print("[Diag] 创建 DreamWaQ 环境...")
env = env_registry.make("go1-dreamwaq-walk", num_envs=1)
# 如果指定 level强制执行
if args.level is not None:
env._force_level = args.level
print(f"[Diag] 强制 level={args.level}")
env.init_state()
# 确保 terrain 初始化(触发 hfield cache
if env._model.num_hfields > 0:
hf = env._model.get_hfield(0)
env._hm_cache = hf.height_matrix
env._h_xmin, env._h_ymin = hf.bound[0], hf.bound[1]
env._h_xmax, env._h_ymax = hf.bound[3], hf.bound[4]
env._h_nr, env._h_nc = hf.height_matrix.shape
cfg_base = getattr(env.cfg, "hfield_z_base", None)
env._h_zbase = float(cfg_base) if cfg_base is not None else float(-hf.height_matrix[0, 0])
print(f"[Diag] HField: {hf.height_matrix.shape} bound={hf.bound}")
print(f"[Diag] z_base={env._h_zbase:.3f}")
else:
print("[Diag] WARNING: 无 hfieldflat 场景),跳过诊断")
return
diag = CollisionDiag(env, render=args.render)
diag.SETTLE_STEPS = args.settle
diag.TEST_STEPS = args.test
try:
diag.run(target_level=args.level, target_type=target_type)
except KeyboardInterrupt:
print("\n[Diag] 中断")
finally:
if diag.renderer:
diag.renderer.close()
print("[Diag] 完成")
if __name__ == "__main__":
main()

View File

@@ -31,7 +31,8 @@ HISTORY_LEN = 5
ACTION_SCALE = 0.25 ACTION_SCALE = 0.25
KP = 28.0 KP = 28.0
KD = 0.7 KD = 0.7
CLIP_ACTIONS = 23.7 CLIP_ACTIONS = 4.0
CLIP_TORQUES = 80.0
CLIP_OBS = 100.0 CLIP_OBS = 100.0
MAX_VX, MAX_VY, MAX_WZ = 1.0, 1.0, 1.0 MAX_VX, MAX_VY, MAX_WZ = 1.0, 1.0, 1.0
@@ -122,7 +123,7 @@ def main():
# Select XML scene # Select XML scene
terrain_map = { terrain_map = {
"flat": "scene_motor_actuator.xml", "flat": "scene_dreamwaq_flat.xml",
"rough": "scene_rough_terrain.xml", "rough": "scene_rough_terrain.xml",
"stairs": "scene_stairs_terrain.xml", "stairs": "scene_stairs_terrain.xml",
"dreamwaq": "scene_dreamwaq_terrain.xml", "dreamwaq": "scene_dreamwaq_terrain.xml",
@@ -243,10 +244,22 @@ def main():
action = np.clip(action, -CLIP_ACTIONS, CLIP_ACTIONS) action = np.clip(action, -CLIP_ACTIONS, CLIP_ACTIONS)
last_action = action.copy() last_action = action.copy()
# PD control # PD control(对齐训练:目标限位 + 力矩裁剪)
target = DEFAULT_ANGLES + action * ACTION_SCALE target = DEFAULT_ANGLES + action * ACTION_SCALE
# 关节目标限位(与训练一致)
jnt_lo = model.jnt_range[:, 0].copy() if hasattr(model, 'jnt_range') else None
jnt_hi = model.jnt_range[:, 1].copy() if hasattr(model, 'jnt_range') else None
# MuJoCo model.actuator_trnid 可能不直接暴露,改用 model.jnt_range
try:
trnid = model.actuator_trnid[:, 0] # transmission joint indices
lo = model.jnt_range[trnid, 0]
hi = model.jnt_range[trnid, 1]
except Exception:
lo = np.full(12, -12.0)
hi = np.full(12, 12.0)
target = np.clip(target, lo, hi)
torques = KP * (target - data.qpos[7:19]) - KD * data.qvel[6:18] torques = KP * (target - data.qpos[7:19]) - KD * data.qvel[6:18]
data.ctrl[:] = np.clip(torques, -CLIP_ACTIONS, CLIP_ACTIONS) data.ctrl[:] = np.clip(torques, -CLIP_TORQUES, CLIP_TORQUES)
mujoco.mj_step(model, data) mujoco.mj_step(model, data)
view.sync() view.sync()

View File

@@ -28,7 +28,7 @@ class DwaqInfer(torch.nn.Module):
"""obs: (N,45), obs_history: (N,225) → action: (N,12)""" """obs: (N,45), obs_history: (N,225) → action: (N,12)"""
code = self.vae.deterministic_code(obs_history) # (N,19) code = self.vae.deterministic_code(obs_history) # (N,19)
latent = torch.cat([code, obs], dim=-1) # (N,64) latent = torch.cat([code, obs], dim=-1) # (N,64)
return self.actor_mlp(latent) return torch.clamp(self.actor_mlp(latent), -4.0, 4.0)
def main(): def main():

View File

@@ -22,13 +22,20 @@ PROJECT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
class DwaqInfer(tnn.Module): class DwaqInfer(tnn.Module):
"""Deterministic inference: CENet mean code + actor.""" """Deterministic inference: CENet mean code + RunningStats normalize + actor."""
def __init__(self, ac: ActorCritic_DWAQ): def __init__(self, ac: ActorCritic_DWAQ):
super().__init__() super().__init__()
self.encoder = ac.encoder self.encoder = ac.encoder
self.encode_mean_vel = ac.encode_mean_vel self.encode_mean_vel = ac.encode_mean_vel
self.encode_mean_latent = ac.encode_mean_latent self.encode_mean_latent = ac.encode_mean_latent
self.actor = ac.actor self.actor = ac.actor
# Bake RunningStats into the exported model
if hasattr(ac, 'actor_normalizer') and ac.actor_normalizer.count > 10:
self.register_buffer("norm_mean", ac.actor_normalizer.mean.clone())
self.register_buffer("norm_std", ac.actor_normalizer.var.sqrt().clone())
self.has_norm = True
else:
self.has_norm = False
def forward(self, obs, obs_history): def forward(self, obs, obs_history):
h = self.encoder(obs_history.reshape(obs_history.shape[0], -1)) # (B,225)->(B,64) h = self.encoder(obs_history.reshape(obs_history.shape[0], -1)) # (B,225)->(B,64)
@@ -36,6 +43,8 @@ class DwaqInfer(tnn.Module):
latent = self.encode_mean_latent(h) # (B,16) mean latent latent = self.encode_mean_latent(h) # (B,16) mean latent
code = torch.cat([vel, latent], dim=-1) # (B,19) = [vel, latent] code = torch.cat([vel, latent], dim=-1) # (B,19) = [vel, latent]
x = torch.cat([code, obs], dim=-1) # (B,64) = [code, obs] x = torch.cat([code, obs], dim=-1) # (B,64) = [code, obs]
if self.has_norm:
x = torch.clamp((x - self.norm_mean) / (self.norm_std + 1e-8), -5.0, 5.0)
return self.actor(x) # (B,12) return self.actor(x) # (B,12)

103
scripts/play_amp_mujoco.py Normal file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Deploy Go1 AMP model in MuJoCo."""
import mujoco, numpy as np, os, torch, time, sys
from mujoco import viewer
# Use AMP's rsl_rl (NOT MotrixLab's)
sys.path.insert(0, '/home/8x54zj-m/amp_go2/rsl_rl')
sys.path.insert(0, '/home/8x54zj-m/amp_go2/legged_gym')
# Force reload of rsl_rl to pick up AMP version
for mod in list(sys.modules.keys()):
if 'rsl_rl' in mod:
del sys.modules[mod]
XML_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'motrix_envs/src/motrix_envs/locomotion/go1/xmls')
CKPT = '/home/8x54zj-m/amp_go2/legged_gym/logs/go1_amp/Jul05_01-15-41_flat/model_6000.pt'
import argparse
p = argparse.ArgumentParser()
p.add_argument("--terrain", default="flat", choices=["flat", "flat_stairs"])
args = p.parse_args()
scene = "scene_flat_stairs.xml" if args.terrain == "flat_stairs" else "go1_motor_actuator.xml"
os.chdir(XML_DIR)
m = mujoco.MjModel.from_xml_string(open(scene).read())
d = mujoco.MjData(m)
# Spawn on level 1 platform if using flat_stairs
spawn_x, spawn_y, spawn_z = 0.0, 0.0, 0.45
if args.terrain == "flat_stairs":
col = np.random.randint(0, 4)
spawn_x = -12.0 + col * 8.0
spawn_y = -4.0 # level 1 center
# Sample terrain height at spawn
floor_id = mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_GEOM, "floor")
if floor_id >= 0 and m.geom_type[floor_id] == mujoco.mjtGeom.mjGEOM_HFIELD:
hf = m.geom_dataid[floor_id]
nr, nc = int(m.hfield_nrow[hf]), int(m.hfield_ncol[hf])
sx, sy, zt, sb = m.hfield_size[hf]
hd = m.hfield_data[m.hfield_adr[hf]:m.hfield_adr[hf]+nr*nc].reshape(nr, nc)
gp = m.geom_pos[floor_id]
col_idx = int(np.clip(((spawn_x-gp[0])/sx*0.5+0.5)*(nc-1), 0, nc-1))
row_idx = int(np.clip(((spawn_y-gp[1])/sy*0.5+0.5)*(nr-1), 0, nr-1))
spawn_z = float(gp[2] + sb + hd[row_idx, col_idx] * zt) + 0.45
print(f"[Terrain] flat_stairs level=1 spawn=({spawn_x:.1f},{spawn_y:.1f},{spawn_z:.2f})")
ckpt = torch.load(CKPT, map_location='cpu', weights_only=False)
normalizer = ckpt['amp_normalizer']
actor = torch.nn.Sequential(
torch.nn.Linear(45, 512), torch.nn.ELU(),
torch.nn.Linear(512, 256), torch.nn.ELU(),
torch.nn.Linear(256, 128), torch.nn.ELU(),
torch.nn.Linear(128, 12)
)
sd = {k.replace('actor.',''): v for k,v in ckpt['model_state_dict'].items() if k.startswith('actor.')}
actor.load_state_dict(sd)
actor.eval()
DEFAULT = np.array([-0.1,0.8,-1.5, 0.1,0.8,-1.5, -0.1,1.0,-1.5, 0.1,1.0,-1.5], dtype=np.float32)
LIN_VEL_SCALE, ANG_VEL_SCALE = 2.0, 0.25
DOF_POS_SCALE, DOF_VEL_SCALE = 1.0, 0.05
CMD_SCALE = np.array([2.0, 2.0, 0.25], dtype=np.float32)
KP, KD, ACT_SCALE = 20.0, 0.5, 0.25
def get_gravity(q):
qw,qx,qy,qz = q[3],q[0],q[1],q[2]
return np.array([2*(-qz*qx+qw*qy), -2*(qz*qy+qw*qx), 1-2*(qw*qw+qz*qz)])
def compute_obs(d, last_action, cmd):
obs = np.zeros(45, dtype=np.float32)
obs[0:3] = d.qvel[3:6] * ANG_VEL_SCALE
obs[3:6] = get_gravity(d.qpos[3:7])
obs[6:9] = cmd * CMD_SCALE
obs[9:21] = (d.qpos[7:19] - DEFAULT) * DOF_POS_SCALE
obs[21:33] = d.qvel[6:18] * DOF_VEL_SCALE
obs[33:45] = last_action
return obs
d.qpos[7:19] = DEFAULT; d.qpos[0:3] = [spawn_x, spawn_y, spawn_z]; d.qvel[:] = 0
mujoco.mj_forward(m, d)
view = viewer.launch_passive(m, d)
cmd = np.array([0.8, 0.0, 0.0], dtype=np.float32)
last_action = np.zeros(12, dtype=np.float32)
print("AMP Go1 | W/S前后 Q/E左右 A/D旋转 Space停 R重置 Esc退出")
while view.is_running():
if view.is_running():
obs = compute_obs(d, last_action, cmd)
obs_t = torch.from_numpy(obs).unsqueeze(0).float()
obs_norm = obs_t.clone()
obs_norm[:, :43] = normalizer.normalize(obs_t[:, :43])
with torch.no_grad():
act = actor(obs_norm)[0].numpy()
act = np.clip(act, -18, 18)
last_action = act.copy()
target = DEFAULT + act * ACT_SCALE
d.ctrl[:] = KP * (target - d.qpos[7:19]) - KD * d.qvel[6:18]
mujoco.mj_step(m, d)
view.sync()
time.sleep(0.001)

107
scripts/test_abrupt_stop.py Normal file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""无头 MuJoCo 测试:前进 → 突然停止 → 观察姿态变化。"""
import numpy as np
import mujoco
import onnxruntime as ort
import os, sys, time
_PROJECT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
XML_DIR = os.path.join(_PROJECT, "motrix_envs", "src", "motrix_envs", "locomotion", "go1", "xmls")
ONNX = os.path.join(_PROJECT, "runs/go1-dreamwaq-walk/rslrl/26-07-02_20-03-23-_36054_PPO/policy.onnx")
NUM_OBS = 45
NUM_ACTIONS = 12
HISTORY_LEN = 5
ACTION_SCALE = 0.25
KP = 28.0
KD = 0.7
DECIMATION = 4
DEFAULT_ANGLES = np.array([0.0, 0.9, -1.8, 0.0, 0.9, -1.8, 0.0, 0.9, -1.8, 0.0, 0.9, -1.8], dtype=np.float32)
def compute_obs(model, data, commands, last_action):
obs = np.zeros(NUM_OBS, dtype=np.float32)
g = None # no gyro sensor in headless
obs[0:3] = (g if g is not None else data.qvel[3:6]) * 0.25
grav_world = model.opt.gravity.copy()
grav_world = grav_world / np.linalg.norm(grav_world)
R = data.xmat[1].reshape(3, 3)
obs[3:6] = (R.T @ grav_world).astype(np.float32)
obs[6:9] = commands * np.array([2.0, 2.0, 0.25], dtype=np.float32)
obs[9:21] = (data.qpos[7:19] - DEFAULT_ANGLES) * 1.0
obs[21:33] = data.qvel[6:18] * 0.05
obs[33:45] = last_action
return obs
def get_base_pose(data):
"""返回 base 的 z 高度和 roll/pitch"""
quat = data.xquat[1] # base body quaternion
w, x, y, z = quat[0], quat[1], quat[2], quat[3]
# roll, pitch from quaternion
sinr_cosp = 2 * (w * x + y * z)
cosr_cosp = 1 - 2 * (x * x + y * y)
roll = np.arctan2(sinr_cosp, cosr_cosp)
sinp = 2 * (w * y - z * x)
pitch = np.arcsin(np.clip(sinp, -1, 1))
return float(data.xpos[1, 2]), np.degrees(roll), np.degrees(pitch)
def main():
session = ort.InferenceSession(ONNX, providers=['CPUExecutionProvider'])
xml_file = os.path.join(XML_DIR, "scene_dreamwaq_flat.xml")
model = mujoco.MjModel.from_xml_path(xml_file)
data = mujoco.MjData(model)
# 初始化
data.qpos[7:19] = DEFAULT_ANGLES
data.qpos[2] = 0.35
mujoco.mj_forward(model, data)
history = np.zeros((1, HISTORY_LEN, NUM_OBS), dtype=np.float32)
last_action = np.zeros(NUM_ACTIONS, dtype=np.float32)
print(f"{'Step':>5s} {'Cmd_vx':>7s} {'base_z':>8s} {'roll':>8s} {'pitch':>8s} {'speed_xy':>8s}")
print("-" * 60)
for step in range(3000):
# 命令:前 1500 步前进,之后突然停止
if step < 1500:
vx, vy, wz = 0.5, 0.0, 0.0 # 前进
else:
vx, vy, wz = 0.0, 0.0, 0.0 # 突然停止!
for _ in range(DECIMATION):
mujoco.mj_step(model, data)
if step % DECIMATION == 0:
cmd = np.array([vx, vy, wz], dtype=np.float32)
obs = compute_obs(model, data, cmd, last_action)
history = np.concatenate([history[:, 1:, :], obs.reshape(1, 1, -1)], axis=1)
outputs = session.run(None, {
'obs': obs.reshape(1, -1).astype(np.float32),
'obs_history': history.reshape(1, -1).astype(np.float32),
})
action = outputs[0][0]
last_action = action
target = DEFAULT_ANGLES + action * ACTION_SCALE
data.ctrl[:] = KP * (target - data.qpos[7:19]) - KD * data.qvel[6:18]
# 每 100 步打印
if step % 100 == 0:
bz, roll, pitch = get_base_pose(data)
speed_xy = np.linalg.norm(data.qvel[0:2])
print(f"{step:5d} {vx:7.1f} {bz:8.3f} {roll:+8.1f} {pitch:+8.1f} {speed_xy:8.3f}")
# 最终状态
bz, roll, pitch = get_base_pose(data)
print(f"\n最终: base_z={bz:.3f} roll={roll:.1f}° pitch={pitch:.1f}°")
if abs(roll) > 30 or abs(pitch) > 30:
print("⚠ 机器人倾覆!突然停止导致翻跟头")
else:
print("✅ 机器人在突然停止后保持稳定")
if __name__ == "__main__":
main()