# 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)