feat(config): 添加自定义配置解析器,过滤未知 YAML 字段
新增 configs 模块,封装 draccus.wrap 扩展解析逻辑: 加载 YAML 配置时自动过滤 dataclass 不识别的字段,避免解析报错; 支持 ChoiceRegistry 嵌套类型字段过滤; 移除已废弃的左右 xArm6 UMI 录制配置文件。
This commit is contained in:
parent
a187221e45
commit
811a7f7717
@ -1,43 +0,0 @@
|
||||
robot:
|
||||
type: uf::robot
|
||||
id: "uf_robot"
|
||||
robot_ip: "192.168.1.29"
|
||||
robot_dof: 6
|
||||
control_space: "cartesian"
|
||||
gripper_type: 0
|
||||
max_linear_velocity: 250
|
||||
start_joints: [-14.7, 25.6, -49.6, -282.5, 95.9, -65.9]
|
||||
start_tcp_pose: [400, 0, 150, -90, 0, 0]
|
||||
cameras:
|
||||
fisheye:
|
||||
# type: uf::umi_camera
|
||||
# serial_number: "250801DR48FB26001379"
|
||||
# width: 1280
|
||||
# height: 1280
|
||||
# fps: 30
|
||||
type: opencv
|
||||
index_or_path: "/dev/v4l/by-id/usb-XVisio_Technology_XVisio_vSLAM_250801DR48FB26001379-video-index0"
|
||||
width: 1280
|
||||
height: 1280
|
||||
fps: 100
|
||||
fourcc: "YUYV"
|
||||
|
||||
teleop:
|
||||
type: uf::umi_teleop
|
||||
serial_number: "250801DR48FP26001318"
|
||||
use_vive_tracker: False
|
||||
vive_tracker_id: "LHR-555DC7BF"
|
||||
use_gripper: True
|
||||
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
|
||||
robot_base_pose: [400, 0, 150, -90, 0, 0]
|
||||
|
||||
dataset:
|
||||
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
|
||||
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm6_umi_datas"
|
||||
repo_id: "ufactory/xarm6_umi_datas"
|
||||
single_task: "Pick up the purple grape and drop into the box on the left."
|
||||
fps: 60
|
||||
episode_time_s: 60 # max duration for one episode
|
||||
reset_time_s: 20 # time for resetting env between episodes
|
||||
num_episodes: 100
|
||||
push_to_hub: False
|
||||
@ -1,43 +0,0 @@
|
||||
robot:
|
||||
type: uf::robot
|
||||
id: "uf_robot"
|
||||
robot_ip: "192.168.1.83"
|
||||
robot_dof: 6
|
||||
control_space: "cartesian"
|
||||
gripper_type: 2
|
||||
max_linear_velocity: 250
|
||||
start_joints: [14.5, 25.5, -49.6, -76.8, 95.9, 66.6]
|
||||
start_tcp_pose: [400, 0, 150, 90, 0, 0]
|
||||
cameras:
|
||||
fisheye:
|
||||
# type: uf::umi_camera
|
||||
# serial_number: "250801DR48FB26001396"
|
||||
# width: 1280
|
||||
# height: 1280
|
||||
# fps: 30
|
||||
type: opencv
|
||||
index_or_path: "/dev/v4l/by-id/usb-XVisio_Technology_XVisio_vSLAM_250801DR48FB26001396-video-index0"
|
||||
width: 1280
|
||||
height: 1280
|
||||
fps: 100
|
||||
fourcc: "YUYV"
|
||||
|
||||
teleop:
|
||||
type: uf::umi_teleop
|
||||
serial_number: "250801DR48FP26001295"
|
||||
use_vive_tracker: False
|
||||
vive_tracker_id: "LHR-2425BAD3"
|
||||
use_gripper: True
|
||||
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
|
||||
robot_base_pose: [400, 0, 150, 90, 0, 0]
|
||||
|
||||
dataset:
|
||||
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
|
||||
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm6_umi_datas"
|
||||
repo_id: "ufactory/xarm6_umi_datas"
|
||||
single_task: "Pick up the purple grape and drop into the box on the left."
|
||||
fps: 60
|
||||
episode_time_s: 60 # max duration for one episode
|
||||
reset_time_s: 20 # time for resetting env between episodes
|
||||
num_episodes: 100
|
||||
push_to_hub: False
|
||||
1
src/lerobot_robot_ufactory/configs/__init__.py
Normal file
1
src/lerobot_robot_ufactory/configs/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .parser import wrap
|
||||
151
src/lerobot_robot_ufactory/configs/parser.py
Normal file
151
src/lerobot_robot_ufactory/configs/parser.py
Normal file
@ -0,0 +1,151 @@
|
||||
import sys
|
||||
import tempfile
|
||||
import yaml
|
||||
import inspect
|
||||
from dataclasses import fields, is_dataclass
|
||||
from types import UnionType
|
||||
from typing import Union, get_args, get_origin
|
||||
from lerobot.configs.parser import *
|
||||
|
||||
|
||||
def _is_choice_registry(config_type):
|
||||
return (
|
||||
isinstance(config_type, type)
|
||||
and hasattr(config_type, "get_known_choices")
|
||||
and hasattr(config_type, "get_choice_class")
|
||||
)
|
||||
|
||||
|
||||
def _unwrap_optional(config_type):
|
||||
origin = get_origin(config_type)
|
||||
if origin in (Union, UnionType):
|
||||
non_none_args = [arg for arg in get_args(config_type) if arg is not type(None)]
|
||||
if len(non_none_args) == 1:
|
||||
return non_none_args[0]
|
||||
return config_type
|
||||
|
||||
|
||||
def _pop_cli_arg(arg_name, args):
|
||||
value = None
|
||||
filtered_args = []
|
||||
prefix = f"--{arg_name}="
|
||||
index = 0
|
||||
while index < len(args):
|
||||
arg = args[index]
|
||||
if arg.startswith(prefix):
|
||||
value = arg[len(prefix):]
|
||||
elif arg == f"--{arg_name}":
|
||||
if index + 1 >= len(args):
|
||||
raise ValueError(f"Missing value for --{arg_name}")
|
||||
value = args[index + 1]
|
||||
index += 1
|
||||
else:
|
||||
filtered_args.append(arg)
|
||||
index += 1
|
||||
return value, filtered_args
|
||||
|
||||
def _filter_unknown_config_fields(data, config_type, resolve_choice=True):
|
||||
config_type = _unwrap_optional(config_type)
|
||||
origin = get_origin(config_type)
|
||||
|
||||
if origin is list:
|
||||
args = get_args(config_type)
|
||||
if isinstance(data, list) and args:
|
||||
return [_filter_unknown_config_fields(item, args[0]) for item in data]
|
||||
return data
|
||||
|
||||
if origin is dict:
|
||||
args = get_args(config_type)
|
||||
if isinstance(data, dict) and len(args) == 2:
|
||||
return {key: _filter_unknown_config_fields(value, args[1]) for key, value in data.items()}
|
||||
return data
|
||||
|
||||
if isinstance(data, dict) and _is_choice_registry(config_type) and resolve_choice:
|
||||
choice_key = getattr(draccus, "CHOICE_TYPE_KEY", "type")
|
||||
choice_name = data.get(choice_key) or data.get("type")
|
||||
if choice_name in config_type.get_known_choices():
|
||||
choice_type = config_type.get_choice_class(choice_name)
|
||||
filtered = _filter_unknown_config_fields(data, choice_type, resolve_choice=False)
|
||||
if choice_key in data:
|
||||
filtered = {choice_key: data[choice_key], **filtered}
|
||||
elif "type" in data:
|
||||
filtered = {"type": data["type"], **filtered}
|
||||
return filtered
|
||||
return data
|
||||
|
||||
if not isinstance(data, dict) or not is_dataclass(config_type):
|
||||
return data
|
||||
|
||||
known_fields = {field.name: field for field in fields(config_type)}
|
||||
return {
|
||||
key: _filter_unknown_config_fields(value, known_fields[key].type)
|
||||
for key, value in data.items()
|
||||
if key in known_fields
|
||||
}
|
||||
|
||||
def _filtered_config_path(config_type, config_path):
|
||||
if config_path is None:
|
||||
return None, None
|
||||
|
||||
config_path = Path(config_path)
|
||||
with config_path.open("r", encoding="utf-8") as f:
|
||||
raw_config = yaml.safe_load(f) or {}
|
||||
|
||||
filtered_config = _filter_unknown_config_fields(raw_config, config_type)
|
||||
if filtered_config == raw_config:
|
||||
return config_path, None
|
||||
|
||||
suffix = config_path.suffix if config_path.suffix else ".yaml"
|
||||
with tempfile.NamedTemporaryFile("w", suffix=suffix, encoding="utf-8", delete=False) as f:
|
||||
yaml.safe_dump(filtered_config, f, sort_keys=False, allow_unicode=True)
|
||||
return Path(f.name), Path(f.name)
|
||||
|
||||
|
||||
def wrap(config_path: Path | None = None) -> Callable[[F], F]:
|
||||
"""
|
||||
HACK: Similar to draccus.wrap but does three additional things:
|
||||
- Will remove '.path' arguments from CLI in order to process them later on.
|
||||
- If a 'config_path' is passed and the main config class has a 'from_pretrained' method, will
|
||||
initialize it from there to allow to fetch configs from the hub directly
|
||||
- Will load plugins specified in the CLI arguments. These plugins will typically register
|
||||
their own subclasses of config classes, so that draccus can find the right class to instantiate
|
||||
from the CLI '.type' arguments
|
||||
"""
|
||||
def wrapper_outer(fn: F) -> F:
|
||||
@wraps(fn)
|
||||
def wrapper_inner(*args: Any, **kwargs: Any) -> Any:
|
||||
argspec = inspect.getfullargspec(fn)
|
||||
argtype = argspec.annotations[argspec.args[0]]
|
||||
if len(args) > 0 and type(args[0]) is argtype:
|
||||
cfg = args[0]
|
||||
args = args[1:]
|
||||
else:
|
||||
cli_args = sys.argv[1:]
|
||||
plugin_args = parse_plugin_args(PLUGIN_DISCOVERY_SUFFIX, cli_args)
|
||||
for plugin_cli_arg, plugin_path in plugin_args.items():
|
||||
try:
|
||||
load_plugin(plugin_path)
|
||||
except PluginLoadError as e:
|
||||
# add the relevant CLI arg to the error message
|
||||
raise PluginLoadError(f"{e}\nFailed plugin CLI Arg: {plugin_cli_arg}") from e
|
||||
cli_args = filter_arg(plugin_cli_arg, cli_args)
|
||||
config_path_cli, cli_args = _pop_cli_arg("config_path", cli_args)
|
||||
if has_method(argtype, "__get_path_fields__"):
|
||||
path_fields = argtype.__get_path_fields__()
|
||||
cli_args = filter_path_args(path_fields, cli_args)
|
||||
if has_method(argtype, "from_pretrained") and config_path_cli:
|
||||
cfg = argtype.from_pretrained(config_path_cli, cli_args=cli_args)
|
||||
else:
|
||||
config_path_for_parse = Path(config_path_cli) if config_path_cli else config_path
|
||||
filtered_path, temp_path = _filtered_config_path(argtype, config_path_for_parse)
|
||||
try:
|
||||
cfg = draccus.parse(config_class=argtype, config_path=filtered_path, args=cli_args)
|
||||
finally:
|
||||
if temp_path is not None:
|
||||
temp_path.unlink(missing_ok=True)
|
||||
response = fn(cfg, *args, **kwargs)
|
||||
return response
|
||||
|
||||
return cast(F, wrapper_inner)
|
||||
|
||||
return cast(Callable[[F], F], wrapper_outer)
|
||||
Loading…
Reference in New Issue
Block a user