Current code snapshot: smooth/bridge task configs, smooth random terrain, roll/pitch stability rewards, M20 URDF updates, eval profile and stability training documentation.
213 lines
8.5 KiB
Python
213 lines
8.5 KiB
Python
# Copyright (c) 2025 Deep Robotics
|
||
# SPDX-License-Identifier: BSD 3-Clause
|
||
|
||
"""上限评估的网格汇总:把逐环境结果折叠成「难度等级 × 地形类型」的成功率矩阵。
|
||
|
||
纯数据变换,只依赖 numpy,与仿真框架无关。
|
||
|
||
``score`` 的公式与 ``criterion.py`` / 旧 ``eval.py`` 完全一致(三者代数等价):
|
||
对每个地形类型,先算逐级累积成功率 ``acc[i] = Π_{j<=i} rate[j]``,
|
||
再算 ``score = Σ_i acc[i] * (i+1) * (1 - rate[i+1])``,最后一级的 ``(1 - rate)`` 取 1。
|
||
含义是「在爬到第 i+1 级之前失败」的累积期望,值越大说明越能爬到高难度。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Sequence
|
||
|
||
import numpy as np
|
||
|
||
from eval_kit.core.terrain_spec import TerrainSpec
|
||
|
||
|
||
def accumulated_success(rates: Sequence[float]) -> list[float]:
|
||
"""逐级累积成功率。"""
|
||
accumulated: list[float] = []
|
||
running = 1.0
|
||
for rate in rates:
|
||
running *= float(rate)
|
||
accumulated.append(running)
|
||
return accumulated
|
||
|
||
|
||
def upper_limit_score(rates: Sequence[float]) -> float:
|
||
"""由某一地形类型各难度等级的成功率算 score。"""
|
||
rates = [float(rate) for rate in rates]
|
||
accumulated = accumulated_success(rates)
|
||
score = 0.0
|
||
for index, acc_rate in enumerate(accumulated):
|
||
next_failure = 1.0 - rates[index + 1] if index + 1 < len(rates) else 1.0
|
||
score += acc_rate * (index + 1) * next_failure
|
||
return score
|
||
|
||
|
||
def aggregate_runs(runs: Sequence[tuple[int, dict[str, Any]]]) -> dict[str, Any]:
|
||
"""把多次(不同随机种子)的指标聚合成 mean/std。
|
||
|
||
单次 rollout 的噪声足以淹没两个策略之间的真实差异,所以做对比时应该跑多个
|
||
种子并看分布,而不是看单个数。
|
||
|
||
Args:
|
||
runs: ``(seed, metrics)`` 列表,``metrics`` 来自
|
||
:func:`eval_kit.core.metrics.compute_metrics`。
|
||
"""
|
||
if not runs:
|
||
raise ValueError("没有任何一次 run 可以聚合。")
|
||
|
||
# 逐环境的私有字段不参与聚合
|
||
metric_keys: list[str] = []
|
||
for _, metrics in runs:
|
||
for key, value in metrics.items():
|
||
if key.startswith("_"):
|
||
continue
|
||
if isinstance(value, (int, float)) and key not in metric_keys:
|
||
metric_keys.append(key)
|
||
|
||
summary: dict[str, Any] = {}
|
||
for key in metric_keys:
|
||
values = np.array([float(metrics.get(key, float("nan"))) for _, metrics in runs], dtype=np.float64)
|
||
finite = values[np.isfinite(values)]
|
||
if finite.size == 0:
|
||
summary[key] = {
|
||
"mean": float("nan"),
|
||
"std": float("nan"),
|
||
"min": float("nan"),
|
||
"max": float("nan"),
|
||
"values": values.tolist(),
|
||
"finite_runs": 0,
|
||
}
|
||
continue
|
||
summary[key] = {
|
||
"mean": float(np.mean(finite)),
|
||
"std": float(np.std(finite, ddof=1)) if finite.size > 1 else 0.0,
|
||
"min": float(np.min(finite)),
|
||
"max": float(np.max(finite)),
|
||
"values": values.tolist(),
|
||
"finite_runs": int(finite.size),
|
||
}
|
||
|
||
consistent: dict[str, Any] = {}
|
||
for key in runs[0][1]:
|
||
if not key.startswith("_") and isinstance(runs[0][1][key], str):
|
||
values = {metrics.get(key) for _, metrics in runs}
|
||
consistent[key] = runs[0][1][key]
|
||
if len(values) > 1:
|
||
consistent[f"{key}_INCONSISTENT"] = sorted(str(value) for value in values)
|
||
|
||
return {
|
||
"num_runs": len(runs),
|
||
"seeds": [int(seed) for seed, _ in runs],
|
||
"summary": summary,
|
||
"constant": consistent,
|
||
"per_run": [{"seed": int(seed), "metrics": metrics} for seed, metrics in runs],
|
||
}
|
||
|
||
|
||
def build_upper_limit_matrix(
|
||
per_env: dict[str, list[float]],
|
||
level_ids: np.ndarray,
|
||
type_ids: np.ndarray,
|
||
terrain_types: Sequence[str],
|
||
spec: TerrainSpec,
|
||
num_levels: int,
|
||
) -> dict[str, Any]:
|
||
"""把逐环境结果汇总成网格。
|
||
|
||
Args:
|
||
per_env: 含 ``success`` / ``max_progress`` / ``max_tilt`` 的逐环境列表,
|
||
由 :func:`eval_kit.core.metrics.compute_metrics` 在
|
||
``include_per_env=True`` 时给出。
|
||
level_ids: ``(N,)`` 每个环境所属的难度等级(0 起)。
|
||
type_ids: ``(N,)`` 每个环境所属的地形类型序号(对应 ``terrain_types``)。
|
||
terrain_types: 地形类型名,同时决定矩阵列顺序。
|
||
spec: 地形规格,用于取每级的难度值。
|
||
num_levels: 难度等级数(矩阵行数)。
|
||
"""
|
||
success = np.asarray(per_env["success"], dtype=np.float64)
|
||
max_progress = np.asarray(per_env["max_progress"], dtype=np.float64)
|
||
max_tilt = np.asarray(per_env["max_tilt"], dtype=np.float64)
|
||
level_ids = np.asarray(level_ids, dtype=np.int64)
|
||
type_ids = np.asarray(type_ids, dtype=np.int64)
|
||
|
||
if not (success.shape == max_progress.shape == max_tilt.shape == level_ids.shape == type_ids.shape):
|
||
raise ValueError("逐环境数组与网格编号数组长度不一致。")
|
||
|
||
per_type: dict[str, Any] = {}
|
||
for type_index, terrain_type in enumerate(terrain_types):
|
||
level_values = spec.levels_for(terrain_type, num_levels)
|
||
rates: list[float] = []
|
||
level_metrics: list[dict[str, Any]] = []
|
||
for level_index, level_value in enumerate(level_values):
|
||
mask = (type_ids == type_index) & (level_ids == level_index)
|
||
total = int(np.count_nonzero(mask))
|
||
if total == 0:
|
||
rates.append(float("nan"))
|
||
level_metrics.append(
|
||
{
|
||
"level": level_index + 1,
|
||
"difficulty": float(level_value),
|
||
"success_rate": float("nan"),
|
||
"success_count": 0.0,
|
||
"num_envs": 0,
|
||
"avg_progress_m": float("nan"),
|
||
"avg_max_tilt_rad": float("nan"),
|
||
}
|
||
)
|
||
continue
|
||
count = float(np.count_nonzero(success[mask] > 0.5))
|
||
rates.append(count / total)
|
||
level_metrics.append(
|
||
{
|
||
"level": level_index + 1,
|
||
"difficulty": float(level_value),
|
||
"success_rate": count / total,
|
||
"success_count": count,
|
||
"num_envs": total,
|
||
"avg_progress_m": float(np.mean(max_progress[mask])),
|
||
"avg_max_tilt_rad": float(np.mean(max_tilt[mask])),
|
||
}
|
||
)
|
||
|
||
per_type[terrain_type] = {
|
||
"levels": level_metrics,
|
||
"success_rate": rates,
|
||
"success_rate_accumulated": accumulated_success(rates),
|
||
"score": upper_limit_score(rates),
|
||
"max_level": len(level_values),
|
||
"difficulty_unit": spec.unit.get(terrain_type, ""),
|
||
}
|
||
|
||
# 打成便于打印与比较的矩阵:行 = 难度等级,列 = 地形类型
|
||
success_matrix: list[list[float]] = []
|
||
accumulated_matrix: list[list[float]] = []
|
||
difficulty_matrix: list[list[float]] = []
|
||
for level_index in range(num_levels):
|
||
success_row: list[float] = []
|
||
accumulated_row: list[float] = []
|
||
difficulty_row: list[float] = []
|
||
for terrain_type in terrain_types:
|
||
entry = per_type[terrain_type]
|
||
rates = entry["success_rate"]
|
||
accumulated = entry["success_rate_accumulated"]
|
||
levels = entry["levels"]
|
||
success_row.append(rates[level_index] if level_index < len(rates) else float("nan"))
|
||
accumulated_row.append(accumulated[level_index] if level_index < len(accumulated) else float("nan"))
|
||
difficulty_row.append(levels[level_index]["difficulty"] if level_index < len(levels) else float("nan"))
|
||
success_matrix.append(success_row)
|
||
accumulated_matrix.append(accumulated_row)
|
||
difficulty_matrix.append(difficulty_row)
|
||
|
||
return {
|
||
"benchmark": spec.name,
|
||
"terrain_types": list(terrain_types),
|
||
"num_levels": num_levels,
|
||
"difficulty_unit": {terrain_type: spec.unit.get(terrain_type, "") for terrain_type in terrain_types},
|
||
"per_type": per_type,
|
||
"scores": {terrain_type: per_type[terrain_type]["score"] for terrain_type in terrain_types},
|
||
"matrix": {
|
||
"success_rate": success_matrix,
|
||
"success_rate_accumulated": accumulated_matrix,
|
||
"difficulty": difficulty_matrix,
|
||
},
|
||
}
|