Add eval_kit evaluation suite with WebRTC visualization
- eval_kit/core: 零框架依赖的指标内核(Trace/metrics/aggregation/terrain_spec/report/naming) - eval_kit/isaaclab: 适配层(env_cfg 改造、rollout 采集、checkpoint 加载) - eval_kit/profiles.py / protocols.py: 机器人形态描述与评估口径预设 - eval_kit/tests: 24 项内核自检 + 21 项命令行自检(都不需要仿真) - eval_suite.py: 编排入口,每个(阶段, 种子)一个子进程;与 eval.py 指标口径一致 - WebRTC 可视化:--livestream 透传、视口相机跟随/自由(--view-env / --free-camera)、 连接前停顿(--pause-s)、自动放开客户端改画幅、打印连接地址 - 诊断与替代路径:rollout 进度打印、--record-frames 抓帧
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2025 Deep Robotics
|
||||
# SPDX-License-Identifier: BSD 3-Clause
|
||||
|
||||
"""按正则列表解析名字,与 IsaacLab 的 ``resolve_matching_names`` 语义一致,
|
||||
但**匹配不到不抛异常**。
|
||||
|
||||
IsaacLab 的实现用 ``re.fullmatch`` 逐个比对,并且:某个名字同时命中多个正则会报错,
|
||||
某个正则一个都没命中也报错。对环境配置来说这种严格是有用的(写错了应该立刻发现),
|
||||
但评价代码不该因为「这个机器人没有轮子」就崩掉——那时正确行为是跳过依赖轮子的指标。
|
||||
|
||||
这个模块是纯逻辑,不依赖 isaaclab,因此可以单独测试。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
class NameResolution:
|
||||
"""一次名字解析的结果。"""
|
||||
|
||||
__slots__ = ("indices", "names", "unmatched_patterns", "ambiguous_names")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
indices: list[int],
|
||||
names: list[str],
|
||||
unmatched_patterns: list[str],
|
||||
ambiguous_names: list[tuple[str, list[str]]],
|
||||
) -> None:
|
||||
self.indices = indices
|
||||
self.names = names
|
||||
#: 一个都没命中的正则。
|
||||
self.unmatched_patterns = unmatched_patterns
|
||||
#: 同时命中多个正则的名字,以及命中的正则。
|
||||
self.ambiguous_names = ambiguous_names
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.indices)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.indices)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"NameResolution({len(self.indices)} 项, "
|
||||
f"未命中={self.unmatched_patterns}, 多重命中={self.ambiguous_names})"
|
||||
)
|
||||
|
||||
|
||||
def resolve_matching_indices(patterns: Sequence[str], names: Sequence[str]) -> NameResolution:
|
||||
"""把正则列表解析成 ``names`` 中的下标。
|
||||
|
||||
行为与 IsaacLab 的 ``resolve_matching_names`` 一致之处:用 ``re.fullmatch``;
|
||||
返回顺序遵循 ``patterns`` 的顺序。
|
||||
不同之处:不抛异常,而是把「未命中」和「多重命中」记在返回值里,由调用方决定
|
||||
是警告还是忽略。
|
||||
|
||||
Args:
|
||||
patterns: 正则列表,例如 ``("base_link", ".*_knee")``。
|
||||
names: 候选名字,例如 ``articulation.body_names``。
|
||||
"""
|
||||
hits: dict[int, list[str]] = {}
|
||||
pattern_hits: dict[str, int] = {pattern: 0 for pattern in patterns}
|
||||
|
||||
for index, name in enumerate(names):
|
||||
for pattern in patterns:
|
||||
try:
|
||||
matched = re.fullmatch(pattern, name) is not None
|
||||
except re.error as exc: # 正则本身写错,属于配置错误,应当立刻暴露
|
||||
raise ValueError(f"机器人描述里的正则 {pattern!r} 无法编译:{exc}") from exc
|
||||
if matched:
|
||||
hits.setdefault(index, []).append(pattern)
|
||||
pattern_hits[pattern] += 1
|
||||
|
||||
indices: list[int] = []
|
||||
resolved_names: list[str] = []
|
||||
ambiguous: list[tuple[str, list[str]]] = []
|
||||
taken: set[int] = set()
|
||||
|
||||
# 按 patterns 顺序输出,跳过已被更早的正则取走的下标
|
||||
for pattern in patterns:
|
||||
for index, matched_patterns in hits.items():
|
||||
if pattern not in matched_patterns or index in taken:
|
||||
continue
|
||||
taken.add(index)
|
||||
indices.append(index)
|
||||
resolved_names.append(names[index])
|
||||
if len(matched_patterns) > 1:
|
||||
ambiguous.append((names[index], list(matched_patterns)))
|
||||
|
||||
unmatched = [pattern for pattern, count in pattern_hits.items() if count == 0]
|
||||
return NameResolution(indices, resolved_names, unmatched, ambiguous)
|
||||
Reference in New Issue
Block a user