Robotics

PACE — 腿式机器人 Sim-to-Real

PACE 是什么

PACE(Precise Adaptation through Continuous Evolution,基于持续进化的精确自适应)不训练你的运动控制策略。 它只解决腿式机器人 sim-to-real 崩溃的那个关键问题:仿真器里的执行器/关节动力学和真实硬件对不上。

PACE 测量真实执行器的响应,然后用 CMA-ES(进化优化)找出一组物理参数,让 Isaac Lab 里仿真关节的运动方式和真机一致。之后你把这些参数烧进正常的强化学习训练里,策略就能顺利迁移到硬件。

它与 NVIDIA Isaac Lab 集成,并遵循其 task/environment 约定。


识别的参数

每个关节识别 5 类参数(ANYmal = 12 关节 × 4 + 1 个全局延迟 = 49 个参数):

参数含义范围(ANYmal)
Armature 电枢惯量折算的转子惯量1e-5 – 1.0 kg·m²
粘性摩擦与速度成正比的阻尼0 – 7.0 Nm·s/rad
静摩擦库仑摩擦0 – 0.5
编码器偏置关节零点偏移±0.1 rad
延迟(全局)执行器延迟0 – 10 仿真步

整个流程就是三个脚本:data_collection.pyfit.pyplot_trajectory.py


前置条件

  • 已安装 Isaac Lab(推荐 Isaac Sim 5.0+;4.5 可运行但精度下降)
  • 用 Isaac Lab 的 Python 解释器以可编辑模式安装 PACE:
git clone https://github.com/leggedrobotics/pace-sim2real.git
cd pace-sim2real
python -m pip install -e source/pace_sim2real

工作流 1 — 内置 ANYmal 例子(纯仿真)

这是验证工具链能端到端跑通的"hello world",针对仿真激励数据做参数识别,无需硬件。

conda activate <isaaclab_env>
cd path/to/pace-sim2real

# 1. 生成 chirp 激励轨迹并记录关节响应
python scripts/pace/data_collection.py \
  --task Isaac-Pace-Anymal-D-v0 --num_envs 1 \
  --min_frequency 0.1 --max_frequency 10 --duration 20 --device cuda
# → 写入 data/anymal_d_sim/chirp_data.pt

# 2. 用 CMA-ES 拟合参数(4096 个并行环境 = 种群)
python scripts/pace/fit.py \
  --task Isaac-Pace-Anymal-D-v0 --num_envs 4096 --headless
# → 写入 logs/pace/anymal_d_sim/<时间戳>/ (mean_XXX.pt, best_trajectory.pt, config.pt)

# 3. 检查拟合质量和收敛情况
python scripts/pace/plot_trajectory.py --robot_name anymal_d_sim --plot_trajectory
python scripts/pace/plot_trajectory.py --robot_name anymal_d_sim --plot_score
tensorboard --logdir logs/pace/anymal_d_sim   # 观察直方图逐渐收窄

工作流 2 — 在你自己的机器人上做 Sim-to-Real

真正的目标。和工作流 1 的关键区别:步骤 B 把仿真数据换成真实硬件数据。

   ┌─ A. 在 PACE 里建模你的机器人 ──────────────┐
   │  新建 <robot>_pace_env_cfg.py:            │
   │   • 每种执行器一个 PaceDCMotorCfg          │
   │   • PaceCfg: joint_order, bounds_params    │
   │   • Scene: 你的机器人 USD + 初始姿态        │
   │   • 注册 Isaac-Pace-<Robot>-v0             │
   └────────────────────────────────────────────┘

   ┌─ B. 采集**真实**激励数据 ──────────────────┐
   │  在真机上跑 chirp 扫频                       │
   │ (激励信号设计同 data_collection.py)         │
   │  保存测得的 q, q̇, τ → data/<robot>/         │
   │       chirp_data.pt (顺序要对齐 joint_order)│
   └────────────────────────────────────────────┘

   ┌─ C. 拟合(CMA-ES) ─────────────────────────┐
   │  python scripts/pace/fit.py \              │
   │    --task Isaac-Pace-<Robot>-v0            │
   │    --num_envs 4096 --headless              │
   └────────────────────────────────────────────┘

   ┌─ D. 验证 ──────────────────────────────────┐
   │  plot_trajectory.py + TensorBoard          │
   │  在**不同**的 chirp/幅值/PD 增益上复验,     │
   │  不能只看拟合用的那条轨迹                    │
   └────────────────────────────────────────────┘

   ┌─ E. 烧进 RL 训练 ──────────────────────────┐
   │  在你的策略环境里用 PaceDCMotor,           │
   │  用识别出的参数设置:                        │
   │   • joint_armature 电枢惯量                 │
   │   • joint_viscous_friction 粘性摩擦         │
   │   • joint_friction 静摩擦                   │
   │   • encoder_bias, max_delay                │
   │  训练策略 → 部署到硬件                       │
   └────────────────────────────────────────────┘

步骤 A — 机器人建模

复制 source/pace_sim2real/pace_sim2real/tasks/manager_based/pace/anymal_pace_env_cfg.py 作为模板。每种执行器类型定义一个 PaceDCMotorCfg(ANYmal 只有一种,你的机器人可能有多种——通过 joint_names_expr 关联对应关节)。

