Skip to content

Hands On Sim2Real

konodoki edited this page Jul 23, 2026 · 11 revisions

手把手 8:把新模型从仿真带到真机

本页关注把已经能在仿真运行的 policy 接入状态、过渡和真机安全路径。

1. 先确认模型接口

至少明确:

  • 输入关节顺序与单位。
  • 四元数顺序。
  • 角速度坐标系。
  • 模型期望的控制频率。
  • 输出是绝对关节位置还是 offset。
  • kps/kds 的来源。
  • 是否需要速度命令和历史观测。

这些不一致时,状态机和过渡正确也无法保证动作正确。

2. 给 policy 强类型

使用具体类型或联合类型,不要让 _policy() 返回未知类型:

def _policy(self, ctx: BxiExample) -> "MotionPolicy":
    return cast("MotionPolicy", getattr(ctx, self.policy_attr))

3. 真机模型状态模板

class HardwarePolicyState(
    RobotControlState,
    EntryFrameProvider,
    RunningFrameProvider,
):
    def __init__(self, name: str, state_id: int, policy_attr: str):
        super().__init__(name, state_id)
        self.policy_attr = policy_attr

    def _policy(self, ctx: BxiExample) -> "MotionPolicy":
        return cast("MotionPolicy", getattr(ctx, self.policy_attr))

    def on_prepare(
        self,
        ctx: BxiExample,
        from_state: StateBehavior[BxiExample],
    ) -> None:
        policy = self._policy(ctx)
        ctx.preheat_model(
            policy,
            with_cmd_vel=True,
            cmd_vel=self.get_cmd_vel(ctx),
        )

    def get_entry_frame(self, ctx: BxiExample) -> MotorFrame:
        policy = self._policy(ctx)
        qpos = getattr(policy, "target_dof_pos", None)
        if qpos is None:
            qpos = getattr(policy, "default_dof_pos", None)
        if qpos is None:
            raise ValueError(f"state '{self.name}' policy has no entry position")
        return self._motor_frame(qpos, policy.kps, policy.kds)

    def sample_running_frame(
        self,
        ctx: BxiExample,
        dt: float,
        *,
        advance: bool,
    ) -> MotorFrame | None:
        policy = self._policy(ctx)
        qpos, _ = policy.inference_step(
            ctx.current_q,
            ctx.current_dq,
            ctx.current_quat_wxyz,
            ctx.current_omega,
            self.get_cmd_vel(ctx),
        )
        return self._motor_frame(qpos, policy.kps, policy.kds)

    def on_update(self, ctx: BxiExample, dt: float) -> None:
        if ctx.is_orientation_unsafe(ctx.current_quat_xyzw):
            ctx.request_state("zero_torque", trigger="safety")
            return
        self._apply_frame(
            ctx,
            self.sample_running_frame(ctx, dt, advance=True),
        )

如果 policy 有 timestep 或 history,必须让 advance=False 路径不提交这些变化。

4. YAML 注册

states:
  hardware_policy:
    behavior: HardwarePolicyState
    params:
      policy_attr: hardware_policy
    speed_profile: normal
    transitions:
      on_event:
        normal_event:
          to: normal
          transition: soft_switch
        zero_torque_event: zero_torque

入口第一次验证建议使用稳定的进入过渡:

hardware_policy_event:
  to: hardware_policy
  transition: first_frame_switch

确认进入帧、增益和安全退出无误后,再尝试 dual_running_blend

5. 仿真验证顺序

  1. 只加载模型,不切状态,检查输入输出 shape。
  2. 在零速度下进入状态。
  3. 使用低增益或受控环境验证进入姿态。
  4. 验证 normal 和 zero torque 返回边。
  5. 验证姿态安全条件。
  6. 验证速度 profile 的缩放和限幅。
  7. 最后验证运行混合。

6. 真机前检查

  • 急停和零力矩事件独立可用。
  • 初始姿态与模型进入帧足够接近。
  • qpos/kp/kd shape 与自由度数一致。
  • 电机增益没有仿真专用倍率。
  • 控制周期与 policy 训练频率关系明确。
  • 延迟和推理超时监控已开启。
  • 机器人周围留出足够空间。

7. 过渡安全

活动过渡期间状态机仍处理源状态 event,因此 zero torque 可以中断进入过程。目标状态会收到:

def on_prepare_cancel(
    self,
    ctx: BxiExample,
    from_state: StateBehavior[BxiExample],
) -> None:
    ...

如果准备过程申请了 GPU buffer、动作缓存或外部资源,在此释放或恢复。

8. 逐步增加速度范围

先使用保守 profile:

speed_profiles:
  hardware_safe:
    vx_scale: 0.2
    vx_min: -0.2
    vx_max: 0.2
    vy_scale: 0.2
    yaw_scale: 0.2

确认真机稳定后逐步扩大,不要一开始复用高速 profile。

9. 常见问题

进入瞬间跳变:

  • 检查 get_entry_frame() 与模型第一轮正式输出是否一致。
  • 检查预热是否使用了当前真机观测。
  • 检查增益起点配置。

仿真正常、真机振荡:

  • 检查单位、关节顺序、延迟和增益。
  • 检查状态估计噪声和滤波。
  • 检查推理周期抖动。

过渡中动作提前播放:

  • advance_to 设为 false
  • 检查状态在 advance=False 时是否仍推进内部状态。

安全事件没有立即生效:

  • 检查源状态是否配置了对应 event 边。
  • 检查 remote_eventsMotionCommands slot。
  • 检查是否使用了非零 delay。

10. 发布前检查清单

  1. 仿真完成进入、运行、退出和中断验证。
  2. 真机首次使用保守速度与增益。
  3. 所有模型状态保留 zero torque 路径。
  4. 状态能力在启动图检查中通过。
  5. 运行采样严格遵守 advance
  6. 准备资源有取消回滚。

Clone this wiki locally