| import dataclasses |
| import enum |
| import logging |
| import os |
| import pathlib |
| import socket |
|
|
| import numpy as np |
| import tyro |
|
|
| from openpi.policies import policy as _policy |
| from openpi.policies import policy_config as _policy_config |
| from openpi.serving import websocket_policy_server |
| from openpi.training import config as _config |
|
|
|
|
| def _interpolate_trajectory(traj: np.ndarray, target_len: int) -> np.ndarray: |
| """将动作轨迹线性插值到目标帧数(逐维度 np.interp)""" |
| n = len(traj) |
| if n == target_len: |
| return traj |
| x_old = np.linspace(0.0, 1.0, n) |
| x_new = np.linspace(0.0, 1.0, target_len) |
| result = np.zeros((target_len, traj.shape[1]), dtype=np.float32) |
| for i in range(traj.shape[1]): |
| result[:, i] = np.interp(x_new, x_old, traj[:, i]) |
| return result |
|
|
|
|
| class SmoothedPolicy: |
| """对模型输出动作做平滑后处理: |
| - Trick 2: 首帧插入当前机器人状态,消除首帧跳变 |
| - Trick 3: 线性插值到 target_actions 帧,使轨迹更平滑连续 |
| """ |
|
|
| def __init__(self, policy, target_actions: int = 32, prepend_current_state: bool = True): |
| self._policy = policy |
| self._target_actions = target_actions |
| self._prepend_current_state = prepend_current_state |
|
|
| @property |
| def metadata(self): |
| return self._policy.metadata |
|
|
| def infer(self, obs: dict) -> dict: |
| result = self._policy.infer(obs) |
| actions = result.get("actions") |
| if actions is None: |
| return result |
|
|
| actions = np.array(actions, dtype=np.float32) |
|
|
| |
| if self._prepend_current_state and "state" in obs: |
| current_state = np.array(obs["state"], dtype=np.float32) |
| if current_state.ndim == 1 and current_state.shape[0] == actions.shape[1]: |
| actions = np.concatenate([current_state[None, :], actions], axis=0) |
|
|
| |
| if len(actions) != self._target_actions: |
| actions = _interpolate_trajectory(actions, self._target_actions) |
|
|
| result["actions"] = actions |
| return result |
|
|
|
|
| class RTCPrefixPolicy: |
| """方案 C 近似版:用上一 chunk 末尾动作状态混合当前观察状态, |
| 使模型感知到前一 chunk 执行到哪里,从而生成轨迹连续的新 chunk。 |
| |
| obs 中若含 _rtc_mode=True,则提取 _prefix_actions 并做状态混合, |
| 再剥除所有 _rtc_* 键后调用下层 policy.infer()。 |
| """ |
|
|
| def __init__(self, policy, blend: float = 0.3): |
| """ |
| blend: 机器人真实状态的权重(0=完全用前缀末动作, 1=完全用机器人状态) |
| 推荐 0.2~0.4:偏低减少 chunk 边界抖动,偏高减少状态偏差累积 |
| """ |
| self._policy = policy |
| self._blend = blend |
|
|
| @property |
| def metadata(self): |
| return self._policy.metadata |
|
|
| def infer(self, obs: dict) -> dict: |
| |
| rtc_mode = obs.pop("_rtc_mode", False) |
| prefix_actions = obs.pop("_prefix_actions", None) |
| obs.pop("_inference_delay", None) |
|
|
| if rtc_mode and prefix_actions is not None and len(prefix_actions) > 0: |
| last_action = np.array(prefix_actions[-1], dtype=np.float32).flatten() |
| current_state = np.array(obs.get("state", []), dtype=np.float32).flatten() |
|
|
| if last_action.shape == current_state.shape and len(last_action) >= 6: |
| blend = self._blend |
| blended = current_state.copy() |
| |
| blended[0:3] = (1.0 - blend) * last_action[0:3] + blend * current_state[0:3] |
| blended[5] = (1.0 - blend) * last_action[5] + blend * current_state[5] |
| |
| if len(last_action) >= 7: |
| blended[6] = last_action[6] |
| obs["state"] = blended |
| logging.debug( |
| "RTC prefix blend=%.2f | xyz_delta=[%.4f,%.4f,%.4f]", |
| blend, |
| last_action[0] - current_state[0], |
| last_action[1] - current_state[1], |
| last_action[2] - current_state[2], |
| ) |
|
|
| return self._policy.infer(obs) |
|
|
|
|
| class EnvMode(enum.Enum): |
| """Supported environments.""" |
|
|
| ALOHA = "aloha" |
| ALOHA_SIM = "aloha_sim" |
| DROID = "droid" |
| LIBERO = "libero" |
|
|
|
|
| @dataclasses.dataclass |
| class Checkpoint: |
| """Load a policy from a trained checkpoint.""" |
|
|
| |
| config: str |
| |
| dir: str |
| |
| |
| |
| assets_dir: str = "" |
|
|
|
|
| @dataclasses.dataclass |
| class Default: |
| """Use the default policy for the given environment.""" |
|
|
|
|
| @dataclasses.dataclass |
| class Args: |
| """Arguments for the serve_policy script.""" |
|
|
| |
| env: EnvMode = EnvMode.ALOHA_SIM |
|
|
| |
| |
| default_prompt: str | None = None |
|
|
| |
| port: int = 8000 |
| |
| record: bool = False |
|
|
| |
| policy: Checkpoint | Default = dataclasses.field(default_factory=Default) |
|
|
| |
| |
| smooth: bool = True |
| |
| target_actions: int = 32 |
| |
| prepend_current_state: bool = True |
|
|
| |
| |
| rtc_prefix: bool = True |
| |
| rtc_blend: float = 0.3 |
|
|
|
|
| |
| DEFAULT_CHECKPOINT: dict[EnvMode, Checkpoint] = { |
| EnvMode.ALOHA: Checkpoint( |
| config="pi05_aloha", |
| dir="gs://openpi-assets/checkpoints/pi05_base", |
| ), |
| EnvMode.ALOHA_SIM: Checkpoint( |
| config="pi0_aloha_sim", |
| dir="gs://openpi-assets/checkpoints/pi0_aloha_sim", |
| ), |
| EnvMode.DROID: Checkpoint( |
| config="pi05_droid", |
| dir="gs://openpi-assets/checkpoints/pi05_droid", |
| ), |
| EnvMode.LIBERO: Checkpoint( |
| config="pi05_libero", |
| dir="gs://openpi-assets/checkpoints/pi05_libero", |
| ), |
| } |
|
|
|
|
| def create_default_policy(env: EnvMode, *, default_prompt: str | None = None) -> _policy.Policy: |
| """Create a default policy for the given environment.""" |
| if checkpoint := DEFAULT_CHECKPOINT.get(env): |
| return _policy_config.create_trained_policy( |
| _config.get_config(checkpoint.config), checkpoint.dir, default_prompt=default_prompt |
| ) |
| raise ValueError(f"Unsupported environment mode: {env}") |
|
|
|
|
| def _ensure_assets(ckpt_dir: str, assets_dir: str) -> None: |
| """确保 checkpoint 目录下有 assets/,否则自动软链接。 |
| |
| 查找优先级: |
| 1. 显式传入的 assets_dir |
| 2. 同级目录下任意含 assets/ 的 checkpoint(按名称排序取第一个) |
| """ |
| ckpt_path = pathlib.Path(ckpt_dir).resolve() |
| assets_link = ckpt_path / "assets" |
|
|
| |
| if assets_link.is_symlink() and not assets_link.exists(): |
| logging.warning("检测到失效的 assets 软链接,已删除: %s", assets_link) |
| assets_link.unlink() |
|
|
| if assets_link.exists(): |
| return |
|
|
| |
| candidates: list[pathlib.Path] = [] |
| if assets_dir: |
| candidates.append(pathlib.Path(assets_dir).resolve()) |
| |
| for sibling in sorted(ckpt_path.parent.iterdir()): |
| if sibling.is_dir() and sibling != ckpt_path and (sibling / "assets").is_dir(): |
| candidates.append(sibling / "assets") |
|
|
| if not candidates: |
| logging.warning("未找到任何 assets 目录,将由 create_trained_policy 报错") |
| return |
|
|
| src = candidates[0] |
| try: |
| os.symlink(src, assets_link) |
| logging.info("自动创建 assets 软链接: %s -> %s", assets_link, src) |
| except OSError as e: |
| logging.error( |
| "创建 assets 软链接失败: %s\n" |
| "请手动运行: ln -s %s %s", |
| e, src, assets_link, |
| ) |
| raise |
|
|
|
|
| def create_policy(args: Args) -> _policy.Policy: |
| """Create a policy from the given arguments.""" |
| match args.policy: |
| case Checkpoint(): |
| _ensure_assets(args.policy.dir, args.policy.assets_dir) |
| return _policy_config.create_trained_policy( |
| _config.get_config(args.policy.config), args.policy.dir, default_prompt=args.default_prompt |
| ) |
| case Default(): |
| return create_default_policy(args.env, default_prompt=args.default_prompt) |
|
|
|
|
| def main(args: Args) -> None: |
| policy = create_policy(args) |
| policy_metadata = policy.metadata |
|
|
| |
| if args.record: |
| policy = _policy.PolicyRecorder(policy, "policy_records") |
|
|
| |
| if args.smooth: |
| policy = SmoothedPolicy( |
| policy, |
| target_actions=args.target_actions, |
| prepend_current_state=args.prepend_current_state, |
| ) |
| logging.info( |
| "SmoothedPolicy enabled: target_actions=%d, prepend_current_state=%s", |
| args.target_actions, |
| args.prepend_current_state, |
| ) |
|
|
| |
| |
| if args.rtc_prefix: |
| policy = RTCPrefixPolicy(policy, blend=args.rtc_blend) |
| logging.info("RTCPrefixPolicy enabled: blend=%.2f", args.rtc_blend) |
|
|
| hostname = socket.gethostname() |
| try: |
| local_ip = socket.gethostbyname(hostname) |
| except socket.gaierror: |
| local_ip = "127.0.0.1" |
| logging.warning("Could not resolve hostname %s; using %s for logging only", hostname, local_ip) |
| logging.info("Creating server (host: %s, ip: %s)", hostname, local_ip) |
|
|
| server = websocket_policy_server.WebsocketPolicyServer( |
| policy=policy, |
| host="0.0.0.0", |
| port=args.port, |
| metadata=policy_metadata, |
| ) |
| server.serve_forever() |
|
|
|
|
| if __name__ == "__main__": |
| logging.basicConfig(level=logging.INFO, force=True) |
| main(tyro.cli(Args)) |
|
|