Fix terrain curriculum traversal and hfield roughness
This commit is contained in:
@@ -279,6 +279,25 @@ class DreamWaQTask(Go1WalkTask):
|
|||||||
|
|
||||||
# ── 高度测量 ──
|
# ── 高度测量 ──
|
||||||
|
|
||||||
|
def _ensure_hfield_cache(self) -> bool:
|
||||||
|
"""Load hfield metadata before reset-time terrain height queries."""
|
||||||
|
if self._hm_cache is not None:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
hf = self._model.get_hfield(0)
|
||||||
|
self._hf_cache = hf
|
||||||
|
self._hm_cache = hf.height_matrix
|
||||||
|
nr, nc = self._hm_cache.shape
|
||||||
|
b = hf.bound
|
||||||
|
self._h_xmin, self._h_ymin = b[0], b[1]
|
||||||
|
self._h_xmax, self._h_ymax = b[3], b[4]
|
||||||
|
self._h_nr, self._h_nc = nr, nc
|
||||||
|
cfg_base = getattr(self.cfg, "hfield_z_base", None)
|
||||||
|
self._h_zbase = float(cfg_base) if cfg_base is not None else float(-self._hm_cache[0, 0])
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def _get_heights(self, data: mtx.SceneData) -> np.ndarray:
|
def _get_heights(self, data: mtx.SceneData) -> np.ndarray:
|
||||||
n = data.shape[0]
|
n = data.shape[0]
|
||||||
nx, ny = len(self._hx), len(self._hy)
|
nx, ny = len(self._hx), len(self._hy)
|
||||||
@@ -286,25 +305,12 @@ class DreamWaQTask(Go1WalkTask):
|
|||||||
base_pos = pose[:, 0:3]
|
base_pos = pose[:, 0:3]
|
||||||
yaw = quaternion.get_yaw(pose[:, 3:7])
|
yaw = quaternion.get_yaw(pose[:, 3:7])
|
||||||
cos_yaw, sin_yaw = np.cos(yaw), np.sin(yaw)
|
cos_yaw, sin_yaw = np.cos(yaw), np.sin(yaw)
|
||||||
try:
|
if not self._ensure_hfield_cache():
|
||||||
if self._hm_cache is None:
|
return np.zeros((n, nx * ny), dtype=np.float32)
|
||||||
hf = self._model.get_hfield(0)
|
|
||||||
self._hf_cache = hf
|
|
||||||
hm = hf.height_matrix
|
|
||||||
self._hm_cache = hm
|
|
||||||
nr, nc = hm.shape
|
|
||||||
b = hf.bound
|
|
||||||
self._h_xmin, self._h_ymin = b[0], b[1]
|
|
||||||
self._h_xmax, self._h_ymax = b[3], b[4]
|
|
||||||
self._h_nr, self._h_nc = nr, nc
|
|
||||||
cfg_base = getattr(self.cfg, "hfield_z_base", None)
|
|
||||||
self._h_zbase = float(cfg_base) if cfg_base is not None else float(-hm[0, 0])
|
|
||||||
hm, nr, nc = self._hm_cache, self._h_nr, self._h_nc
|
hm, nr, nc = self._hm_cache, self._h_nr, self._h_nc
|
||||||
xmin, ymin, xmax, ymax = self._h_xmin, self._h_ymin, self._h_xmax, self._h_ymax
|
xmin, ymin, xmax, ymax = self._h_xmin, self._h_ymin, self._h_xmax, self._h_ymax
|
||||||
z_base = self._h_zbase
|
z_base = self._h_zbase
|
||||||
w, h = xmax - xmin, ymax - ymin
|
w, h = xmax - xmin, ymax - ymin
|
||||||
except Exception:
|
|
||||||
return np.zeros((n, nx * ny), dtype=np.float32)
|
|
||||||
|
|
||||||
heights = np.zeros((n, nx * ny), dtype=np.float32)
|
heights = np.zeros((n, nx * ny), dtype=np.float32)
|
||||||
idx = 0
|
idx = 0
|
||||||
@@ -324,13 +330,12 @@ class DreamWaQTask(Go1WalkTask):
|
|||||||
|
|
||||||
def _sample_terrain_height(self, xy: np.ndarray, radius: float = 0.0) -> np.ndarray:
|
def _sample_terrain_height(self, xy: np.ndarray, radius: float = 0.0) -> np.ndarray:
|
||||||
n = xy.shape[0]
|
n = xy.shape[0]
|
||||||
try:
|
if not self._ensure_hfield_cache():
|
||||||
|
return np.zeros(n, dtype=np.float32)
|
||||||
hm, nr, nc = self._hm_cache, self._h_nr, self._h_nc
|
hm, nr, nc = self._hm_cache, self._h_nr, self._h_nc
|
||||||
xmin, ymin, xmax, ymax = self._h_xmin, self._h_ymin, self._h_xmax, self._h_ymax
|
xmin, ymin, xmax, ymax = self._h_xmin, self._h_ymin, self._h_xmax, self._h_ymax
|
||||||
z_base = self._h_zbase
|
z_base = self._h_zbase
|
||||||
w, h = xmax - xmin, ymax - ymin
|
w, h = xmax - xmin, ymax - ymin
|
||||||
except Exception:
|
|
||||||
return np.zeros(n, dtype=np.float32)
|
|
||||||
col = np.clip(np.nan_to_num((xy[:, 0] - xmin) / max(w, 1e-6) * (nc - 1), nan=0).astype(np.int32), 0, nc - 1)
|
col = np.clip(np.nan_to_num((xy[:, 0] - xmin) / max(w, 1e-6) * (nc - 1), nan=0).astype(np.int32), 0, nc - 1)
|
||||||
row = np.clip(np.nan_to_num((ymax - xy[:, 1]) / max(h, 1e-6) * (nr - 1), nan=0).astype(np.int32), 0, nr - 1)
|
row = np.clip(np.nan_to_num((ymax - xy[:, 1]) / max(h, 1e-6) * (nr - 1), nan=0).astype(np.int32), 0, nr - 1)
|
||||||
if radius <= 0:
|
if radius <= 0:
|
||||||
@@ -523,12 +528,15 @@ class DreamWaQTask(Go1WalkTask):
|
|||||||
ep_bh = old_info.get("ep_base_height", np.zeros(self._num_envs, dtype=np.float32))[done]
|
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 = np.zeros(num_reset, dtype=np.float32)
|
||||||
avg_bh[mask] = ep_bh[mask] / ep_steps[mask].astype(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²
|
# Require actual traversal of most of the current terrain cell.
|
||||||
|
# Tracking velocity while staying near the spawn point must not
|
||||||
|
# promote the curriculum.
|
||||||
move_up_raw = (
|
move_up_raw = (
|
||||||
(avg_tracking > 0.5)
|
(avg_tracking > 0.5)
|
||||||
& not_fallen
|
& not_fallen
|
||||||
& (avg_orient > -0.0005)
|
& (avg_orient > -0.0005)
|
||||||
& (avg_bh > -0.001)
|
& (avg_bh > -0.001)
|
||||||
|
& (distance > self._cell_size / 2.0)
|
||||||
)
|
)
|
||||||
# ── 渐进升级:连续通过 3 次 + 冷却期 + 每次只升 1 级 ──
|
# ── 渐进升级:连续通过 3 次 + 冷却期 + 每次只升 1 级 ──
|
||||||
if not hasattr(self, '_consecutive_pass_count'):
|
if not hasattr(self, '_consecutive_pass_count'):
|
||||||
@@ -536,9 +544,10 @@ class DreamWaQTask(Go1WalkTask):
|
|||||||
self._pending_upgrade = np.zeros(self._num_envs, dtype=bool)
|
self._pending_upgrade = np.zeros(self._num_envs, dtype=bool)
|
||||||
self._upgrade_cooldown = np.zeros(self._num_envs, dtype=np.int32)
|
self._upgrade_cooldown = np.zeros(self._num_envs, dtype=np.int32)
|
||||||
|
|
||||||
# 每 env: 连续通过 +1, 失败重置(仅更新被 reset 的 env)
|
# Cooldown episodes do not contribute to a new validation streak.
|
||||||
|
cooldown_active = self._upgrade_cooldown[done_idx] > 0
|
||||||
self._consecutive_pass_count[done_idx] = np.where(
|
self._consecutive_pass_count[done_idx] = np.where(
|
||||||
move_up_raw,
|
move_up_raw & ~cooldown_active,
|
||||||
self._consecutive_pass_count[done_idx] + 1, 0)
|
self._consecutive_pass_count[done_idx] + 1, 0)
|
||||||
# 冷却期以该 env 自己完成的 episode 为单位,而不是全局 reset 批次数。
|
# 冷却期以该 env 自己完成的 episode 为单位,而不是全局 reset 批次数。
|
||||||
self._upgrade_cooldown[done_idx] = np.maximum(0, self._upgrade_cooldown[done_idx] - 1)
|
self._upgrade_cooldown[done_idx] = np.maximum(0, self._upgrade_cooldown[done_idx] - 1)
|
||||||
@@ -578,7 +587,8 @@ class DreamWaQTask(Go1WalkTask):
|
|||||||
self._curric_raw_all_ok = 0; self._curric_streak_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_upgrade_ok = 0; self._curric_cooldown_block = 0
|
||||||
self._curric_down_ok = 0; self._curric_down_track = 0
|
self._curric_down_ok = 0; self._curric_down_track = 0
|
||||||
self._curric_down_stability = 0; self._curric_total = 0
|
self._curric_down_stability = 0; self._curric_progress_ok = 0
|
||||||
|
self._curric_total = 0
|
||||||
self._curric_log_counter += 1
|
self._curric_log_counter += 1
|
||||||
self._curric_track_ok += int((avg_tracking > 0.5).sum())
|
self._curric_track_ok += int((avg_tracking > 0.5).sum())
|
||||||
self._curric_survive_ok += int(not_fallen.sum())
|
self._curric_survive_ok += int(not_fallen.sum())
|
||||||
@@ -591,6 +601,7 @@ class DreamWaQTask(Go1WalkTask):
|
|||||||
self._curric_down_ok += int(move_down.sum())
|
self._curric_down_ok += int(move_down.sum())
|
||||||
self._curric_down_track += int((avg_tracking < 0.35).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_down_stability += int((~not_fallen | (avg_orient <= -0.001) | (avg_bh <= -0.002)).sum())
|
||||||
|
self._curric_progress_ok += int((distance > self._cell_size / 2.0).sum())
|
||||||
self._curric_total += num_reset
|
self._curric_total += num_reset
|
||||||
if self._curric_log_counter % 50 == 0:
|
if self._curric_log_counter % 50 == 0:
|
||||||
def pct(n): return 100*n/max(self._curric_total,1)
|
def pct(n): return 100*n/max(self._curric_total,1)
|
||||||
@@ -606,6 +617,7 @@ class DreamWaQTask(Go1WalkTask):
|
|||||||
f"down={pct(self._curric_down_ok):.0f}% "
|
f"down={pct(self._curric_down_ok):.0f}% "
|
||||||
f"downT={pct(self._curric_down_track):.0f}% "
|
f"downT={pct(self._curric_down_track):.0f}% "
|
||||||
f"downS={pct(self._curric_down_stability):.0f}% "
|
f"downS={pct(self._curric_down_stability):.0f}% "
|
||||||
|
f"progress={pct(self._curric_progress_ok):.0f}% "
|
||||||
f"max_init={self._max_init_level}")
|
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)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5fe0435e385f736b46a910b53d30135d7f6280c0d8daa2bd2f64b7df7962d998
|
oid sha256:a8289226407a014f04e4d5e9d7f526c2c154d50d1fe91865fce3451c212b31cb
|
||||||
size 1439313
|
size 912324
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ BORDER_M = 5.0
|
|||||||
PROPORTIONS = [0.1, 0.1, 0.35, 0.35, 0.1]
|
PROPORTIONS = [0.1, 0.1, 0.35, 0.35, 0.1]
|
||||||
CUM = [sum(PROPORTIONS[:i + 1]) for i in range(len(PROPORTIONS))]
|
CUM = [sum(PROPORTIONS[:i + 1]) for i in range(len(PROPORTIONS))]
|
||||||
PLATFORM_M = 3.0
|
PLATFORM_M = 3.0
|
||||||
_SLOPE_SCALE = 0.4 # 上游原值(已验证 z_scale 上限远超 0.54)
|
_SLOPE_SCALE = 0.25 # MotrixSim-friendly slope range
|
||||||
|
ROUGH_GRID_M = 0.20 # Correlate roughness over 20 cm, not each 5 cm pixel.
|
||||||
|
ROUGH_BASE_M = 0.01
|
||||||
|
ROUGH_GAIN_M = 0.03
|
||||||
|
OBSTACLE_BASE_M = 0.03
|
||||||
|
OBSTACLE_GAIN_M = 0.10
|
||||||
|
|
||||||
CELL_PX = int(CELL_M / HS) # 160
|
CELL_PX = int(CELL_M / HS) # 160
|
||||||
BORDER_PX = int(BORDER_M / HS) # 100
|
BORDER_PX = int(BORDER_M / HS) # 100
|
||||||
@@ -61,11 +66,16 @@ def draw_slope(canvas, x0, y0, difficulty, noise=False):
|
|||||||
hi = max(edge_h, 0)
|
hi = max(edge_h, 0)
|
||||||
hf = np.clip(hf, lo, hi).astype(np.uint16)
|
hf = np.clip(hf, lo, hi).astype(np.uint16)
|
||||||
if noise:
|
if noise:
|
||||||
na = int(0.05 / VS)
|
# Independent 5 cm samples created 10 cm jumps between adjacent
|
||||||
n = np.random.randint(-na, na + 1, (CELL_PX, CELL_PX), dtype=np.int16)
|
# pixels. Interpolate a 20 cm grid for physically coherent roughness.
|
||||||
# 噪声也只在平台外
|
coarse_px = max(2, int(round(ROUGH_GRID_M / HS)))
|
||||||
|
amp = ROUGH_BASE_M + ROUGH_GAIN_M * difficulty
|
||||||
|
coarse = np.random.uniform(
|
||||||
|
-amp, amp, (coarse_px, coarse_px)).astype(np.float32)
|
||||||
|
n = cv2.resize(coarse, (CELL_PX, CELL_PX), interpolation=cv2.INTER_LINEAR)
|
||||||
n[cx - p2:cx + p2, cy - p2:cy + p2] = 0
|
n[cx - p2:cx + p2, cy - p2:cy + p2] = 0
|
||||||
hf = np.clip(hf.astype(np.int32) + n, 0, 65535).astype(np.uint16)
|
hf = np.clip(hf.astype(np.float32) * VS + n, 0, None)
|
||||||
|
hf = np.rint(hf / VS).astype(np.uint16)
|
||||||
canvas[y0:y0 + CELL_PX, x0:x0 + CELL_PX] += hf
|
canvas[y0:y0 + CELL_PX, x0:x0 + CELL_PX] += hf
|
||||||
|
|
||||||
|
|
||||||
@@ -105,7 +115,7 @@ def draw_obstacles(canvas, x0, y0, difficulty):
|
|||||||
"""离散障碍物(随机矩形块)。"""
|
"""离散障碍物(随机矩形块)。"""
|
||||||
if difficulty <= 0:
|
if difficulty <= 0:
|
||||||
return
|
return
|
||||||
max_h = int((0.05 + 0.2 * difficulty) / VS)
|
max_h = int((OBSTACLE_BASE_M + OBSTACLE_GAIN_M * difficulty) / VS)
|
||||||
if max_h <= 0:
|
if max_h <= 0:
|
||||||
return
|
return
|
||||||
p2 = PLATFORM_PX // 2
|
p2 = PLATFORM_PX // 2
|
||||||
|
|||||||
Reference in New Issue
Block a user