"""Dependency-free task registry and bounded PPO settings.""" import json import math import re from pathlib import Path CONTRACTS = Path(__file__).resolve().parents[2] / "contracts" TASK = json.loads((CONTRACTS / "mobile-manipulator-v2.json").read_text()) ROBOTS = {r["id"]: r for r in json.loads((CONTRACTS / "mobile-robots-v1.json").read_text())} MOBILE_TASKS = { "MobileManipulator-LeKiwi-v1": "lekiwi-v1", "MobileManipulator-LeKiwi-Bundle": "lekiwi-bundle", } def validate_mobile_params(value): if not isinstance(value, dict) or value.keys() - { "rolloutSteps", "objectPosition", "goalPosition", "stage", "sourceJobId", "positionJitter", "evaluationEpisodes", "navigationBootstrapSteps", }: raise ValueError("mobileParams 包含未知参数") steps = value.get("rolloutSteps", 128) if isinstance(steps, bool) or not isinstance(steps, int) or not 8 <= steps <= 4096: raise ValueError("rolloutSteps 必须为 8–4096 的整数") stage = value.get("stage", "navigate") if stage not in ("navigate", "reach", "pick-place"): raise ValueError("stage 必须为 navigate / reach / pick-place") source = value.get("sourceJobId") if source is not None and ( not isinstance(source, str) or not re.fullmatch(r"[0-9a-f]{32}", source) ): raise ValueError("sourceJobId 必须是服务内的训练作业 ID,不接受 checkpoint 路径") jitter = value.get("positionJitter", 0.1) if isinstance(jitter, bool) or not isinstance(jitter, (int, float)) or not 0 <= jitter <= 0.3: raise ValueError("positionJitter 必须在 0–0.3 m 内") episodes = value.get("evaluationEpisodes", 10) if isinstance(episodes, bool) or not isinstance(episodes, int) or not 2 <= episodes <= 64: raise ValueError("evaluationEpisodes 必须为 2–64 的整数") bootstrap = value.get("navigationBootstrapSteps", 4096) if isinstance(bootstrap, bool) or not isinstance(bootstrap, int) or not 0 <= bootstrap <= 10000: raise ValueError("navigationBootstrapSteps 必须为 0–10000 的整数") result = { "navigationBootstrapSteps": bootstrap, "rolloutSteps": steps, "stage": stage, "positionJitter": jitter, "evaluationEpisodes": episodes, } if source is not None: result["sourceJobId"] = source for key, default in ( ("objectPosition", TASK["objectStart"]), ("goalPosition", TASK["goalStart"]), ): position = value.get(key, default) if ( not isinstance(position, list) or len(position) != 3 or any( isinstance(x, bool) or not isinstance(x, (float, int)) or not -TASK["positionScale"] <= x <= TASK["positionScale"] or not math.isfinite(x) for x in position ) or position[2] < TASK["objectStart"][2] ): raise ValueError(f"{key} 必须是任务范围内的三维坐标,z 不低于物体半高") result[key] = position.copy() return result def mobile_metadata(task_id): return { "id": task_id, "name": f"移动操作 · {ROBOTS[MOBILE_TASKS[task_id]]['label']}", "family": "mobile-manipulator", "robotId": MOBILE_TASKS[task_id], "browserCompatible": True, "terrainPresets": [], "terrainParameters": {}, "sensorTypes": [], "sensorParameters": {}, "mapSyncScope": "当前移动操作场景快照", "controlDt": TASK["controlDt"], }