Current code snapshot: smooth/bridge task configs, smooth random terrain, roll/pitch stability rewards, M20 URDF updates, eval profile and stability training documentation.
95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
# 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)
|