最关键的一点:joint_order 必须匹配你真实控制栈的关节顺序。Isaac Lab 用广度优先排序,通常和真机不同,顺序对不上会悄无声息地毁掉拟合结果。最后在 .../tasks/manager_based/__init__.py 里把环境注册为 Isaac-Pace-<Robot>-v0

参考执行器配置:

ANYDRIVE_PACE_ACTUATOR_CFG = PaceDCMotorCfg(
    joint_names_expr=[".*HAA", ".*HFE", ".*KFE"],
    saturation_effort=140.0,   # 峰值力矩能力
    effort_limit=89.0,         # 硬件力矩限制
    velocity_limit=8.5,        # 最大关节速度
    stiffness={".*": 85.0},    # P 增益 [Nm/rad]
    damping={".*": 0.6},       # D 增益 [Nm s/rad]
    encoder_bias=[0.0] * 12,   # 初始估计(全零)
    max_delay=10,              # 最大预期延迟 [仿真步]
)

bounds_params 中的参数排布(n_joints 为关节数):

  • 电枢惯量:[0 : n_joints]
  • 粘性摩擦:[n_joints : 2*n_joints]
  • 静摩擦:[2*n_joints : 3*n_joints]
  • 编码器偏置:[3*n_joints : 4*n_joints]
  • 全局延迟:[-1]

步骤 B — "真实"二字全靠这一步

仿真里的 data_collection.py 只是用来原型化激励信号。真正的 sim-to-real 要把同一个 chirp 跑在硬件上,把测得的轨迹放进 data/<robot>/chirp_data.pt

激励信号设计参数(来自脚本):

  • trajectory_direction:对称 ± 关节运动,用于抵消基座反作用力
  • trajectory_bias:每个关节的振荡中心
  • trajectory_scale:振荡幅值

chirp 信号是可靠的基线,但更高级的轨迹优化方法可以进一步提升辨识质量。

步骤 E — 部署

utils/pace_actuator.pyutils/pace_actuator_cfg.py 拷贝或导入到你的学习项目里,然后:

  1. 导入执行器模型:

    from pace_sim2real.utils import PaceDCMotorCfg, PaceDCMotor
    
  2. 初始化时用识别出的值设置 max_delayencoder_bias

  3. 训练前配置 articulation 的 joint_armaturejoint_viscous_frictionjoint_friction


工作流 3 — 迭代精修循环

sim-to-real 很少一次到位。最佳实践循环:

设定保守、物理上合理的 bounds  (查数据手册 > 理论极值)

拟合  →  在一**族**轨迹上验证(不同频率/幅值/PD 增益)

所有轨迹都拟合良好?  ──否──►  调整 bounds、改进激励信号、重新拟合
        │是

**逐步**部署到硬件,监控 力矩/温度/速度/急停

仍有残差?  ──►  在失效工况下采新的硬件数据,重新拟合

关键规则:

  • 不要对单条轨迹过拟合 —— 单个 chirp 拟合得再完美 ≠ 真机迁移成功。要在不同频率、幅值、相位偏移和 PD 增益上验证。
  • 延迟被离散成整数仿真步 → 损失曲面不连续;收敛会有噪声,用保守的延迟 bounds,盯住延迟直方图。
  • 种群大小:256–512 快速原型 · 512–2048 标准识别 · 2048–4096+ 发论文级别。
  • 永远先在仿真里验证;绝不把未验证的参数直接推给硬件。

脚本参考

脚本用途主要参数
data_collection.py采集 chirp 激励数据用于系统辨识--task--num_envs--min_frequency--max_frequency--duration--device--headless
fit.py运行 CMA-ES 参数辨识--task--num_envs--device--headless
plot_trajectory.py可视化轨迹和分数收敛--folder_name--mean_name--robot_name--plot_trajectory--plot_score

优化日志写入 logs/pace/<robot_name>/<时间戳>/:

  • config.pt —— 完整配置和数据集元信息
  • best_trajectory.pt —— 最优仿真关节轨迹
  • mean_XXX.pt —— 每次迭代的平均参数估计
  • progress.pt —— 最优分数和完整参数历史(当 save_optimization_process = True 时)

硬件安全清单

部署到真机之前:

  • ✅ 仿真行为稳定,无关节振荡
  • ✅ 力矩低于安全阈值
  • ✅ 监控温度
  • ✅ 已测试急停
  • ✅ 参数已在未见过的运动上验证

心智模型

PACE = 用进化搜索对执行器做系统辨识,让你本来就在用的仿真器变得物理真实 —— 然后照常训练和部署。

用工作流 1 熟悉工具,工作流 2 用在你的机器人上,工作流 3 收尾闭合剩余差距。


引用

@article{bjelonic2025towards,
  title         = {Towards Bridging the Gap: Systematic Sim-to-Real Transfer for Diverse Legged Robots},
  author        = {Bjelonic, Filip and Tischhauser, Fabian and Hutter, Marco},
  journal       = {arXiv preprint arXiv:2509.06342},
  year          = {2025},
  eprint        = {2509.06342},
  archivePrefix = {arXiv},
  primaryClass  = {cs.RO},
}
Previous
ORCA Hand