跳转至

P0.D 无人机仿真环境——从 Gazebo SITL 到 GPU 并行 RL 工厂

性质:工程实践教学 | ⚪ 部分共享——仿真-真机切换的设计模式与机械臂 P0.2 共享(Bridge/Adapter 抽象),但具体仿真器(Flightmare / Isaac Sim / gym-pybullet-drones vs Gazebo / MuJoCo)和控制接口(MAVLink / CTBR vs ros2_control / JointTrajectory)完全不同。 | 难度跨度:⭐ ~ ⭐⭐⭐⭐ | 预计精读:10-14 小时

一句话定位:仿真是无人机算法的"风洞"——你 90% 的代码会先在仿真里跑通再上真机。本章不教任何一种控制或规划算法,而是教**怎么搭好那个让算法跑起来的台子**:从 Gazebo+PX4 的物理保真 SITL 闭环,到 Flightmare 的 Unity 光真渲染,到 Aerial Gym / OmniDrones 的 4096 并行 RL 工厂,讲透**物理保真度 / 渲染真实度 / 并行规模"不可能三角"**的取舍逻辑,给出一棵可直接套用的选型决策树,并拆解 sim-to-real gap 的六大来源——因为搭仿真的终极目的,是让仿真里学到的东西能搬到真机上不崩。


环境配置指南

本章涉及六个仿真器,没有一个环境能同时装下所有——Gazebo/PX4 走 ROS 2 + apt 路线,Flightmare 要 Unity + ZeroMQ,Aerial Gym/OmniDrones 要 NVIDIA Isaac(CUDA + 闭源运行时),它们的 Python/CUDA 依赖互相冲突。正确做法是按用途切分 conda/docker 环境,下文给出每条路线的独立配置。先读这一节再动手,能省掉至少一整天的依赖地狱。

系统要求

维度 入门路线(gym-pybullet-drones) SITL 路线(Gazebo + PX4) RL 路线(Aerial Gym / OmniDrones)
操作系统 Ubuntu 20.04/22.04 / macOS / Windows Ubuntu 22.04(Gazebo Harmonic 不支持 20.04) Ubuntu 22.04(Isaac 官方支持)
GPU 不需要(纯 CPU) 不需要(Gazebo 用 OpenGL 软渲染也能跑) NVIDIA RTX,显存 \(\ge\) 8 GB(4096 环境建议 \(\ge\) 16 GB)
CUDA 11.8 / 12.x(须与 PyTorch 匹配,见 v8 Ch36)
内存 8 GB 8 GB \(\ge\) 16 GB(Isaac Sim 建议 32 GB)
磁盘 2 GB 10 GB(PX4 源码 + Gazebo 资产) 30 GB+(Isaac Sim 容器约 20 GB)

为什么 RL 路线门槛陡增:gym-pybullet-drones 在 CPU 上跑单个 PyBullet 实例,笔记本即可;而 Aerial Gym / OmniDrones 的核心价值是"千级并行",这只有把整个仿真状态张量放进 GPU 显存、用 CUDA kernel 一次步进所有环境才能实现——所以它**强依赖 NVIDIA 显卡**,AMD/Intel 显卡和 macOS 完全无法运行。这是一个"想要并行红利就必须交的硬件税"。

版本兼容表

组件 推荐版本 最低版本 最高测试版本 备注
Python 3.10 3.8 3.11 Isaac Lab 当前主推 3.10;gym-pybullet-drones 需 \(\ge\)3.8
PyTorch 2.1.0 1.13 2.4.0 须与 CUDA 版本严格匹配
PX4-Autopilot v1.15 v1.13 main v1.14 起默认仿真器从 Gazebo Classic 切到 Gazebo(gz)
Gazebo Harmonic (gz-harmonic) Garden Harmonic Harmonic 仅支持 Ubuntu 22.04+;20.04 须用 Gazebo Classic 11
ROS 2 Humble Foxy Jazzy Humble 对应 Ubuntu 22.04,是当前 LTS 主力
gym-pybullet-drones 1.0.0 0.5.2 main 依赖 pybullet>=3.2gymnasium>=0.28stable-baselines3>=2.0
Flightmare master (2021) master 已停止维护,Unity 渲染端需 2020.1.10f1;物理端 C++14
Isaac Sim 4.5 / 5.1 4.2 5.1 Pegasus Simulator v5.1 对应 Isaac 5.1,v4.5.1 对应 Isaac 4.5
Isaac Gym (Preview 4) 1.0rc4 1.0rc3 1.0rc4 Aerial Gym 依赖;NVIDIA 已停更,被 Isaac Lab 取代但仍可用
Aerial Gym Simulator main (2025) main 依赖 Isaac Gym Preview 4 + Python 3.8
OmniDrones 0.1 devel 依赖 Isaac Sim + Isaac Lab(旧称 Orbit)
stable-baselines3 2.3.0 2.0.0 2.3.0 入门 RL 用;gymnasium API

版本锁定建议:仿真器生态版本耦合极强,建议锁到精确小版本(如 pybullet==3.2.6),而不是只锁大版本。原因是 PyBullet/Gazebo/Isaac 经常在小版本里改物理默认参数或 API 签名——一次 pip install -U 可能让你昨天跑通的训练今天就报错。Isaac 生态尤其敏感:Isaac Sim 4.5 和 5.1 的 Python 扩展 API 有 breaking changes,Pegasus / OmniDrones 必须配对应版本,错配会在 import 阶段直接崩。

安装步骤

路线 A:gym-pybullet-drones(入门,5 分钟,任意系统)

# 1) 建独立 conda 环境,避免污染系统 Python
conda create -n drones python=3.10 -y
conda activate drones

# 2) 克隆并安装(editable 模式,方便改源码学习)
git clone https://github.com/utiasDSL/gym-pybullet-drones.git
cd gym-pybullet-drones
pip install -e .            # 会自动拉 pybullet / gymnasium / stable-baselines3

# 3) 冒烟测试:弹出 PyBullet GUI,看 Crazyflie 悬停
python gym_pybullet_drones/examples/pid.py
# 成功标志:弹出窗口,4 个 Crazyflie 在 PID 控制下飞 8 字轨迹

路线 B:Gazebo + PX4 SITL(物理保真闭环,Ubuntu 22.04)

# 1) 拉 PX4 源码(含子模块,约 2 GB)
git clone https://github.com/PX4/PX4-Autopilot.git --recursive
cd PX4-Autopilot

# 2) 一键装齐工具链(编译器、Python 依赖、Gazebo)
bash ./Tools/setup/ubuntu.sh
# 装完务必重启终端(刷新用户组/环境变量)

# 3) 编译并启动 SITL + Gazebo(首次编译约 10-20 分钟)
make px4_sitl gz_x500
# 成功标志:Gazebo 窗口出现 X500 四旋翼,终端打印 "INFO  [commander] Ready for takeoff!"

gz_x500 这个 target 名拆解gz 表示用新 Gazebo(不是 gazebo-classic),x500 是机型(Holybro X500 V2 四旋翼,PX4 的默认参考机架)。换成 gz_x500_depth 会加一个深度相机,gz_standard_vtol 是垂直起降固定翼。target 名 = 仿真器前缀 + 机型,这个命名规律记住后你能自己推断几十个组合。

路线 C:Aerial Gym Simulator(GPU 并行 RL,Ubuntu 22.04 + NVIDIA)

# 前置:已装 NVIDIA 驱动 + CUDA(见 v8 Ch36),nvidia-smi 能看到显卡

# 1) 下载 Isaac Gym Preview 4(需在 NVIDIA 开发者官网注册下载)
#    解压后:
cd isaacgym/python && pip install -e .
# 验证:python examples/joint_monkey.py 能弹出窗口

# 2) 装 Aerial Gym
git clone https://github.com/ntnu-arl/aerial_gym_simulator.git
cd aerial_gym_simulator && pip install -e .

# 3) 跑 1024 并行位置控制示例
python aerial_gym/examples/position_control_example.py
# 成功标志:一个窗口里 1024 个四旋翼同时悬停/移动,终端打印 FPS(应达数万 steps/s)

Isaac Gym vs Isaac Lab 的命名混乱预警:NVIDIA 的无人机 RL 工具链有一段"改名史"——Isaac Gym (Preview 4) 是独立的旧 GPU 物理库(Aerial Gym 依赖它),已停止更新;Isaac Sim 是带 Omniverse 渲染的大型仿真平台;Isaac Lab(旧称 Orbit)是建在 Isaac Sim 之上的 RL 框架(OmniDrones 依赖它)。三者不是版本递进关系而是**并存的不同栈**。本章 RL 路线给两条:Aerial Gym(轻、快、几何控制)走 Isaac Gym;OmniDrones(机型多、与 Isaac Lab 生态打通)走 Isaac Sim。详细区分见 §P0.D.6。


Quick Start(5 分钟跑通最小 RL 训练)

下面这条路径让你在**最便宜的硬件(纯 CPU 笔记本)上、5 分钟内**亲手训练出一个会悬停的强化学习策略——目的是先建立"RL 训练无人机确实可行"的直觉,再回头啃原理。

conda activate drones        # 接路线 A 的环境
cd gym-pybullet-drones

# 用内置脚本训练 HoverAviary(PPO,stable-baselines3)
python gym_pybullet_drones/examples/learn.py --multiagent false
# 预期输出(节选):
[INFO] BaseAviary.__init__() loaded parameters from the drone's .urdf:
[INFO] m 0.027000, L 0.039700, ...        # Crazyflie 质量 27g、轴距 39.7mm
---------------------------------
| rollout/           |          |
|    ep_len_mean     | 178      |          # 回合长度(越长 = 飞得越久没坠毁)
|    ep_rew_mean     | -42.3    |          # 回合奖励(向 0 收敛 = 越来越稳)
| time/              |          |
|    fps             | 2100     |          # CPU 单环境约 2k steps/s
---------------------------------
...
Reached the target reward, stopping training.   # 约 3-5 万步后达标

成功标志ep_rew_mean 从 -200 左右单调上升到 -50 以内,最后弹出回放窗口看到 Crazyflie 稳定悬停在目标点。这 5 分钟你已经完整体验了"环境→PPO→策略"的最小闭环——本章后半程就是把这个闭环从 CPU 2k steps/s 拉到 GPU 4096 环境 4000 万 steps/s,并讲清楚每一步为什么这么搭。


前置自测

开始前先回答下面 5 个问题。答不出 2 题以上,建议先回前置章节补齐——本章假设你已理解四旋翼的基本动力学和 RL 训练流程,欠了账会在 §P0.D.2(控制接口)和 §P0.D.6(RL 管线)卡住。

  1. 四旋翼的 6 自由度刚体动力学方程包含哪几部分? 推力和力矩是怎么从 4 个电机转速合成出来的?为什么这个映射叫"控制分配(Control Allocation)"? (答不出 → 回 D1 微分平坦 / D2 几何控制)

  2. 强化学习里 on-policy 算法(如 PPO)的训练吞吐量为什么受"环境采样速度"主导? 如果单个环境每秒只能跑 100 步,1 亿步样本需要多久?并行 4096 个环境能把这个时间缩短到什么量级? (答不出 → 回足式 RL Part 0 / v8 Ch36 CUDA 基础)

  3. CUDA 的"主机-设备数据传输(host-device transfer)"为什么慢? 在训练循环里频繁调用 tensor.cpu() 会发生什么?这和"GPU 并行仿真把状态留在显存里"有什么关系? (答不出 → 回 v8 Ch36 CUDA 基础)

  4. 域随机化(Domain Randomization)的核心假设是什么? 它是想让仿真更逼真,还是让策略更鲁棒?这两种表述的区别为什么重要? (答不出 → 回足式 RL sim-to-real 章节 / 机械臂 P0.2)

  5. MAVLink 是什么? PX4/ArduPilot 这类飞控固件和上位机(地面站 / 机载电脑)之间靠什么协议通信?仿真时这个协议链路是怎么被"接管"的? (答不出 → 回 D8 敏捷飞行平台 §Agilicious / 本章 §P0.D.3)

参考答案要点(先自己答,再对照):

  1. 四旋翼动力学 = 平动(牛顿第二定律,\(m\ddot{\mathbf{p}} = m\mathbf{g} + \mathbf{R}\mathbf{f}\)\(\mathbf{f}\) 为机体系总推力)+ 转动(欧拉方程,\(\mathbf{J}\dot{\boldsymbol{\omega}} = \boldsymbol{\tau} - \boldsymbol{\omega}\times\mathbf{J}\boldsymbol{\omega}\))。4 个电机转速 \(n_i\)\(f_i = c_T n_i^2\) 得单桨推力,再经一个固定的 \(4\times4\) 混控矩阵(mixer)合成总推力 \(T = \sum f_i\) 和三轴力矩 \(\boldsymbol{\tau}\)。这个 \(4\times4\) 矩阵的逆映射(从期望 \(T,\boldsymbol{\tau}\) 反算每个电机转速)就是控制分配。

  2. PPO 每次策略更新前要先用当前策略采集一批轨迹(rollout),这一步是纯仿真步进,无法靠 GPU 算力加速——只能靠"同时跑很多个环境"提高吞吐。单环境 100 步/秒 → 1 亿步需 ~278 小时(11.6 天);4096 并行(假设线性加速到 ~40 万步/秒)→ 约 4 分钟。这就是 GPU 并行仿真把"周级训练"压到"分钟级"的根本原因。

  3. CPU 内存和 GPU 显存是两块物理隔离的内存,tensor.cpu() 触发一次 PCIe 总线上的数据拷贝并强制 CUDA 流同步(等所有未完成的 GPU kernel 跑完),打断异步流水线。GPU 并行仿真的核心优化正是**让物理状态、观测、动作、奖励全程留在显存里**,整个 rollout 不发生一次 host-device 传输——一旦你在训练循环里 print 一个 GPU tensor,吞吐可能掉 10-100 倍。

  4. 域随机化在每个(或每批)仿真回合里随机扰动物理参数(质量、惯量、电机时间常数、推力系数、延迟、噪声等)。它的目的**不是让仿真更逼真,而是让策略对参数变化更鲁棒**——真实世界只是随机化覆盖的参数分布中的一个点。这个区别重要在于:追求"逼真"会陷入无止境的精细建模,而追求"鲁棒"只需让随机化范围**包住**真实值即可,工程上可达且可量化。

  5. MAVLink(Micro Air Vehicle Link)是无人机领域事实标准的轻量二进制消息协议,飞控固件(PX4/ArduPilot)和地面站(QGroundControl)、机载电脑(用 MAVROS / MAVSDK)之间靠它通信。仿真时,飞控固件的传感器输入不再来自真实 IMU/GPS,而是由仿真器通过 MAVLink 的 HIL_* 系列消息(HITL)或内部 simulator 接口(SITL)注入——固件以为自己在飞,实际上"眼睛"接的是仿真器。


本章目标

学完本章后,你应该能够:

  1. **画出**任意无人机仿真器的"三层解耦"架构——物理引擎(刚体+气动)/ 渲染引擎(RGB/深度/事件相机)/ 控制接口(MAVLink/CTBR/RPM),并据此判断一个新仿真器属于哪一代、强在哪一层
  2. **解释**物理保真度、渲染真实度、并行规模三者构成的"不可能三角",并能针对一个具体任务(如视觉避障 RL、VIO 数据生成、控制器 HITL 验证)说出该优先牺牲哪个维度
  3. 从零搭通 Gazebo + PX4 的 SITL 闭环,理解 SITL/HITL 的数据注入点差异,能解释 lockstep 模式为什么对确定性复现至关重要
  4. **配置并运行**一个 4096 并行环境的 RL 训练(Aerial Gym 或 OmniDrones),看懂 num_envsdtdecimationmax_episode_length 等核心配置项,知道每个值怎么取、改了会怎样
  5. 系统列举 sim-to-real gap 的六大来源(动力学参数 / 气动残差 / 电机与执行器 / 传感器 / 延迟 / 接触),并对每一类说出至少一种缩小手段
  6. **使用决策树**为给定项目选对仿真器——状态策略 RL、视觉策略 RL、经典论文复现、入门教学各应落到哪个工具
  7. 理解 "同一份代码跑仿真和真机"的 Bridge/Adapter 设计模式(Agilicious 范式),并能说出它和机械臂 ros2_control 抽象的异同

本章知识导航

本章的知识结构是一棵以"怎么搭好无人机算法的仿真台子"为根的树。树干是"先认识工具全景,再理解取舍逻辑,然后动手搭两类代表性环境(SITL 闭环 + RL 工厂),最后回答终极问题:怎么让仿真里的东西搬到真机不崩"。

无人机仿真器全景与四代演进 (§P0.D.1 选型矩阵)
为什么没有"最好"的仿真器? (§P0.D.2 三层解耦 + 不可能三角)
    ├─→ 控制接口:CTBR/SRT/RPM 三选一 (§P0.D.3) ──→ 决定 sim-to-real 难度
    ├─→ SITL/HITL 闭环:Gazebo + PX4 (§P0.D.4) ──→ 物理保真路线
    │        └─ lockstep / MAVLink 注入点 / QGC
    ├─→ 渲染保真路线:Flightmare + 3DGS (§P0.D.5) ──→ 视觉策略路线
    ├─→ 并行 RL 工厂:Aerial Gym / OmniDrones (§P0.D.6) ──→ 训练速度路线
    │        └─ num_envs / dt / decimation / 观测-动作-奖励
    ├─→ sim-to-real gap 六大来源 (§P0.D.7) ──→ 终极问题
    │        └─ 域随机化 / 系统辨识 / 残差建模 / sim2sim 验证
    └─→ Bridge/Adapter:一份代码跑仿真和真机 (§P0.D.8)
        选型决策树 + 完整选型工作流 (§P0.D.9)
小节 主题 难度 一句话
§P0.D.1 仿真器全景与四代演进 六大仿真器,从 Gazebo 到 3DGS 的代际地图
§P0.D.2 三层解耦与不可能三角 ⭐⭐ 物理/渲染/并行三者不可兼得
§P0.D.3 控制接口 CTBR/SRT/RPM ⭐⭐⭐ 动作空间选错,sim-to-real 直接崩
§P0.D.4 Gazebo + PX4 SITL 搭建 ⭐⭐ 物理保真闭环,飞控在环
§P0.D.5 渲染保真:Flightmare 与 3DGS ⭐⭐⭐ 光真渲染喂给视觉策略
§P0.D.6 并行 RL 工厂 ⭐⭐⭐ 4096 环境,周级→分钟级
§P0.D.7 sim-to-real gap 六大来源 ⭐⭐⭐⭐ 仿真到真机为什么会崩、怎么救
§P0.D.8 Bridge/Adapter 一码两用 ⭐⭐⭐ Agilicious 范式,仿真真机零重编切换
§P0.D.9 选型决策树与工作流 ⭐⭐ 给项目选对工具的可执行流程

两条阅读线

  • 工程搭建线(会用就行):§P0.D.1 → §P0.D.4 → §P0.D.6 → §P0.D.9,重点是把环境跑起来、把训练跑通
  • 原理理解线(理解取舍):§P0.D.1 → §P0.D.2 → §P0.D.3 → §P0.D.7 → §P0.D.8,重点是搞懂"为什么这么选"和 sim-to-real

无论哪条线,§P0.D.1 和 §P0.D.2 都是必读——它们是全章选型判断的地基。


前置知识桥接

回顾 D1/D2(微分平坦与几何控制):D1 建立了四旋翼"给定平坦输出曲线即可代数恢复全部状态与控制"的核心性质,D2 给出了 SE(3) 上的几何控制器。本章不重复这些控制算法,但它们是仿真要"装进去"的东西——当你在 Aerial Gym 里看到"parallelized geometric controllers for SE(3)",那正是 D2 几何控制器的 GPU 并行版本;当你在 PX4 SITL 里看到 rate loop,那是 D2 内环的固件实现。仿真器是控制算法的容器,理解控制才能理解仿真器在仿真什么。

回顾 D8(敏捷飞行平台 §Agilicious):D8 讲透了 Agilicious 的 Pilot→Bridge→Guard 三层架构,强调"Bridge 让同一份 C++ 二进制不重编即可切换 RotorS / Flightmare / 真机"。本章 §P0.D.8 把这个模式抽象成通用的 Bridge/Adapter 设计原则,并和机械臂的 ros2_control 抽象对比——你会看到无人机和机械臂在"仿真真机切换"这件事上殊途同归。如果 D8 没读,这里只需记住一个结论:好的仿真集成不是"为仿真写一套代码、为真机写另一套",而是把"仿真还是真机"收敛成一个可替换的底层适配器

回顾足式 RL Part 0(sim-to-real 与域随机化):足式方向已经建立了"GPU 并行(IsaacGym/IsaacLab)→ PPO → 域随机化 → 真机部署"的完整心智模型。本章的 RL 部分(§P0.D.6、§P0.D.7)直接复用这套工具链——PPO、并行环境、域随机化的概念完全相通。差异只在两点:① 无人机动作空间是 4D CTBR 而非 12D 关节位置(§P0.D.3 专门讲);② 无人机 sim-to-real 的主要 gap 来源是气动残差和电机延迟,而非接触模型(§P0.D.7 专门讲)。把足式的经验迁过来,只需替换这两个无人机特有的部件。

回顾 v8 Ch36(CUDA 基础):GPU 并行仿真的全部魔法都建立在"把状态张量留在显存、用 CUDA kernel 一次步进所有环境"上。Ch36 学的 host-device 传输开销、CUDA 流、显存管理,本章 §P0.D.6 会直接用上——尤其是"为什么训练循环里不能 print GPU tensor"这个高频陷阱。


如果跳过本章会怎样

跳过 P0.D,你会在两个具体的地方撞墙。

场景一:"我的控制器在 MATLAB 里仿真完美,上真机就炸。" 你跳过了仿真器的物理保真度讨论,用一个理想的点质量模型验证了控制器,所有响应都漂亮。但真机上电机有 ~25 ms 时间常数、桨叶有 blade flapping、高速下有气动阻力——这些你的理想仿真一个都没有。结果是控制器在仿真里的增益在真机上要么发散要么迟钝。没有本章对 sim-to-real gap 六大来源(§P0.D.7)的系统拆解,你会把宝贵的真机时间浪费在"为什么仿真和真机不一样"的盲目试错上,甚至摔坏硬件。

场景二:"我的 RL 策略训练要两周,根本没法迭代。" 你跳过了 GPU 并行仿真,用 gym-pybullet-drones 的单 CPU 环境训练一个视觉避障策略。单环境 ~2000 steps/s,一个像样的策略要 5 亿步——按这个速度要训 3 天,而你的奖励函数还要调十几版。没有本章对并行 RL 工厂(§P0.D.6)的讲解,你不知道 Aerial Gym 能用 4096 并行把同样的训练压到一小时内,也不知道 CTBR 动作空间(§P0.D.3)为什么能让训出来的策略真的能上真机。你会在"训练太慢"和"训出来上不了真机"之间反复横跳。


预计阅读时间

模式 时长 适合
精读 10-14 小时 第一次系统学无人机仿真:逐节读动机→搭建→验证,亲手跑通 SITL 和一个 RL 训练,做完每节练习。建议分 3-4 次。
速读 3-4 小时 有 ROS/RL 基础、想建立全局图景:读每节的"动机"和对比表、§P0.D.2 不可能三角、§P0.D.9 决策树,跳过具体安装命令和代码走读。
速查 30-60 分钟 已搭过、回来查特定配置:直接定位到对应小节,看配置表 + 命令 + 选型决策树。

科研发展脉络

在钻进具体工具前,先把无人机仿真这条线的来龙去脉理清——知道每一代"从哪来、解决了前代什么痛点、又留下什么给后人",比孤立地记工具名有用得多。无人机仿真经历了四代演进:

阶段 年代 代表 特征
G1 Gazebo 时代 2013-2018 RotorS(ethz-asl, ~1.5k★) Gazebo Classic 插件;ODE/DART 物理;URDF 定义;所有早期论文的标配
G2 Unity/UE 光真时代 2018-2021 AirSim(Microsoft, ~18k★)、Flightmare(UZH RPG, ~1.3k★) 光真渲染 + 轻量物理;RGB/深度相机仿真;PX4 HITL/SITL
G3 GPU 并行时代 2022-2024 Aerial Gym(Isaac Gym)、OmniDrones(Isaac Sim)、gym-pybullet-drones 4096+ 并行环境;RL 训练从周→分钟;当前 RL 研究标配
G4 3DGS 渲染时代 2024-2025 FiGS/SOUS VIDE(Stanford MSL) 真实场景 3DGS 重建 → 130 fps 渲染 → 视觉策略训练;下一代范式

这条线的主线逻辑:每一代都在"补上一代最痛的短板"。G1 Gazebo 物理够用但渲染丑、跑得慢,喂不了视觉策略——于是 G2 接 Unity/UE 把渲染做到光真。G2 渲染漂亮但物理引擎是串行的,RL 采样慢得令人发指——于是 G3 把整个仿真搬进 GPU,4096 环境并行,训练从周压到分钟。G3 的渲染又是合成的、和真实场景有 gap——于是 G4 用 3DGS 把真实拍摄的场景重建成可渲染资产,让视觉策略直接在"真实场景的数字孪生"里训练。

本质洞察:四代演进不是"后一代全面取代前一代",而是**每一代在某个维度上做到极致,代价是牺牲另一个维度**。RotorS 至今仍是论文复现的金标准(因为它和 PX4 的接口最经典、最多人验证过);Flightmare 的物理其实比 Gazebo 简单,但渲染碾压;Aerial Gym 并行无敌,但渲染只是 ray-casting 的深度图。没有"最先进"的仿真器,只有"最匹配你任务"的仿真器——这正是 §P0.D.2"不可能三角"和 §P0.D.9"决策树"要解决的核心问题。

关键实验室脉络:ETH ASL(RotorS 奠基 Gazebo 范式)→ UZH RPG(Flightmare 把视觉竞速带火,也是 Agilicious 的源头)→ NTNU ARL(Aerial Gym 把几何控制 GPU 并行化)→ 清华 thu-uav(OmniDrones / SimpleFlight,把 CTBR + 零样本 sim-to-real 做成基准)→ Stanford MSL(FiGS,开 3DGS 仿真先河)。这条线和 D8/D9(敏捷飞行、RL)的实验室脉络高度重叠——做仿真器的人往往就是做控制和 RL 的人,因为只有他们最清楚仿真要"装"什么、什么 gap 最致命。


本章符号与术语约定

符号/缩写 含义 首见
SITL 软件在环(Software-In-The-Loop) §P0.D.4
HITL 硬件在环(Hardware-In-The-Loop) §P0.D.4
CTBR 集体推力 + 机体角速度(Collective Thrust & Body Rates) §P0.D.3
SRT 单旋翼推力(Single-Rotor Thrust) §P0.D.3
RPM 转速指令(Revolutions Per Minute,电机转速) §P0.D.3
MAVLink 微型飞行器通信链路协议 §P0.D.4
BEM 叶素动量理论(Blade Element Momentum),高保真气动模型 §P0.D.5
3DGS 三维高斯泼溅(3D Gaussian Splatting),新型渲染/重建 §P0.D.5
num_envs 并行环境数 §P0.D.6
decimation 物理步与控制步的比值(控制降采样因子) §P0.D.6
lockstep 锁步模式(仿真与飞控严格同步推进) §P0.D.4
DR 域随机化(Domain Randomization) §P0.D.7
SysID 系统辨识(System Identification) §P0.D.7

§P0.D.1 仿真器全景与四代演进——先认识工具箱 ⭐

模块功能与在管线中的位置

在搭任何环境之前,你得先知道工具箱里有哪些工具、各自擅长什么。这一节是全章的"地图"——把六个主流无人机仿真器摊开比一遍,让你在后面每一节深入某个工具时,始终知道它在整张地图上的位置。

动机:为什么不能"随便选一个仿真器"

设想你刚接到一个无人机项目,第一反应可能是"装个最流行的仿真器就开干"。这是新手最常踩的坑。无人机仿真器之间的差异**不是"好用/难用"的程度差异,而是"能不能做这件事"的能力差异**:

  • 你想训一个**视觉避障 RL 策略**,选了 RotorS——结果发现它渲染丑、又是单环境串行,采样慢到训不动,渲染质量也喂不出能迁移到真机的视觉特征。
  • 你想做**飞控控制器的 HITL 验证**,选了 Aerial Gym——结果发现它根本不接 PX4/MAVLink,它的设计目标是 RL 训练,不是飞控在环。
  • 你想**复现一篇 2017 年的经典规划论文**,选了 OmniDrones——结果论文的代码是基于 RotorS 的 Gazebo 接口写的,你得把整套接口重写一遍。

每一个错配都意味着几天甚至几周的返工。所以选型不是"挑一个顺眼的",而是"先想清楚任务要什么,再去地图上找那个匹配的点"。

反面:如果只用一个仿真器走天下会怎样

反事实:假设你强行用一个仿真器(比如 Gazebo)做完所有事会怎样? → 视觉策略训练:Gazebo 的 OGRE 渲染质量中等且无法 GPU 批量出图,视觉 RL 采样速度比 Flightmare 慢一个数量级。 → 大规模 RL:Gazebo 物理是 CPU 串行的,开 100 个并行实例就把 CPU 跑满了,远达不到 RL 需要的 4096 并行。 → 真实场景导航:Gazebo 只能渲染人工建模的场景,无法重现真实世界的纹理和光照细节。 结论:一个仿真器最多在地图的一个角落做到优秀。专业团队的常态是"手里有 3-4 个仿真器,按任务切换"——这正是本章要教你建立的工具意识。

理论:六大仿真器选型矩阵

下面这张表是全章最重要的参考,建议截图存下。每一列都是后续小节会深入展开的维度:

仿真器 物理保真度 渲染 GPU 并行 控制接口 飞控集成 最适场景
gym-pybullet-drones 中(多模式 6-DoF,可选阻力/地效/下洗模型) 低(OpenGL) 仅 CPU RPM/PID/velocity 无(自带简化控制器) 入门、Crazyflie 原型、多机教学
Flightmare 中(6-DoF + 可选 BEM 气动) Unity 光真,渲染达 230 Hz 部分(百级 CPU 向量化) CTBR + 自定义 PX4 SITL/HITL 视觉竞速、VIO 训练、人机交互
Aerial Gym 中(6-DoF + SE(3) 几何控制) GPU 光线投射(深度/分割) 千级~百万级并行 CTBR/RPM/几何控制 无(专注 RL) 杂乱场景避障 RL、电机指令策略
OmniDrones 中(6-DoF + 4 种机型 + 过驱动) Isaac Sim 光真 千级并行 CTBR/RPM 无(专注 RL) SimpleFlight 基准、高并行多机 RL
RotorS 中(Gazebo ODE/DART) Gazebo 低真(OGRE) 无(单实例) RPM/wrench PX4/经典控制器 经典论文复现、传感器仿真
AirSim 中(simple_flight 内置) UE 光真 单实例 RPM/velocity PX4 SITL/HITL 已归档,不推荐新项目
Pegasus(Isaac Sim) 高(Isaac PhysX) Isaac 光真 多机(非 RL 千级) PX4/ArduPilot/自定义 PX4/ArduPilot 原生 Isaac 生态下的 PX4 SITL + 光真

怎么读这张表:从右往左读最有效——先在"最适场景"列找到和你任务最像的那一行,再回头看它在物理/渲染/并行三列上的取舍是否你能接受。比如你要做"杂乱场景避障 RL",定位到 Aerial Gym 行,看到它"渲染只是 GPU 光线投射的深度图"——如果你的策略只用深度图(很多避障策略确实如此),完全够用;但如果你需要 RGB 语义,就得换 Flightmare 或加 3DGS。

几个容易混淆的点澄清

  • gym-pybullet-drones 的"多模式物理":它不是只有一个理想模型,而是提供 PYB(纯 PyBullet 刚体)、DYN(显式动力学积分)、PYB_GND(加地面效应)、PYB_DRAG(加阻力)、PYB_DW(加下洗 downwash,多机互扰)等多档保真度。教学价值极高——你能亲手看到"加上阻力模型后悬停油门怎么变"。
  • AirSim 为什么"不推荐新项目":AirSim 是微软出品、曾经的光真仿真王者(UE 渲染),但微软已于 2022 年**正式归档(archived)**该仓库,停止维护。它的精神继任者是闭源的 Project AirSim 和社区 fork(如 Colosseum)。新项目若要 UE 级光真 + PX4,更推荐 Pegasus(Isaac Sim)或仍活跃的 fork。本章把它列出来是因为大量存量论文和教程基于它,你迟早会遇到。
  • Pegasus 不是独立仿真器,而是 Isaac Sim 的"无人机扩展":它把 PX4/ArduPilot 接进 Isaac Sim,让你在 Isaac 的光真 PhysX 环境里跑飞控在环。它和 OmniDrones 都建在 Isaac Sim 上,但定位不同:Pegasus 偏"PX4 SITL + 光真渲染 + 多机",OmniDrones 偏"纯 RL 训练 + 高并行"。

历史:每一代仿真器是被什么"逼"出来的

选型矩阵给了静态的对比,但要真正理解这些工具,得知道它们各自是被什么痛点"逼"出来的——历史比参数更能解释"为什么它长这样"。

G1(RotorS,2013-2018)——被"没有标准无人机仿真"逼出来。在 RotorS 之前,做无人机的人各自用 MATLAB/Simulink 搭简化模型,没有统一的、带传感器仿真的、能接控制器的平台。ETH ASL 的 RotorS 第一次把"多旋翼模型 + IMU/相机传感器 + Gazebo 物理 + ROS 接口"打包成一个标准件,让全世界的论文能在同一个平台上复现彼此的工作。它的设计完全是 Gazebo 范式——每个传感器、每个旋翼都是一个 Gazebo 插件(plugin),通过 wrench(力/力矩)消息施加到刚体上。这个"插件化"设计直到今天仍是 Gazebo 系仿真的标志。RotorS 解决的核心痛点是"标准化",代价是继承了 Gazebo 的全部局限——渲染丑、单实例、慢。

G2(Flightmare/AirSim,2018-2021)——被"视觉算法没法在仿真里训练"逼出来。2018 年前后,视觉惯性里程计(VIO)、基于学习的视觉导航、视觉竞速开始火,但 Gazebo 的 OGRE 渲染质量喂不动这些算法——仿真图像和真实相机差太远,训出来上真机就废。微软的 AirSim 用虚幻引擎(UE)、UZH RPG 的 Flightmare 用 Unity,第一次把"游戏级光真渲染"带进无人机仿真。Flightmare 更进一步,把渲染和物理彻底解耦(ZeroMQ 异步连接,见 §P0.D.5),让渲染追求光真的同时物理仍能飞速积分。G2 解决的核心痛点是"渲染喂不动视觉算法",代价是物理引擎相对简单、并行能力有限(百级 CPU)。

G3(Aerial Gym/OmniDrones,2022-2024)——被"RL 采样太慢,训练以周计"逼出来。2021 年后,深度强化学习成为无人机控制和导航的热点,但 G1/G2 都是 CPU 串行或弱并行的——一个像样的 RL 策略要训几天到几周,根本没法迭代调参。NVIDIA 的 Isaac Gym 证明了"把整个仿真搬进 GPU、几千环境并行"能把训练从周压到分钟。NTNU ARL 的 Aerial Gym(基于 Isaac Gym)和清华的 OmniDrones(基于 Isaac Sim)把这套 GPU 并行范式带进无人机领域,分别走"极致并行+几何控制"和"光真渲染+生态完整"两条路。G3 解决的核心痛点是"RL 采样吞吐",代价是强依赖 NVIDIA GPU、渲染要么简化(Aerial Gym 只出深度)要么吃显存(OmniDrones 光真)。

G4(FiGS/3DGS,2024-2025)——被"合成渲染再逼真也和真实有 gap"逼出来。G2/G3 的渲染再好,渲的也是"人工建模的场景",和真实世界的纹理、光照、材质仍有差距,视觉策略的 sim-to-real gap 主要卡在这里。3DGS(三维高斯泼溅)反转了思路——不再"建一个场景努力像真实",而是"拍真实场景重建成可渲染孪生"(见 §P0.D.5)。Stanford MSL 的 FiGS/SOUS VIDE 用 3DGS 重建真实场景作渲染器,把视觉策略的渲染 gap 压到极小。G4 解决的核心痛点是"合成渲染的真实 gap",代价是要先去真实场景采集重建、且 3DGS 本身不含物理(要另配碰撞几何)。

本质洞察(贯穿四代的一条暗线):把四代的"痛点—代价"连起来看,有一条清晰的暗线——每一代都在"把仿真的某个维度推向真实",但推一个维度就要在另一个维度上付出代价。G1 推"标准化"(牺牲渲染速度),G2 推"渲染真实"(牺牲物理与并行),G3 推"采样吞吐"(牺牲渲染或绑定 GPU),G4 推"渲染的真实 gap"(牺牲采集成本和物理完整性)。这条暗线就是 §P0.D.2"不可能三角"的历史版本——不可能三角不是某一代的技术局限,而是仿真这件事的根本约束,每一代只是在三角的不同顶点之间重新选择落点。 理解了这条暗线,你看任何未来的新仿真器都能立刻问出关键问题:"它把哪个维度推向了真实?又在哪个维度上付了代价?"

多视角理解:用"汽车测试"类比仿真器分层

跨领域类比(标注边界):无人机仿真器的分代,像汽车工业的三种测试设施: - G1 Gazebo \(\approx\) 整车试验台架:能测动力学和控制,但环境是简化的、慢的,一次测一辆。 - G2 Flightmare/AirSim \(\approx\) 驾驶模拟器(带逼真画面):画面拟真,主要测"司机/感知系统看到画面后的反应",但底盘物理是简化的。 - G3 Aerial Gym/OmniDrones \(\approx\) 数字孪生 + 大规模蒙特卡洛:同时跑几千个虚拟测试,专为"用海量数据训练决策系统"设计,但每个实例的物理和画面都做了取舍。

像的地方:都是"用虚拟替代真实,在不同保真度上换取不同好处"。 不像的地方:汽车测试设施之间是流程上的先后关系(先台架后路试),而仿真器之间**不是先后而是并列**——你可能在 Aerial Gym 训完策略后,回到 Flightmare 做视觉验证,再到 Gazebo+PX4 做飞控在环,最后上真机。不要把这个类比延伸成"先用 G1 再用 G3"的线性流程。

配置详解:怎么快速判断一个新仿真器属于哪一代

无人机仿真生态每年都冒出新工具。与其记住每一个,不如掌握一套"三问法"快速给它在地图上定位:

问题 看什么 推断
渲染是什么引擎? OpenGL/OGRE → G1;Unity/UE → G2;GPU 光线投射/Isaac → G3;3DGS → G4 决定能不能喂视觉策略
能并行多少环境? 单实例 → G1/G2;千级 GPU → G3 决定 RL 采样速度
暴露什么控制接口? RPM/wrench → 偏经典控制;CTBR → 偏 RL;MAVLink → 偏飞控在环 决定能做什么类型的项目

举例:你看到一个新仿真器宣称"基于 Isaac Sim、支持 8192 并行、动作空间是 CTBR"——三问法立刻告诉你这是个 G3 的 RL 训练工具,和 OmniDrones 是同类,适合状态/视觉策略训练,但大概率不接 PX4(不适合飞控在环)。这套定位法比记工具名重要得多,因为工具会过时,分层逻辑不会。

⚠️ 常见陷阱

💡 概念误区:把"星标数高"等同于"最适合我的项目"
   新手想法:"AirSim 有 18k 星、Gazebo 用的人最多,那就用它们准没错"
   实际上:星标数反映的是历史影响力和社区规模,不是技术适配度。AirSim 已归档
          停止维护;Gazebo 用户多是因为它通用(机器人都用),但在无人机 RL
          这个细分场景上它既不快也不并行。星标数和"你这个具体任务的最优选择"
          几乎无关。
   正确理解:选型只看"任务-能力匹配度"(§P0.D.9 决策树),把星标数当作"社区
            支持好不好找答案"的次要参考,而非首要决策依据。
🔧 配置陷阱:以为装一个仿真器就能覆盖入门到 RL 的全流程
   错误做法:只建一个 conda 环境,想把 gym-pybullet-drones、PX4、Isaac 全装进去
   现象:pip 依赖冲突——PyBullet 要的 numpy 版本和 Isaac 要的对不上,
        Isaac Gym 锁 Python 3.8 而 Isaac Lab 要 3.10,装到一半互相覆盖
   根本原因:六大仿真器分属三条技术栈(PyBullet/Gazebo/Isaac),它们的底层
            依赖(CUDA、Python、物理库)互相不兼容
   正确做法:按用途切分独立环境——drones 环境装 gym-pybullet-drones,
            系统级装 PX4+Gazebo(apt),aerialgym 环境装 Isaac Gym 系。
            用 conda env 或 docker 隔离,别想着"一锅炖"
💡 概念误区:认为渲染质量高的仿真器物理也一定更准
   新手想法:"Flightmare/AirSim 画面这么逼真,物理肯定也比 Gazebo 准"
   实际上:渲染和物理是两套完全独立的引擎(见 §P0.D.2 三层解耦)。Flightmare
          的物理引擎其实比 Gazebo 简单(默认是轻量 6-DoF 积分),它的卖点是
          Unity 渲染而非物理保真。画面逼真 ≠ 动力学准确,这是两个正交的维度
   正确理解:判断物理保真度要看动力学模型(是否含气动残差、电机时间常数、
            blade flapping),判断渲染要看渲染引擎——两者分开评估,不能用
            一个推断另一个

练习

  1. 【选型练习·定位】 访问以下三个仓库的 README:Aerial Gym(ntnu-arl)、Pegasus Simulator、gym-pybullet-drones。用本节的"三问法"分别判断它们的渲染引擎、并行能力、控制接口,填一张三行的表。验收标准:你的判断和本节选型矩阵对应行一致。预计 30 分钟。

  2. 【对比练习·反事实】 假设你要做一个"室内无人机用深度相机避障"的 RL 项目。分别论证:① 为什么 RotorS 不合适(从并行和渲染两方面);② 为什么 AirSim 不推荐(从维护状态);③ 为什么 Aerial Gym 合适(从并行和深度渲染)。每条至少 2 句话,落到具体能力维度,不要泛泛而谈。预计 20 分钟。

  3. 【思考题·代际预判】 四代演进的主线是"每代补上一代最痛的短板"。基于这个规律,你认为 G5 最可能补什么短板?(提示:G4 的 3DGS 解决了"真实场景渲染",但它有什么仍未解决的痛点?想想"真实场景的物理交互"和"场景泛化"。)写出你的预测和理由,无标准答案。预计 15 分钟。


§P0.D.2 三层解耦与"不可能三角"——为什么没有最好的仿真器 ⭐⭐

模块功能与在管线中的位置

上一节给了"是什么"的地图,这一节回答"为什么是这样"——为什么仿真器之间是并列而非递进、为什么没有一个全能选手。答案藏在两个结构性事实里:仿真器内部是三层解耦的,而这三层在资源上构成一个不可能三角。理解了这一节,你就能自己推断任意仿真器的取舍,而不必死记选型表。

动机:从"仿真器是一个黑盒"到"仿真器是三个可拆的盒子"

新手眼里仿真器是一个整体黑盒——"我给它指令,它给我画面和状态"。但只要你想深入定制(换渲染、换物理、换控制接口),就必须把这个黑盒拆开。拆开后你会发现,几乎所有现代无人机仿真器都由**三个可独立替换的层**组成:

┌─────────────────────────────────────────────────────────┐
│  控制接口层 (Control Interface)                            │
│  ├─ 接收什么指令:RPM / CTBR / velocity / MAVLink         │
│  └─ 把指令翻译成施加到刚体上的力和力矩                      │
├─────────────────────────────────────────────────────────┤
│  物理引擎层 (Physics / Dynamics)                          │
│  ├─ 刚体动力学:6-DoF 牛顿-欧拉方程积分                    │
│  ├─ 气动模型:阻力 / 地效 / 下洗 / BEM(可选,分档)       │
│  └─ 输出:位置、速度、姿态、角速度(仿真"真值")           │
├─────────────────────────────────────────────────────────┤
│  渲染引擎层 (Rendering)                                    │
│  ├─ 把物理真值 + 场景几何 → 传感器图像                     │
│  ├─ RGB / 深度 / 分割 / 事件相机 / 点云                    │
│  └─ 引擎:OpenGL / Unity / UE / Isaac RTX / 3DGS          │
└─────────────────────────────────────────────────────────┘

本质洞察:Flightmare 论文最核心的设计贡献,就是把渲染引擎和物理引擎**彻底解耦、用 ZeroMQ 异步消息连接**——这意味着你可以让物理引擎以 200 kHz 飞速积分,同时渲染引擎以 230 Hz 出图,两者互不阻塞。理解了这个解耦,你才明白为什么 Flightmare 能"物理简单但渲染光真"——因为这两层根本是两个独立进程,各自优化各自的。这不是 Flightmare 一家的设计,而是整个现代仿真器的通用架构。

反面:如果三层不解耦会怎样

反事实:假如三层是焊死的(像早期一些一体化仿真器)会怎样? → 想换更逼真的渲染:必须连物理一起重写,因为渲染直接读物理的内部状态结构。 → 想做纯状态 RL(不需要图像):还是得跑完整的渲染管线,白白浪费 GPU。 → 想让物理跑快点(RL 采样):渲染拖住物理,因为它们在同一个同步循环里。 结论:三层解耦的价值,正是**让你按需关闭/替换某一层**。RL 训练状态策略时,Aerial Gym 可以完全不出图(关掉渲染层),把全部 GPU 算力给物理并行——这就是它快的原因之一。

理论:物理保真度 / 渲染真实度 / 并行规模的"不可能三角"

三层解耦解释了"仿真器可拆",但还没解释"为什么不能三层都做到极致"。答案是这三个维度**抢同一份计算资源**,构成一个不可能三角——你最多同时优化两个,第三个必然妥协:

              物理保真度 (Physics Fidelity)
                  高保真气动 / BEM / 电机动力学
                         ╱╲
                        ╱  ╲
                       ╱    ╲
                      ╱  你只 ╲
                     ╱  能选这 ╲
                    ╱  个三角形 ╲
                   ╱  内部的一个 ╲
                  ╱     点       ╲
                 ╱________________╲
   渲染真实度                      并行规模
  (Visual Fidelity)            (Parallelism)
   Unity/UE/3DGS               4096+ GPU 环境
选两个,牺牲第三个 代表仿真器 牺牲了什么 适合什么
物理保真 + 渲染真实 Pegasus(Isaac Sim)、AirSim 牺牲并行(单实例/少量) 飞控在环 + 光真感知验证
渲染真实 + 并行规模 OmniDrones(部分)、3DGS 系 牺牲物理保真(简化动力学换吞吐) 视觉策略大规模训练
物理保真 + 并行规模 Aerial Gym 牺牲渲染真实(只出深度/分割图,无光真 RGB) 状态/深度策略大规模训练

这个三角为什么是硬约束:本质是计算预算的分配问题。高保真物理(每步解 BEM 气动、积分电机动力学)每个环境的单步成本高 → 同样算力下能并行的环境数就少;高真实渲染(光追、全局光照)每帧 GPU 成本高 → 要么降帧率要么减并行;大规模并行要求每个环境足够轻 → 物理和渲染都得简化。三者抢的是同一块 GPU 的 FLOPs 和显存,按下葫芦浮起瓢。

多视角理解:不可能三角的双重解读

双重解读:这个不可能三角可以从两个角度理解: - 角度1(资源约束视角):三个维度抢同一份 GPU 算力,是个硬性的"零和"分配——这是上面讲的。 - 角度2(任务需求视角):好消息是,没有任何单一任务同时需要三个维度都拉满。视觉避障 RL 需要"渲染 + 并行",但它的策略对气动残差不敏感(飞得慢),物理可以简化;飞控控制器验证需要"物理 + 渲染",但它一次只测一架,不需要并行;状态策略 RL 需要"物理 + 并行",它根本不看图像,渲染可以关掉。

把两个角度合起来看,不可能三角就从"令人沮丧的限制"变成了"清晰的选型指南":先问你的任务真正需要哪两个维度,第三个大胆牺牲。这正是 §P0.D.9 决策树的理论基础。

理论-工程桥接:三层解耦如何指导你的代码组织

正因为仿真器是三层解耦的,你自己的算法代码也应该按这三层的边界来组织——这不是巧合,而是让代码能跨仿真器移植的关键。具体地: - 你的**控制器**只应依赖"物理真值"(位置/速度/姿态),不应直接调用某个仿真器的 API——这样换仿真器时控制器零修改。 - 你的**感知模块**只应依赖"传感器图像"的标准格式(如 ROS sensor_msgs/Image),不关心图像是 Unity 还是 Isaac 渲染的。 - "当前用哪个仿真器"这件事,应该收敛到一个**适配层(Bridge)**——这正是 §P0.D.8 要展开的 Agilicious 范式。

换句话说,仿真器的三层解耦架构,反过来约束了你应该怎么写算法代码:跟着仿真器的接缝切分你的模块,移植成本最低。

配置详解:怎么在实践中"关掉一层"

理解三层解耦后,最实用的技能是"按需关层"。下表给出三个仿真器关闭渲染层(做纯状态 RL)的典型配置:

仿真器 关闭渲染的配置 效果
gym-pybullet-drones gui=False(构造 Aviary 时) 不开 PyBullet GUI,无渲染开销,采样提速
Aerial Gym headless=True + 任务不含 camera sensor 完全跳过 GPU 光线投射,全部算力给物理
OmniDrones / Isaac headless=True + enable_cameras=False 不初始化 RTX 渲染管线,显存省一大块

本质洞察(关层的代价):关掉渲染层不是"免费提速"——它意味着你的策略**只能用状态信息(位置/速度/姿态),看不到图像**。如果你的最终任务需要视觉(避障、目标跟踪),训练时关了渲染,部署时却要看图像,就会有巨大的 observation gap。所以"关哪一层"必须由**最终部署形态**倒推:部署看图像 → 训练就不能关渲染;部署只用状态/位置(如轨迹跟踪)→ 训练大胆关渲染换速度。

⚠️ 常见陷阱

💡 概念误区:认为"提高仿真保真度"总是好的、越高越好
   新手想法:"既然要 sim-to-real,那物理和渲染当然越逼真越好"
   实际上:保真度的提升有边际递减且有代价。把物理从 6-DoF 升到含 BEM 气动,
          单步成本可能翻数倍,并行环境数砍半,训练时间翻倍——但如果你的策略
          飞得慢(气动残差本就可忽略),这份保真度对最终性能毫无帮助,纯属
          浪费算力。盲目堆保真度是新手常见的"伪精确"陷阱
   正确理解:保真度要"按需匹配任务的敏感维度"。先问"我的任务对哪个物理效应
            敏感",只在那个维度上加保真度。飞得慢的避障任务不需要 BEM,飞
            100km/h 的竞速任务才需要(见 §P0.D.7)
🔧 配置陷阱:做纯状态 RL 却忘了关渲染,白白拖慢训练
   错误做法:用 OmniDrones 训一个轨迹跟踪策略(只用位置/速度观测),但用默认
            配置启动,渲染层全程开着
   现象:训练 FPS 远低于预期,GPU 显存占用居高不下,开不了更多并行环境
   根本原因:默认配置初始化了 RTX 渲染管线,每步都在出图——但你的观测里根本
            没用到图像,这些渲染计算全是浪费
   正确做法:状态策略训练务必 headless=True 且关闭相机传感器。验证方法:对比
            开/关渲染的 FPS,关掉后应有可观提速;用 nvidia-smi 看显存应明显下降
⚠️ 编程陷阱:跨仿真器移植时,控制器直接耦合了某仿真器的状态接口
   错误做法:控制器里写死 env.get_pybullet_state() 这类某仿真器专有 API 来读状态
   现象:想从 gym-pybullet-drones 换到 Aerial Gym 时,控制器整个报错,因为
        Aerial Gym 根本没有这个 API,状态是 GPU tensor 而非 PyBullet 返回值
   根本原因:违反了三层解耦原则——控制器应只依赖"物理真值"的抽象(位置/速度/
            姿态的标准结构),而不是某个仿真器的具体接口
   正确做法:在控制器和仿真器之间加一个薄适配层,把各仿真器的状态统一成你自己
            的 State 结构。控制器只认 State,不认仿真器(详见 §P0.D.8)

练习

  1. 【分析练习·三角定位】 在不可能三角里标出本章六大仿真器的位置(每个画一个点,靠近它优化的两个顶点、远离它牺牲的顶点)。然后用一句话说明每个点"牺牲了哪个维度"。验收标准:和 §P0.D.1 选型矩阵的物理/渲染/并行三列一致。预计 25 分钟。

  2. 【配置练习·关层提速】 用 gym-pybullet-drones 跑同一个 HoverAviary 训练两次:第一次 gui=True,第二次 gui=False。记录两次的 FPS(终端会打印)。计算提速倍数。验收标准:gui=False 应明显更快(通常 1.5-3x)。思考:如果是 GPU 仿真器,关渲染的提速会更大还是更小?为什么?预计 30 分钟。

  3. 【综合思考题·任务驱动选维度】 对下面三个任务,分别说出"需要哪两个维度、可以牺牲哪个",并据此从六大仿真器里选一个:① 训练一个 100km/h 竞速的状态策略;② 训练一个室内 RGB 视觉目标跟踪策略,需要真实纹理;③ 验证一个新设计的 MPC 控制器在 PX4 上的表现。每个任务的论证至少 3 句话。预计 30 分钟。


§P0.D.3 控制接口:CTBR / SRT / RPM——选错动作空间,sim-to-real 直接崩 ⭐⭐⭐

模块功能与在管线中的位置

控制接口是三层解耦里最容易被忽视、却最影响 sim-to-real 成败的一层。它回答一个问题:你的算法(控制器或 RL 策略)输出什么,仿真器/真机接收什么? 这个"什么"的选择——是直接给电机转速(RPM)、给每个桨的推力(SRT)、还是给"集体推力 + 机体角速度"(CTBR)——决定了你的策略要不要操心电机动力学、ESC 延迟、桨叶气动这些最难仿真准的东西。选对了,sim-to-real 顺滑;选错了,仿真里完美的策略上真机直接发散。

动机:从"无人机怎么被控制"说起

先回到物理。一架四旋翼最底层的执行器是 4 个电机,每个电机带一个桨,转起来产生推力。理论上,控制无人机就是控制这 4 个电机的转速。但"控制 4 个电机转速"这个动作空间,离"我想让无人机往哪飞"隔着好几层:

你的意图:往前飞
    ↓ ???(怎么翻译?)
4 个电机各转多少 RPM
    ↓ 电机+ESC 动力学(~25 ms 时间常数,有延迟)
4 个桨各产生多少推力(还受桨叶气动、来流速度影响)
    ↓ 混控矩阵
机体受到的总推力 T + 三轴力矩 τ
    ↓ 刚体动力学
加速度、角加速度
    ↓ 积分
无人机的运动

问题来了:你的策略应该在这条链的**哪一层**插入?插得越靠下(越接近电机),策略要替你操心的底层物理就越多;插得越靠上(越接近意图),底层物理就被"外包"给了别的模块。这就是 RPM / SRT / CTBR 三种接口的本质区别——它们是策略接入控制链的三个不同高度的切入点

反面:如果让策略直接输出电机转速(最低层接口)会怎样

反事实:假设你训一个 RL 策略,让它直接输出 4 个电机的 RPM(或等价的单桨推力 SRT),会发生什么? → 策略必须**学会补偿电机动力学**——电机从当前转速到目标转速有 ~25 ms 的过渡,策略得隐式建模这个延迟。 → 策略必须**学会补偿 ESC(电调)非线性**——油门到转速不是线性的。 → 策略必须**学会补偿 blade flapping**(桨叶在高速来流下的挥舞效应)和来流相关的推力变化。 → 这些效应**恰恰是仿真里最难建准的**(见 §P0.D.7)。策略在仿真里学到的补偿,是针对"仿真版电机动力学"的,和真机的电机动力学有 gap → 上真机直接崩

这就是为什么早期"端到端 RL 直接出电机指令"的工作 sim-to-real 极其困难——策略把整条控制链的底层物理都背在自己身上,而这些物理仿真又仿不准。

理论:三种控制接口的完整对照

接口 全称 动作维度 策略输出 谁来处理电机动力学 sim-to-real 难度
RPM Revolutions Per Minute 4D 4 个电机的目标转速 策略自己(隐式)
SRT Single-Rotor Thrust 4D 4 个桨的期望推力 策略自己(隐式)
CTBR Collective Thrust & Body Rates 4D 1D 总推力 + 3D 机体角速度 固件 rate loop(2-8 kHz PID)

CTBR 的 4 个分量是什么

\[ \mathbf{a}_{\text{CTBR}} = [\,c,\ \omega_x,\ \omega_y,\ \omega_z\,]^\top \]
  • \(c\)质量归一化的集体推力(collective thrust),单位是加速度(m/s\(^2\))。物理意义是"我想让无人机沿机体 z 轴产生多大的加速度"。归一化后与无人机质量无关,这是它鲁棒的关键之一。典型范围 \([0, 2g]\)(0 到两倍重力加速度)。
  • \(\omega_x, \omega_y, \omega_z\)期望的机体三轴角速度(body rates),单位 rad/s。物理意义是"我想让无人机以多快的角速度绕机体 x/y/z 轴旋转"。典型范围 \([-10, 10]\) rad/s。

CTBR 怎么落到电机:策略输出 CTBR 后,不是策略、而是飞控固件里一个 2-8 kHz 的高频 rate PID 回路(Betaflight / PX4 的 rate loop),把"期望角速度"和"IMU 实测角速度"的误差转成力矩指令,再经混控矩阵 + 电机模型落到每个电机。关键在于:电机动力学、ESC 延迟、blade flapping 这些难仿真的东西,全被这个 2-8 kHz 的固件回路"吃掉"了,策略根本看不见它们。

为什么 CTBR 赢——Kaufmann ICRA 2022 的基准结论

这不是拍脑袋的选择,而是有严格基准支撑的。Kaufmann 等人在 ICRA 2022 系统对比了四种动作空间(含 SRT、CTBR、线速度、位置)的 sim-to-real 表现,结论很硬:

本质洞察:CTBR 之所以是无人机 RL 的"正确动作空间",本质是它**把控制问题在"难仿真的快变量"和"易仿真的慢变量"之间切了一刀**——电机时间常数(~25 ms)、ESC 延迟、桨叶气动属于"快变量",仿真误差大,CTBR 把它们外包给 2-8 kHz 的固件 rate loop;留给策略的是慢得多的刚体姿态/位置问题(时间尺度 ~100 ms),这部分刚体动力学仿真很准。策略只学仿得准的那部分,仿不准的那部分交给固件——这就是 sim-to-real 鲁棒性的来源。

实验结果:在质量/惯量扰动下,SRT(单桨推力)策略急剧崩溃(因为它隐式学了针对特定电机动力学的补偿,扰动一来补偿就错),而 CTBR 保持稳定(因为电机层的补偿由固件实时做,策略只管刚体层,对参数扰动天然鲁棒)。

多视角理解:CTBR 对照足式 RL 的"关节位置"

这是从足式 RL 迁移到无人机 RL 最关键的概念对照——如果你做过足式,这张表能让你瞬间理解无人机 RL 的动作空间设计:

维度 足式 RL 无人机 RL
动作空间 12D 关节位置偏移 \(\times\) action_scale(0.25) 4D CTBR(质量归一化总推力 + 3 轴机体角速度)
内环(谁吃底层动力学) PD 驱动器 200 Hz–1 kHz Betaflight/PX4 rate PID 2–8 kHz
action_scale 0.25 rad(约 \(\pm\)14°) 推力 \([0, 2g]\) + 角速度 \([-10, 10]\) rad/s
物理意义 关节角度目标 质量归一化加速度方向 + 旋转速度
难仿真的底层被谁吃掉 关节摩擦/电机由 PD 内环吃 电机/ESC/桨叶由 rate loop 吃

跨领域类比(标注边界):CTBR 之于无人机 RL,就像关节位置目标之于足式 RL——两者都是"中层指令":高于电机/力矩(不直接碰最难仿的执行器动力学),低于轨迹/落脚点(仍保留足够的控制带宽)。 像的地方:都依赖一个高频底层回路(无人机的 rate loop / 足式的 PD 驱动器)来吃掉难仿真的执行器动力学,让策略只管仿得准的刚体层。 不像的地方:足式的关节位置是"位置"指令(积分两次到力矩),无人机的 body rate 是"速度"指令(微分一次到角加速度)——一个是位置环外包,一个是角速度环外包。另外足式 12D(每条腿 3 关节\(\times\)4 腿),无人机只有 4D,控制冗余度差异巨大。不要把"维度"这个类比延伸过头。

配置详解:在各仿真器里选控制接口

仿真器 默认接口 怎么切到 CTBR 备注
gym-pybullet-drones RPM(ActionType.RPM ActionType.PID 或自定义 也支持 ONE_D_RPMVEL 等多档,教学友好
Aerial Gym 可配(几何控制 / CTBR / RPM) 在 task config 里选 control_mode 默认提供 SE(3) 几何控制器作为底层
OmniDrones 可配(多 control mode) controller 选 CTBR SimpleFlight 基准就用 CTBR
Flightmare CTBR + 自定义 原生 CTBR 接口 视觉竞速论文标配
PX4 SITL(offboard) 多种(含 CTBR) MAVROS 发 AttitudeTarget(带 body_rate + thrust) 真机部署时同一接口,见 §P0.D.8

理论-工程桥接:CTBR 不只是 RL 的选择,也是真机部署的接口标准。当你训完一个输出 CTBR 的策略,部署到 PX4 真机时,是通过 MAVROS 的 AttitudeTarget 消息发送 (thrust, body_rate_x/y/z)——和仿真里完全一样的 4 个数。正因为接口一致,"仿真训练→真机部署"才能做到策略权重直接搬过去不改接口(§P0.D.8)。如果你训练用 RPM、部署用 CTBR,中间还要插一层转换,gap 又多一处。接口一致性本身就是 sim-to-real 的一部分。

⚠️ 常见陷阱

💡 概念误区:认为"动作空间越底层(越接近电机)越精确、越好"
   新手想法:"直接控制电机 RPM 不是最精确吗?中间少了翻译损失"
   实际上:底层接口确实"无翻译损失",但代价是策略必须自己隐式建模电机动力学、
          ESC 延迟、桨叶气动——而这些恰恰是仿真最仿不准的部分。策略学到的是
          "仿真版底层物理"的补偿,和真机不匹配,sim-to-real 直接崩。精确性
          (仿真内)和鲁棒性(跨仿真-真机)是两回事
   正确理解:sim-to-real 要的是鲁棒性,不是仿真内精确性。CTBR 故意"不那么底层",
            把难仿的快变量外包给固件,换来跨域鲁棒——这是 Kaufmann ICRA 2022
            的核心结论
⚠️ 编程陷阱:CTBR 的推力分量忘了做质量归一化,导致换机型策略失效
   错误做法:策略输出的"推力"用绝对力(牛顿),训练时用 27g 的 Crazyflie 参数
   现象:策略在训练机型上工作,换到 250g 的机型(或真机质量与仿真略有偏差)
        时,同样的推力指令产生完全不同的加速度,策略行为异常
   根本原因:绝对推力和质量耦合——同样 1N 推力,27g 无人机和 250g 无人机的
            加速度差近 10 倍。策略隐式记住了训练机型的质量
   正确做法:CTBR 的推力分量用"质量归一化推力"(单位 m/s²,即期望加速度),
            与质量解耦。这样策略输出的是"我想要多大加速度",落到不同质量的
            机体时由底层自动换算成对应的力。这是 CTBR 鲁棒性的关键设计之一
🔧 配置陷阱:角速度指令范围设得过大,rate loop 跟不上反而震荡
   错误做法:把 body rate 动作范围设成 [-30, 30] rad/s 以为"控制权限更大更灵活"
   现象:策略学会输出极端角速度指令,但真机的 rate loop(受电机带宽限制)跟不上,
        实际角速度远小于指令,且在指令突变处剧烈震荡
   根本原因:body rate 指令范围必须落在底层 rate loop(和电机带宽)能实际跟踪的
            范围内。超出范围的指令是"空头支票",固件无法兑现,反而引入跟踪误差
   正确做法:把 body rate 范围设在 [-10, 10] rad/s 量级(典型四旋翼 rate loop
            可跟踪范围),并在训练奖励里加动作平滑项(惩罚相邻动作的差分),
            避免策略输出物理上无法实现的突变指令(见 §P0.D.7 的动作平滑)

练习

  1. 【对比练习·接口推演】 不查表,自己推一遍:如果一个策略输出 SRT(4 个桨推力),它在 sim-to-real 时会暴露哪些"难仿真的底层物理"?CTBR 把这些中的哪些外包给了固件?列一张两列对照表(SRT 暴露 vs CTBR 外包)。验收标准:至少覆盖电机时间常数、ESC 延迟、桨叶气动三项。预计 20 分钟。

  2. 【配置练习·动作空间切换】 用 gym-pybullet-drones 跑 HoverAviary,分别用 ActionType.RPMActionType.PID(更高层)各训一个悬停策略,对比:① 哪个收敛更快?② 哪个最终奖励更高?记录两者的训练曲线。思考:为什么更高层的动作空间通常更容易训练?(提示:动作空间越高层,策略要学的"映射复杂度"越低。)预计 40 分钟。

  3. 【综合思考题·跨章迁移】 结合足式 RL 的经验回答:足式 RL 用 12D 关节位置、无人机 RL 用 4D CTBR,两者背后是同一个设计原则。用一句话概括这个原则,然后分别说明:① 足式里"谁"扮演了无人机 rate loop 的角色?② 如果有人非要让足式机器人直接输出关节力矩(最底层),会遇到和无人机"直接输出 RPM"类似的什么问题?预计 25 分钟。


§P0.D.4 Gazebo + PX4 SITL 搭建——物理保真闭环,飞控在环 ⭐⭐

模块功能与在管线中的位置

这一节走"物理保真路线"的代表搭建:把真实的飞控固件(PX4)和物理仿真器(Gazebo)连成一个闭环,让飞控以为自己在控制一架真无人机。这是无人机工程的"基本功"——几乎所有涉及 PX4/ArduPilot 的项目(消费级、行业级无人机的主流飞控)都要先在这个闭环里验证。它的价值不在"快"(它是单实例、不并行的),而在"真"——飞控固件的代码原封不动地跑,状态估计、模式切换、故障保护逻辑全都在环。

动机:为什么要让"真飞控"在环

设想你写了一个新的轨迹跟踪控制器,想在无人机上跑。你有两个选择:

  • 选择 A:自己写一个简化的无人机模型(点质量 + 理想推力),在 Python 里仿真控制器。
  • 选择 B:让真实的 PX4 固件跑在仿真里,你的控制器通过 PX4 的标准接口(offboard 模式)控制它。

选择 A 快,但你验证的只是"控制器 + 理想模型"。真机上,你的控制器指令要经过 PX4 的状态估计器(EKF2)、模式管理(commander)、混控、故障保护——这一整套固件逻辑选择 A 完全没覆盖。等你上真机才发现 EKF2 估计有延迟、commander 在某个边界拒绝了你的指令、故障保护误触发——这些 bug 选择 A 永远暴露不出来。

选择 B(SITL)让真飞控在环,你验证的是"控制器 + 真固件 + 物理模型"的完整闭环。代价是慢、单实例、要装一堆东西。但对于会上真机的项目,这个代价必须付——因为飞控固件的复杂逻辑,只有让它真的跑起来才能验证。

本质洞察:SITL 的价值不是"仿真物理"(那是 Gazebo 的事),而是"仿真整个飞控软件栈"。PX4 在 SITL 里运行的是**和真机几乎完全相同的固件代码**——同样的 EKF2、同样的 commander、同样的参数系统。所以 SITL 暴露的是"软件层"的问题(估计、模式、保护逻辑),这正是简化的理想仿真永远覆盖不到的盲区。

反面:SITL 和 HITL 的区别——数据从哪注入

SITL 之外还有一个 HITL(硬件在环),新手常搞混。它们的本质区别是**仿真数据注入飞控软件栈的"位置"不同**:

SITL(软件在环):
  Gazebo 物理 ──传感器真值──→ [PX4 固件运行在 PC 上]──控制指令──→ Gazebo
                          固件是为仿真专门编译的(posix 版)
                          数据注入点"深":执行了更多标准系统代码

HITL(硬件在环):
  Gazebo 物理 ──HIL_* MAVLink 消息──→ [PX4 固件运行在真 Pixhawk 上]
                          固件是真机固件(和飞真无人机一模一样)
                          数据注入点"浅":commander/sensors 进入 HIL 模式,
                                          绕过部分正常功能,直接吃仿真传感器
维度 SITL HITL
固件跑在哪 开发 PC(专门编译的 posix 版) 真 Pixhawk 硬件(真机固件)
数据注入点 深(执行更多标准系统代码) 浅(commander/sensors 有 HIL 模式,绕过部分功能)
测的是什么 软件逻辑 + 物理 硬件特性 + 真实计算资源约束
典型用途 日常开发、CI 自动测试、算法验证 上真机前的最后一道关、测硬件相关问题
连接方式 进程间(本机 UDP) USB/UART 连真硬件

多视角理解(什么时候用哪个): - 角度1(开发阶段视角):SITL 用于"日常迭代"——它快、可脚本化、能进 CI,你 95% 的时间都在用它。HITL 用于"临门一脚"——上真机前,把固件刷进真 Pixhawk,验证"在真实计算资源(有限的 MCU 算力、真实的传感器驱动)下还跑得动吗"。 - 角度2(覆盖盲区视角):SITL 覆盖不到"硬件相关"的问题——比如你的控制器在 PC 上跑很快,但烧进 Pixhawk 的 480MHz MCU 后算不过来、超了控制周期。HITL 专门暴露这类问题。 不像的地方:SITL 在 PC 上跑,算力近乎无限,掩盖了真机的算力约束;HITL 用真硬件,但 Gazebo 仍在 PC 上,所以它测的是"真固件硬件 + 仿真物理",物理仍是仿的。两者都不能替代真飞。

理论:SITL 闭环的完整数据流

┌──────────────────────────────────────────────────────────────┐
│  Gazebo(gz-sim)                                               │
│  ├─ 物理引擎:积分 X500 四旋翼的 6-DoF 动力学                    │
│  ├─ gz_x500 模型:含 4 个旋翼关节 + IMU/GPS/磁力计传感器插件    │
│  └─ 输出:IMU/GPS/气压 真值(加可选噪声)                       │
│         ↕  (gz-transport / MAVLink over UDP)                   │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  PX4 SITL(px4 进程,posix 编译)                          │ │
│  │  ├─ EKF2:融合 IMU/GPS → 状态估计(位置/速度/姿态)        │ │
│  │  ├─ commander:模式管理(manual/offboard/...)+ 故障保护  │ │
│  │  ├─ 位置/姿态/rate 控制器(你也可以从外部 offboard 接管)  │ │
│  │  └─ 混控 + 电机模型 → 4 个旋翼转速指令                     │ │
│  └─────────────────────────────────────────────────────────┘ │
│         ↕  (MAVLink over UDP, 默认端口 14540/14580)            │
└──────────────────────────────────────────────────────────────┘
         ↕  MAVLink
┌──────────────────────┐   ┌──────────────────────────────────┐
│  QGroundControl      │   │  你的代码(MAVROS / MAVSDK / pymavlink)│
│  (地面站,看状态/发指令)│   │  (offboard 控制、读状态、跑你的算法)  │
└──────────────────────┘   └──────────────────────────────────┘

配置详解:lockstep 模式——为什么对确定性复现至关重要

PX4 SITL 默认启用 lockstep(锁步)模式,这是个容易被忽略但极重要的配置:

模式 行为 优点 缺点
lockstep 开(默认) 仿真和飞控**严格同步推进**:Gazebo 每步等飞控处理完才推进,反之亦然 完全确定性可复现——同样输入永远同样输出;不受 PC 算力波动影响 不能比实时快(受限于飞控处理速度);若飞控某步卡住,仿真也停
lockstep 关 仿真按真实墙钟时间跑,飞控尽力跟上 可比实时快;更接近真机的异步特性 不确定——PC 卡一下,时序就变,难以复现 bug

本质洞察:lockstep 解决的是仿真界一个根本矛盾——真实时间 vs 仿真时间的解耦。真机上,传感器和控制器在物理时间里异步运行,谁也不等谁。但仿真里如果也这样,PC 的算力波动(后台开了个浏览器)就会改变时序,让 bug 无法复现。lockstep 让仿真时间"按步走而非按墙钟走"——Gazebo 推进一个 dt,必然等 PX4 处理完这个 dt,时序完全由步数决定、与真实耗时无关。这是"仿真可复现"和"仿真像真机的异步性"之间的取舍:调试和 CI 要可复现(开 lockstep),压力测试要逼真异步(关 lockstep)。

代码走读:用 MAVSDK 让无人机起飞悬停(offboard 控制最小示例)

SITL 跑起来后,最常见的需求是"用代码控制它"。下面用 MAVSDK-Python(PX4 官方推荐的高层 SDK)实现起飞→悬停→降落,逐行讲解:

import asyncio
from mavsdk import System
from mavsdk.offboard import OffboardError, VelocityBodyYawspeed

async def run():
    # 1) 连接到 SITL。udpin://0.0.0.0:14540 是 PX4 SITL 默认对外的 offboard 端口
    drone = System()
    await drone.connect(system_address="udpin://0.0.0.0:14540")

    # 2) 等待飞控连接确认(is_connected 为 True)
    async for state in drone.core.connection_state():
        if state.is_connected:
            print("已连接到飞控")
            break

    # 3) 等待 GPS 定位和 home 位置就绪——没这一步直接 arm 会被 commander 拒绝
    async for health in drone.telemetry.health():
        if health.is_global_position_ok and health.is_home_position_ok:
            print("定位就绪,可以起飞")
            break

    # 4) 解锁(arm)并起飞到默认高度
    await drone.action.arm()           # arm = 让电机可转;真机这一步要格外小心
    await drone.action.takeoff()       # 起飞到参数 MIS_TAKEOFF_ALT(默认 2.5m)
    await asyncio.sleep(5)             # 等它爬升稳定

    # 5) 切 offboard 模式前,必须先发一个 setpoint,否则切换会失败
    #    (PX4 要求 offboard 流在切换前已有数据流,防止"切进去没指令")
    await drone.offboard.set_velocity_body(
        VelocityBodyYawspeed(0.0, 0.0, 0.0, 0.0))  # 机体系零速度=悬停
    try:
        await drone.offboard.start()   # 正式接管,从此你的代码完全控制无人机
    except OffboardError as e:
        print(f"offboard 启动失败: {e._result.result}")
        await drone.action.disarm()
        return

    # 6) 发一段机体系速度指令:以 1 m/s 向前飞 5 秒
    await drone.offboard.set_velocity_body(
        VelocityBodyYawspeed(1.0, 0.0, 0.0, 0.0))   # vx=1, vy=0, vz=0, yaw_rate=0
    await asyncio.sleep(5)

    # 7) 停下并降落
    await drone.offboard.set_velocity_body(
        VelocityBodyYawspeed(0.0, 0.0, 0.0, 0.0))
    await asyncio.sleep(2)
    await drone.action.land()

if __name__ == "__main__":
    asyncio.run(run())

逐行要点解析

  • 第 2 行 udpin://0.0.0.0:14540:这是 PX4 SITL 暴露给外部 offboard 程序的标准端口。14540 给 offboard SDK,14580 给 QGroundControl——记住这两个端口能省很多"连不上"的排查时间。
  • 第 3 步等定位(关键,新手最常漏):PX4 的 commander 有严格的预飞检查(preflight check)——没有有效的全局定位和 home 位置,arm() 会被直接拒绝并报 "Arming denied"。SITL 里 GPS 真值很快就绪,但代码上必须等。这正是 §动机里说的"真固件逻辑在环"——理想仿真不会有这个检查,SITL 会。
  • 第 5 步先发 setpoint 再 start(offboard 协议要求):PX4 规定,切入 offboard 模式前,offboard setpoint 流必须已经在发送(频率 > 2 Hz)。否则 PX4 认为"你要接管但没准备好指令",拒绝切换或立即触发故障保护。这是个固件层的硬约束,很多人卡在这里。
  • 第 6 步机体系速度VelocityBodyYawspeed(vx, vy, vz, yaw_rate) 是机体坐标系下的速度指令——vx=1 是"沿机头方向 1 m/s",不管机头朝哪。如果要世界系(北东地),用 set_velocity_ned

运行验证:怎么确认 SITL 闭环真的在工作

# 终端 1:启动 SITL + Gazebo
cd PX4-Autopilot && make px4_sitl gz_x500

# 终端 2:启动 QGroundControl(下载 AppImage 后)
./QGroundControl.AppImage
# 验证点 1:QGC 自动连上,地图上出现无人机图标,姿态仪表跳动

# 终端 3:跑上面的 MAVSDK 脚本
python takeoff_hover.py
# 验证点 2:Gazebo 里无人机起飞、前飞、降落;
#          QGC 的高度曲线和速度曲线与脚本指令吻合;
#          PX4 终端打印模式切换日志:MANUAL → TAKEOFF → OFFBOARD → LAND

成功标志三连:① Gazebo 里看到无人机按脚本动作;② QGC 遥测曲线与指令吻合;③ PX4 终端的模式切换日志符合预期。三者都对,才说明"物理-固件-你的代码"三方闭环真正打通了——任何一环断了,都会在这三个验证点之一暴露。

⚠️ 常见陷阱

🔧 配置陷阱:在 Ubuntu 20.04 上尝试用 Gazebo Harmonic / make gz_x500
   错误做法:在 20.04 上 git pull 最新 PX4 然后 make px4_sitl gz_x500
   现象:编译或启动时报 Gazebo(gz-sim)找不到 / 版本不兼容;apt 装不上 gz-harmonic
   根本原因:Gazebo Harmonic(及 Garden)不支持 Ubuntu 20.04——20.04 的系统库
            版本太老。PX4 在 20.04 上的官方仿真器是 Gazebo Classic 11
   正确做法:要么升级到 Ubuntu 22.04 用 Gazebo Harmonic(make px4_sitl gz_x500),
            要么在 20.04 上用 Gazebo Classic(make px4_sitl gazebo-classic_iris)。
            注意两者 target 名不同:gz_ 前缀是新 Gazebo,gazebo-classic_ 是旧的
💡 概念误区:以为 SITL 跑通 = 真机一定能飞(混淆"软件验证"和"物理验证")
   新手想法:"SITL 里控制器表现完美,PX4 没报任何错,上真机肯定没问题"
   现象:上真机后控制器震荡或发散,明明 SITL 里好好的
   根本原因:SITL 验证的是"软件逻辑 + 仿真物理"。仿真物理用的是理想化的电机/
            气动模型,没有真机的电机延迟、桨叶气动残差、传感器真实噪声、机架
            振动。这些物理 gap(见 §P0.D.7)SITL 覆盖不到
   正确理解:SITL 通过 = "软件层没问题",不等于"物理层没问题"。完整流程应是
            SITL(软件)→ HITL(硬件软件)→ 真机小范围测试 → 逐步放开。SITL
            是必要不充分条件
⚠️ 编程陷阱:offboard 控制时 setpoint 发送频率过低,触发故障保护
   错误做法:offboard 模式下,控制循环里 sleep(0.5),即 2 Hz 发 setpoint
   现象:飞机刚切进 offboard 就自己退出(fallback 到 hold/land),日志报
        "Offboard mission stopped" 或类似
   根本原因:PX4 要求 offboard setpoint 至少 2 Hz(实际建议 ≥10 Hz,留余量)。
            一旦某两个 setpoint 间隔超过约 0.5 秒,commander 判定"offboard 信号
            丢失",触发故障保护自动接管——这是防止"上位机死机后无人机失控"的
            安全机制
   正确做法:offboard 控制循环频率设到 20-50 Hz(间隔 20-50ms),远高于 2 Hz
            下限。即使你的算法算得慢,也要用上一帧的指令"垫"着持续发送,保持
            setpoint 流不断

练习

  1. 【管线搭建·SITL 闭环】 按路线 B 装好 PX4 + Gazebo,跑通 make px4_sitl gz_x500 并连上 QGroundControl。然后运行本节的 MAVSDK 起飞脚本。验收标准:Gazebo 里无人机完成起飞→前飞→降落,QGC 遥测曲线与指令吻合。预计 90 分钟(含首次编译)。

  2. 【配置练习·lockstep 对比】 查 PX4 文档找到关闭 lockstep 的方法(编译选项 / 参数)。分别在 lockstep 开/关下跑同一段起飞脚本两次,观察:① 开 lockstep 时两次运行的遥测曲线是否完全一致?② 关 lockstep 时呢?用这个实验理解"确定性可复现"的含义。预计 40 分钟。

  3. 【集成测试·故障保护触发】 故意把 offboard 控制循环的发送频率从 20 Hz 降到 1 Hz,运行脚本,观察 PX4 终端日志。验收标准:你应该看到飞机自动退出 offboard 模式 + 故障保护日志。然后改回 20 Hz 验证恢复正常。思考:这个安全机制在真机上保护了什么场景?预计 30 分钟。


§P0.D.5 渲染保真路线:Flightmare 与 3DGS——把"看得见"喂给视觉策略 ⭐⭐⭐

模块功能与在管线中的位置

上一节的 Gazebo+PX4 走"物理保真"路线,渲染只是配角。这一节走另一条岔路——渲染保真路线:当你的算法依赖图像(视觉避障、视觉竞速、VIO、视觉伺服),仿真的渲染质量就从配角变成主角。Flightmare 是这条路线的经典代表(Unity 光真),3DGS 是它的下一代(真实场景重建)。这一节回答"怎么让仿真出的图像,逼真到能训出可迁移到真机的视觉策略"。

动机:为什么物理保真路线喂不动视觉策略

回到 §P0.D.4 的 Gazebo+PX4——它物理很真,但渲染是 Gazebo 的 OGRE 引擎,质量中等、不能 GPU 批量出图。如果你想训一个"看着 RGB 图像避障"的策略,会撞上两堵墙:

  • 质量墙:OGRE 渲染的图像和真实相机拍的差距大(纹理、光照、材质都简化了)。策略在这种图像上学到的视觉特征,上真机看到真实图像就失效——这是视觉的 sim-to-real gap。
  • 速度墙:Gazebo 单实例渲染,出图慢。视觉 RL 要海量图像样本,Gazebo 的出图速度撑不起来。

本质洞察:视觉策略的 sim-to-real gap,主战场不在物理而在像素。一个飞得慢的避障策略,对气动残差几乎不敏感(物理 gap 小),但对"仿真图像 vs 真实图像"的差异极其敏感(渲染 gap 大)。这就是为什么视觉任务要专门走渲染保真路线——你优化的应该是像素的真实度,而不是动力学的精度。这和 §P0.D.2 不可能三角"按任务敏感维度配资源"完全一致。

反面:如果用低真渲染训视觉策略会怎样

反事实:假设你用 Gazebo 的低真渲染训一个 RGB 避障策略会怎样? → 策略可能学到"仿真特有的视觉捷径"——比如仿真里障碍物边缘有特定的锯齿/色块,策略靠这些非真实特征判断距离。 → 上真机:真实障碍物没有这些锯齿,策略的"捷径"失效,避障行为崩溃。 → 这叫"渲染 overfitting"——策略过拟合到仿真渲染器的特性,而非真实世界的视觉规律。 结论:渲染保真不足时,视觉策略学到的是"仿真渲染器的指纹"而非"真实视觉规律"。这就是为什么 Flightmare 用 Unity(接近真实的光照材质),以及为什么 3DGS(直接重建真实场景)是下一代——渲染越接近真实,可学的"捷径"越少,迁移越好

理论:Flightmare 的双引擎解耦架构

Flightmare 是 §P0.D.2"三层解耦"的最纯粹体现——它把渲染和物理拆成两个独立进程,用消息队列连接:

┌────────────────────────────┐         ┌────────────────────────────┐
│  Flightmare 渲染引擎         │         │  Flightmare 物理引擎         │
│  (Unity,独立进程/可独立运行)│  ZeroMQ │  (flightlib,C++)          │
│  ├─ 光真 RGB                 │◀───────▶│  ├─ 轻量 6-DoF 刚体积分      │
│  ├─ 深度 / 分割              │ 异步消息 │  ├─ 可选 BEM 气动模型        │
│  ├─ 点云提取                 │         │  ├─ 多达 ~150 架并行(CPU)  │
│  └─ 渲染达 230 Hz            │         │  └─ 物理达 200,000 Hz        │
│  ├─ 可接 VR 头显             │         │  └─ OpenAI Gym 风格 Python   │
│  └─ 多模态传感器套件          │         │     wrapper + stable-baselines│
└────────────────────────────┘         └────────────────────────────┘

这个架构的几个关键设计点

  • ZeroMQ 异步连接:渲染和物理用高性能异步消息库 ZeroMQ 通信,互不阻塞。物理可以飞速积分(200 kHz),渲染按自己的节奏出图(230 Hz)——两个数字差三个数量级,正说明它们完全解耦。
  • 物理简单、渲染光真:Flightmare 的物理引擎其实比 Gazebo 简单(默认轻量 6-DoF,BEM 气动是可选的)。它的卖点从来不是物理,而是 Unity 渲染。这印证了 §P0.D.1 的陷阱"渲染好 ≠ 物理准"。
  • 百级 CPU 向量化:物理端能并行 ~150 架无人机,随机动作下达 20 万 steps/s,一分钟收集数百万样本。注意这是 CPU 向量化(不是 GPU),所以规模是百级而非千级——这也是它被 G3 的 Aerial Gym/OmniDrones(GPU 千级)超越的地方。
  • 多模态 + VR:除 RGB 外提供深度、分割、点云,甚至能接 VR 头显做人在环交互——这套丰富的传感器套件是它做 VIO/感知研究的优势。

多视角理解:Flightmare 对照 Gazebo 的"分工哲学"

跨领域类比(标注边界):Flightmare 的双引擎解耦,像电影工业的"动作捕捉 + CG 渲染"分离——动捕棚(物理引擎)只管捕捉演员的运动数据,快而精确;渲染农场(Unity 渲染引擎)拿运动数据生成逼真画面,慢而精美。两者完全分离,各自优化。 像的地方:都是"运动/物理"和"视觉/渲染"分两个独立系统,用数据流连接,各自做到极致。 不像的地方:电影里动捕和渲染是离线串行的(先捕后渲);Flightmare 里物理和渲染是在线并行的(同时跑,实时交互)——ZeroMQ 让它们实时双向通信,而非离线单向传递。不要把"离线串行"的印象套到 Flightmare 上。

理论:3DGS 渲染——下一代视觉仿真范式(G4)

Flightmare 用 Unity 渲染的是"人工建模的场景"——美术建的房间、街道。但人工建模再精细,和真实世界仍有 gap。3DGS(3D Gaussian Splatting,三维高斯泼溅) 是 2024-2025 兴起的下一代方案,思路根本不同:

传统渲染(Flightmare/Gazebo):人工建模场景 → 渲染 → 图像(和真实有 gap)

3DGS 渲染(FiGS/SOUS VIDE):
  真实场景拍一组照片 ──→ 3DGS 重建 ──→ 真实场景的"数字孪生"
                                          ↓ 130 fps 渲染
                                    几乎和真实照片一样的图像
                                    训练视觉策略(在真实场景的孪生里)
                                    部署到同一个真实场景 → gap 极小

本质洞察:3DGS 把视觉 sim-to-real 的逻辑**反过来了**——传统方法是"建一个仿真场景,努力让它像真实";3DGS 是"拍真实场景,重建成可渲染的孪生"。前者是"从仿真逼近真实",后者是"从真实生成仿真"。后者的渲染 gap 天然极小,因为它的"场景"本就来自真实照片。Stanford MSL 的 FiGS/SOUS VIDE 用这套方法:3DGS 重建真实场景 → 130 fps 渲染 → MPC 专家蒸馏成视觉策略 → 105 次真机验证,这可能让 3DGS 替代 Flightmare 成为下一代视觉策略训练平台。

3DGS 的局限(为什么还没完全取代 Flightmare)

维度 3DGS 强 3DGS 弱
渲染真实度 极高(来自真实照片)
场景获取 要先去真实场景拍照重建,不能凭空造
物理交互 3DGS 是"渲染表示",不含碰撞几何/物理属性,要另配物理
场景泛化 一个重建场景训的策略,换场景要重新拍重建(泛化是开放问题)
动态物体 标准 3DGS 重建的是静态场景,动态障碍物支持仍在发展

配置详解:选渲染路线的决策维度

你的视觉任务 推荐 理由
只用深度图(很多避障策略) Aerial Gym(GPU 光线投射深度) 深度图不需要光真 RGB,GPU 出图最快
需要光真 RGB、合成场景够用 Flightmare(Unity)/ Pegasus(Isaac) Unity/Isaac 光真渲染,场景可自由设计
需要在特定真实场景部署、要极小渲染 gap 3DGS(FiGS 系) 真实场景孪生,渲染 gap 最小
已归档但存量多 AirSim(UE) 仅用于复现老工作,新项目不选

理论-工程桥接:渲染路线的选择直接决定你的数据管线长什么样。选 Aerial Gym(深度)→ 你的策略观测是深度张量,训练全程在 GPU,无需 ROS 图像话题。选 Flightmare(RGB)→ 你要起 Unity 渲染进程、走 ZeroMQ 收图、可能转 ROS Image 话题。选 3DGS → 你的管线最前面多了"真实场景采集 + 3DGS 重建"这一大块离线预处理。渲染路线不是一个孤立的开关,它牵动整条数据管线的形态——选型时要把这条管线的工程量一并算进去。

⚠️ 常见陷阱

💡 概念误区:以为渲染保真路线就一定要放弃物理保真
   新手想法:"Flightmare 物理简单,那做视觉任务就只能要画面不要物理了"
   实际上:渲染和物理是解耦的两层,可以独立提升。Flightmare 默认物理简单,但
          它支持可选的 BEM 气动模型;更进一步,Pegasus(Isaac Sim)能同时给
          光真渲染和高保真 PhysX 物理——代价是牺牲并行(回到不可能三角)。
          "渲染好就物理差"是 Flightmare 的具体取舍,不是物理定律
   正确理解:渲染和物理是正交维度。具体仿真器在两者上有各自取舍,但你可以根据
            任务选"两者都要"的工具(Pegasus),只是要付出并行规模的代价
🔧 配置陷阱:用合成渲染训视觉策略,却不做任何视觉域随机化
   错误做法:在 Flightmare/Isaac 里用固定的光照、纹理、相机参数训 RGB 策略
   现象:仿真里策略视觉表现完美,上真机(光照不同、纹理不同、相机有畸变)后
        视觉识别全乱,避障/跟踪失效
   根本原因:固定渲染条件让策略过拟合到那一组特定的视觉外观。真实世界的光照、
            纹理、相机畸变千变万化,策略没见过就泛化不了
   正确做法:视觉策略训练必须做视觉域随机化——随机化光照方向/强度、纹理、相机
            内参/畸变/噪声、甚至物体颜色。让策略学"形状/结构"而非"特定外观"。
            这是视觉 sim-to-real 的核心手段(见 §P0.D.7)
💡 概念误区:认为 3DGS 重建的场景可以直接拿来做物理碰撞仿真
   新手想法:"3DGS 重建了真实场景,那无人机撞墙、避障的物理碰撞也能在里面算吧"
   实际上:3DGS 是一种"渲染表示"——它用一堆带颜色和透明度的高斯椭球拟合场景的
          外观,本质是为了"出图好看",并不天然包含碰撞几何(mesh)或物理属性
          (质量、摩擦)。无人机在纯 3DGS 场景里"撞墙"没有物理反应
   正确理解:3DGS 解决的是"渲染 gap",不是"物理仿真"。完整方案要把 3DGS 渲染
            和一个物理引擎(提供碰撞几何)组合——通常从同一组照片另外重建一个
            碰撞 mesh,或用占据栅格表示几何。渲染和物理仍是解耦的两层

练习

  1. 【对比练习·渲染 gap 来源】 列举"仿真 RGB 图像"和"真实相机图像"之间至少 5 个差异维度(提示:光照、纹理、相机畸变、噪声、动态范围……)。对每个维度,说明它可能让视觉策略学到什么"仿真捷径"。验收标准:5 个维度都给出具体的捷径例子。预计 25 分钟。

  2. 【思考题·3DGS 范式反转】 用自己的话解释:"传统渲染是从仿真逼近真实,3DGS 是从真实生成仿真"这句话。然后回答:为什么"从真实生成仿真"的渲染 gap 天然更小?它的代价(相比传统渲染)是什么?预计 20 分钟。

  3. 【综合思考题·任务到渲染路线】 对下面三个视觉任务,分别选渲染路线(Aerial Gym 深度 / Flightmare RGB / 3DGS)并论证:① 室内深度避障 RL;② 需要识别特定 logo 的视觉伺服,部署在固定的实验室;③ 通用的 RGB 目标跟踪,要在各种未知环境用。每个论证 \(\ge\)3 句,落到"任务需要什么渲染特性"。预计 30 分钟。


§P0.D.6 并行 RL 工厂:Aerial Gym / OmniDrones——4096 环境,周级压到分钟级 ⭐⭐⭐

模块功能与在管线中的位置

这一节走"训练速度路线"的代表搭建:用 GPU 并行仿真把强化学习的采样速度拉到极致。它是 G3 时代的核心,也是当前无人机 RL 研究的标配。前面 §P0.D.4 的 SITL 单实例、§P0.D.5 的 Flightmare 百级 CPU 向量化,到这里跃升到**千级到百万级 GPU 并行**——同样一个悬停策略,gym-pybullet-drones 单 CPU 要训几小时,Aerial Gym 几千并行环境一分钟搞定。这一节是本章配置最密集的部分,逐项拆解 RL 环境的核心配置(num_envsdtdecimation、观测-动作-奖励),并讲清每个值怎么取、改了会怎样。

动机:为什么 RL 必须并行——采样吞吐是瓶颈

回顾前置自测第 2 题。PPO 这类 on-policy 算法有个根本特性:每次更新策略前,必须用当前策略重新采集一批新轨迹(旧策略采的数据用一次就得扔)。这个采样步骤是纯仿真步进——它**不吃 GPU 的算力(矩阵乘),只吃仿真步进速度**。所以 RL 训练的吞吐瓶颈不在神经网络(那很快),而在"每秒能仿多少步"。

单环境(gym-pybullet-drones, CPU):~2,000 steps/s
   一个像样的视觉避障策略需要 ~5 亿步
   → 5e8 / 2e3 = 250,000 秒 ≈ 2.9 天(还没算调参的反复!)

4096 并行环境(Aerial Gym, GPU):~数千万 steps/s(电机指令策略可 <1 分钟训完)
   同样 5 亿步 → 几十秒到几分钟

差距是**三到四个数量级**。这不是"快一点",而是"从不可能到可能"——2.9 天一次的迭代,没人能用它调参;一分钟一次的迭代,你一天能试几十版奖励函数。GPU 并行仿真不是优化,是 RL 研究的入场券。

本质洞察:GPU 并行仿真的核心魔法不是"用了 GPU",而是"让整个 rollout 全程不离开显存"。传统做法是 CPU 仿真→数据拷到 GPU→训练,每步都有 host-device 传输。GPU 并行仿真把物理状态、观测、动作、奖励全部表示成 GPU 张量,用 CUDA kernel 一次步进所有 4096 个环境,整个采样过程零拷贝。这就是为什么前置自测第 3 题强调"训练循环里不能 print GPU tensor"——一次 .cpu() 就打断这个零拷贝流水线,吞吐崩盘。

反面:如果在 GPU 并行训练里频繁取数据到 CPU 会怎样

反事实:假设你在 Aerial Gym 的训练循环里加了 print(obs.cpu().numpy()) 调试会怎样? → 每步 .cpu() 触发 GPU→CPU 的 PCIe 传输 + CUDA 流强制同步(等所有 kernel 跑完)。 → 4096 个环境的观测张量每步都拷一遍,PCIe 带宽成瓶颈。 → 更糟的是同步——GPU 本来可以异步流水线执行多个 kernel,.cpu() 强制"等齐",流水线断了。 → 结果:训练速度从数千万 steps/s 暴跌到几十万甚至更低,GPU 利用率从 95% 跌到个位数。 结论:这正是工程实践规范里的经典编程陷阱。GPU 并行训练的调试**绝不能用 .cpu() 取中间值**——要用框架的 logger(TensorBoard 记标量)或定期 torch.save 张量离线分析。

理论:Aerial Gym vs OmniDrones——两条 GPU 并行路线

两者都是 G3 GPU 并行,但底层栈和定位不同:

维度 Aerial Gym(NTNU ARL) OmniDrones(清华 thu-uav)
底层 Isaac Gym (Preview 4) Isaac Sim + Isaac Lab
渲染 GPU 光线投射(深度/分割,无光真 RGB) Isaac Sim 光真渲染
并行规模 千级到百万级(轻量物理) 千级
内置控制器 SE(3) 几何控制器(并行化,支持欠/全/过驱动) 多 control mode(含 CTBR)
机型 任意多旋翼(高度可配) 4 种机型 + 多传感器
杀手锏 电机指令策略 <1 分钟、视觉导航 <1 小时 与 Isaac Lab 生态打通、SimpleFlight 基准
适合 杂乱场景避障、极致并行 高保真渲染需求、多机、基准研究

多视角理解(怎么选): - 角度1(你要不要光真 RGB):要 RGB → OmniDrones(Isaac Sim 光真);只要深度/状态 → Aerial Gym(更快、更并行)。 - 角度2(你要不要极致并行):要把并行推到百万级(如海量电机指令策略)→ Aerial Gym(Isaac Gym 更轻);千级够用且要生态完整 → OmniDrones(Isaac Lab 工具链)。 - 角度3(你的底层栈历史包袱):已在用 Isaac Gym 的足式项目 → Aerial Gym 无缝;已在用 Isaac Lab → OmniDrones 顺手。

配置详解:RL 环境的核心配置项逐个拆解

这是本节的核心。下面是一个典型四旋翼悬停 RL 任务的配置(以 Isaac 生态的通用写法为例),逐项讲透每个值怎么来、改了会怎样:

class HoverTaskConfig:
    # ===== 并行规模 =====
    num_envs = 4096            # 并行环境数:决定采样吞吐和显存占用
    env_spacing = 2.0          # 环境间隔(米):可视化时各环境在世界里的物理间距

    # ===== 时间步 =====
    sim_dt = 0.005             # 物理步长(秒)= 200 Hz 物理仿真
    decimation = 4             # 控制降采样:每 4 个物理步执行 1 个控制步
                               # → 控制频率 = (1/sim_dt) / decimation = 200/4 = 50 Hz
    max_episode_length_s = 10.0  # 单回合最长时间(秒)→ 步数 = 10/(sim_dt*decimation)=500

    # ===== 动作空间(CTBR,见 §P0.D.3)=====
    action_dim = 4             # [质量归一化推力, ωx, ωy, ωz]
    thrust_range = [0.0, 2.0]  # 推力范围(单位 g,即 0 到 2 倍重力加速度)
    bodyrate_range = [-6.0, 6.0]  # 机体角速度范围(rad/s)

    # ===== 观测空间 =====
    # 位置误差(3) + 线速度(3) + 旋转矩阵(9) + 角速度(3) + 上一动作(4) = 22 维
    obs_dim = 22

    # ===== 域随机化(见 §P0.D.7)=====
    randomize_mass = True
    mass_range = [0.8, 1.2]    # 质量 ±20% 随机
    randomize_inertia = True
    inertia_range = [0.8, 1.2]

逐项深度解析(每一项都按"是什么/怎么取/改了会怎样/和谁耦合"四维讲):

num_envs = 4096——并行环境数

维度 说明
是什么 同时仿真的独立环境(无人机)数量。GPU 一个 kernel 一次步进所有这些环境
怎么取 来自"GPU 显存 vs 并行效率"的平衡点。4096 是经验甜点:足够多让采样高效,又不撑爆 16GB 显存
改大(如 8192) 采样吞吐↑(更多并行),但显存占用↑(可能 OOM);超过某点后并行效率边际递减
改小(如 512) 显存↓(小显卡能跑),但采样吞吐↓,训练墙钟时间↑;太小(<256)PPO 的批次方差大,训练不稳
和谁耦合 与 PPO 的 batch_sizemini_batch 耦合——num_envs × rollout_length 决定每次更新的样本量。改 num_envs 要同步检查批次配置

sim_dt = 0.005decimation = 4——物理步长与控制降采样

这两个**必须一起理解**,它们共同决定物理频率和控制频率:

\[ f_{\text{物理}} = \frac{1}{\text{sim\_dt}} = \frac{1}{0.005} = 200\ \text{Hz}, \qquad f_{\text{控制}} = \frac{f_{\text{物理}}}{\text{decimation}} = \frac{200}{4} = 50\ \text{Hz} \]
维度 说明
为什么物理要比控制快 物理积分步长太大会数值发散(积分误差累积);但控制不需要这么高频(策略 50 Hz 决策足够,且匹配真机 CTBR 指令频率 50-100 Hz)。所以"物理高频积分、控制低频决策",中间用 decimation 桥接
sim_dt 怎么取 典型 0.005(200Hz)。可在 0.001-0.01 间调;太大(>0.02)刚体积分不稳定甚至发散;太小(<0.001)算力浪费在过度精细的积分上
decimation 怎么取 由"想要的控制频率"反推:decimation = f_物理 / f_控制。想要 50Hz 控制、200Hz 物理 → decimation=4
关键耦合陷阱 改了 sim_dt 必须同步改 decimation,否则控制频率会漂移!比如 sim_dt 改成 0.0025(400Hz)但 decimation 还是 4 → 控制变成 100Hz,策略的时间感知全乱,训练行为异常

本质洞察(decimation 的本质):decimation 本质是**解耦"物理精度"和"决策频率"两个不该绑定的东西**。物理积分要高频是为了数值稳定(纯数学需求),策略决策频率要匹配真机控制接口(工程需求)——这是两个独立的约束。如果没有 decimation,你被迫让策略以 200Hz 决策,既浪费(策略不需要这么快)又和真机 50Hz 的 CTBR 接口对不上。decimation 让两者各取所需:物理 200Hz 稳,控制 50Hz 准。

max_episode_length_s——回合长度

维度 说明
是什么 单个训练回合的最长时间,到点就 reset(无论成功失败)。换算成步数 = length_s × f_控制
怎么取 要覆盖任务的完整时间尺度。悬停 10 秒够;长轨迹跟踪要更长。太短策略学不到长程行为,太长每回合采样慢、稀疏奖励难传播
改了会怎样 太短→策略只见过任务前段,泛化差;太长→单回合占样本多,探索效率低
和谁耦合 与奖励的时间尺度、reset 条件耦合

观测空间 22 维——为什么是这些量

# 22 维观测 = 位置误差(3) + 线速度(3) + 旋转矩阵(9) + 角速度(3) + 上一动作(4)
obs = torch.cat([
    target_pos - drone_pos,    # 3D 位置误差:策略要"往目标飞"的核心信号
    drone_linvel,              # 3D 线速度:判断当前运动状态,防止冲过头
    rot_matrix.reshape(-1, 9), # 9D 旋转矩阵:表示姿态,比欧拉角无奇异、比四元数无双覆盖
    drone_angvel,              # 3D 角速度:姿态变化率,匹配 CTBR 动作的物理量
    last_action,               # 4D 上一动作:让策略感知自己上一步做了什么,利于动作平滑
], dim=-1)

为什么姿态用旋转矩阵(9D)而非欧拉角(3D)或四元数(4D)

表示 维度 问题
欧拉角 3 万向锁奇异——某些姿态下表示退化,梯度爆炸,策略学不动
四元数 4 双重覆盖——\(q\)\(-q\) 表示同一姿态,神经网络要学这个对称性,徒增难度
旋转矩阵 9 维度高但**无奇异、无双覆盖、连续**——神经网络最友好。代价仅是多 5 维输入,可忽略

理论-工程桥接:观测里放"上一动作"(last_action)不是凑数——它是**动作平滑**的关键。RL 策略容易输出剧烈抖动的动作(这一步满推力、下一步零推力),这在仿真里可能"最优",但真机的电机和 rate loop 跟不上(见 §P0.D.3 陷阱)。把上一动作放进观测,配合奖励里的"动作差分惩罚",策略就能学会平滑过渡——这是把 sim-to-real 的物理约束"编码进训练"的典型手法。

代码走读:观测-动作-奖励的最小实现

def compute_reward(drone_state, target, action, last_action):
    """四旋翼悬停奖励——位置/速度跟踪 + 姿态正则 + 动作平滑 + 存活"""
    pos_err = torch.norm(target - drone_state.pos, dim=-1)   # 位置误差范数
    vel_norm = torch.norm(drone_state.linvel, dim=-1)        # 速度大小

    # 1) 位置跟踪:误差越小奖励越高(用 exp 让奖励在目标附近平滑饱和)
    r_pos = torch.exp(-1.5 * pos_err)
    # 2) 速度正则:悬停时希望速度趋零(防止策略学会"高速画圈经过目标")
    r_vel = torch.exp(-0.5 * vel_norm)
    # 3) 姿态正则:希望机体 z 轴接近世界 z 轴(别翻过来)
    upright = drone_state.rot_matrix[..., 2, 2]   # R[2,2] = 机体 z 在世界 z 的投影
    r_upright = torch.clamp(upright, 0.0, 1.0)
    # 4) 动作平滑:惩罚相邻动作差分(关键的 sim-to-real 项!)
    r_smooth = torch.exp(-0.1 * torch.norm(action - last_action, dim=-1))
    # 5) 存活奖励:每步活着给一点,鼓励不坠毁
    r_alive = 0.1

    reward = (1.0 * r_pos + 0.3 * r_vel + 0.2 * r_upright
              + 0.2 * r_smooth + r_alive)
    return reward

def check_termination(drone_state, episode_step, max_steps):
    """回合终止条件:坠毁/翻覆/超时"""
    crashed = drone_state.pos[..., 2] < 0.1          # 高度过低=坠地
    flipped = drone_state.rot_matrix[..., 2, 2] < 0.0  # z 轴朝下=翻覆
    too_far = torch.norm(drone_state.pos, dim=-1) > 10.0  # 飞太远=失控
    timeout = episode_step >= max_steps
    done = crashed | flipped | too_far | timeout
    return done

奖励设计的几个关键决策

  • exp(-误差) 而非 -误差:指数形式让奖励在目标附近平滑饱和(接近目标时奖励增益变缓),避免策略为了最后一点点误差做剧烈动作。这是连续控制 RL 的常用技巧。
  • 速度正则不能少(反例警示):如果只奖励位置跟踪、不惩罚速度,策略可能学会"高速反复冲过目标点"——每次经过目标都拿位置奖励,但根本没"停下来悬停"。加速度正则强制它真正稳定下来。
  • 动作平滑项是 sim-to-real 的桥r_smooth 惩罚动作突变,直接对应 §P0.D.3 的"body rate 不能突变"陷阱。仿真里它可能略微降低位置精度,但换来的是真机上能实际执行的平滑指令——这是用奖励函数为真机的物理约束"买保险"

运行验证:怎么确认 4096 并行训练在正常工作

# 跑 Aerial Gym 的位置控制训练
python aerial_gym/rl_training/train.py task=position_setpoint num_envs=4096

# 监控三件事:
# 1) 终端的 FPS——应达数百万 steps/s 量级(电机指令策略)
# 2) reward 曲线——应单调上升后收敛(用 TensorBoard 看)
# 3) nvidia-smi——GPU 利用率应稳定在 90%+(说明没有 host-device 同步瓶颈)

成功标志:① FPS 达到预期量级(Aerial Gym 电机指令策略可达每秒数百万步);② TensorBoard 里 reward 单调上升并收敛;③ nvidia-smi 显示 GPU 利用率 90%+(若利用率忽高忽低或偏低,多半是训练循环里有 .cpu() 或 Python 端瓶颈)。

⚠️ 常见陷阱

⚠️ 编程陷阱:在 GPU 并行训练循环里调 .cpu()/.numpy()/.item() 取中间值调试
   错误做法:训练循环里加 print(reward.mean().item()) 或 obs.cpu().numpy() 看数值
   现象:训练速度暴跌 10-100 倍,nvidia-smi 显示 GPU 利用率从 95% 跌到个位数
   根本原因:.cpu()/.item() 触发 GPU→CPU 传输并强制 CUDA 流同步,打断异步流水线。
            4096 环境每步同步一次,开销被放大到不可接受
   正确做法:① 用框架自带 logger(TensorBoard)记标量,它内部批量异步处理;
            ② 必须看张量就 torch.save() 定期存盘,训练后离线分析;
            ③ 监控指标用低频采样(每 N 步取一次),别每步取
🔧 配置陷阱:改了 sim_dt 却忘了同步改 decimation,控制频率悄悄漂移
   错误做法:为了"物理更精确"把 sim_dt 从 0.005 改成 0.0025,decimation 仍是 4
   现象:训练曲线和之前完全不同,策略行为异常;上真机后指令频率对不上
   根本原因:sim_dt=0.0025 → 物理 400Hz,decimation=4 → 控制变成 100Hz(原来
            是 50Hz)。控制频率翻倍,策略的"每步代表多长时间"的隐含假设全变了,
            而真机 CTBR 接口若仍是 50Hz,训练和部署的控制频率不一致
   正确做法:sim_dt 和 decimation 必须联动——确定目标控制频率(如匹配真机 50Hz),
            然后 decimation = f_物理 / 50。改 sim_dt 时重算 decimation 保持控制
            频率不变。把 f_控制 当作"不变量"来锁定
🔧 配置陷阱:num_envs 设太大直接 CUDA out of memory
   错误做法:8GB 显卡上直接 num_envs=8192 跑带相机的视觉任务
   现象:启动时报 CUDA out of memory,或跑几步后 OOM 崩溃
   根本原因:每个环境的状态张量 + (若有)渲染缓冲都占显存,8192 环境 × 视觉
            观测远超 8GB。视觉任务的单环境显存占用比状态任务高一个量级
   正确做法:从小往大试 num_envs(512→1024→2048…),用 nvidia-smi 看显存占用,
            留 10-20% 余量。视觉任务的 num_envs 通常比纯状态任务小一个量级
            (状态 4096,视觉可能只能 512-1024)。OOM 时优先降 num_envs 或
            降相机分辨率

练习

  1. 【配置练习·num_envs 扫描】 用 Aerial Gym 或 OmniDrones 跑同一个悬停任务,把 num_envs 依次设为 256/1024/4096,记录每档的:① FPS(steps/s);② nvidia-smi 显存占用;③ 达到相同 reward 阈值的墙钟时间。画出"num_envs vs FPS"和"num_envs vs 墙钟时间"两条曲线。验收标准:你应观察到 FPS 随 num_envs 上升但边际递减、墙钟时间下降。预计 60 分钟。

  2. 【配置练习·decimation 联动验证】 把 sim_dt 从 0.005 改成 0.0025,故意不改 decimation(保持 4),跑训练,对比改之前的训练曲线。然后正确地把 decimation 改成 8(保持控制频率 50Hz 不变),再跑一次。验收标准:第二次(正确联动)的曲线应和原始曲线接近,第一次(错误)的应明显偏离。用这个实验内化"sim_dt 和 decimation 必须联动"。预计 45 分钟。

  3. 【管线搭建·加一个观测项】 为悬停任务的观测增加一项"到目标的距离的标量"(在原 22 维基础上加 1 维变 23 维)。修改 obs 拼接代码和 obs_dim 配置,确保网络输入维度同步更新。跑通训练,对比加这一项前后的收敛速度。验收标准:训练能正常跑通不报维度错误。思考:这一项是冗余的(位置误差里已含距离信息)还是有用的?为什么?预计 50 分钟。

  4. 【集成测试·奖励项消融】 把奖励函数里的"动作平滑项 r_smooth"权重设为 0,重新训练,对比:① 训练曲线(位置精度可能略升);② 训出来的动作序列的抖动程度(画 action 随时间的曲线)。验收标准:去掉平滑项后,动作曲线明显更抖。思考:为什么"仿真里更优"的策略(无平滑)反而更难上真机?预计 50 分钟。


§P0.D.7 sim-to-real gap 六大来源——仿真到真机为什么会崩、怎么救 ⭐⭐⭐⭐

模块功能与在管线中的位置

这一节是全章的"终极问题"。前面六节教你把各种仿真台子搭起来、把训练跑通——但搭仿真的**终极目的**,是让仿真里学到/验证过的东西能搬到真机上不崩。这一节系统拆解"仿真和真机为什么不一样"(sim-to-real gap)的六大来源,并对每一类给出缩小手段。它是 §P0.D.2 不可能三角"按任务敏感维度配资源"的落地——只有知道 gap 来自哪里,你才知道该在哪个维度上加保真度、加随机化。

动机:那个最痛的瞬间——"仿真完美,真机炸机"

几乎每个做无人机的人都经历过这个瞬间:策略/控制器在仿真里跑了上千次,每次都完美悬停、精准跟踪。满怀信心上真机,结果——要么剧烈震荡,要么直接发散摔机。这个落差就是 sim-to-real gap。

问题不在于"gap 存在"(gap 永远存在),而在于**你能不能预判 gap 来自哪里、提前在仿真里把它"喂"给策略**。盲目试错代价极高(摔一架真机几千到几万块,还可能伤人)。系统化地拆解 gap 来源,是把"撞运气"变成"工程可控"的唯一办法。

本质洞察:sim-to-real gap 的本质,不是"仿真不够逼真"这一个笼统的问题,而是六个可分别量化、分别治理的具体差异。新手把它当成一团迷雾("仿真和真机就是不一样"),专家把它拆成六个抽屉,每个抽屉有对应的诊断方法和治理手段。本节的全部价值,就是把这团迷雾拆成六个抽屉——拆开了,gap 就从"玄学"变成"工程"。

反面:如果不系统拆解 gap,只靠"调到仿真里更好"会怎样

反事实:假设你不理会 gap 来源,只一味地"让策略在仿真里表现更好"会怎样? → 你会把策略过拟合到仿真的具体参数(特定质量、特定电机模型、特定噪声)。 → 仿真里的"更好",恰恰意味着"更依赖仿真的具体设定"——也就是 gap 更大。 → 这是个反直觉的陷阱:仿真性能和 sim-to-real 鲁棒性常常是负相关的。一个在仿真里"刚刚好"但鲁棒的策略,比一个仿真里"完美"但脆弱的策略更能上真机。 结论:sim-to-real 的目标不是"最大化仿真性能",而是"最大化跨域(仿真→真机)的鲁棒性"。这正是域随机化的哲学(见下文),也是为什么 §P0.D.2 反复强调"按任务匹配保真度"而非"无脑堆保真度"。

理论:sim-to-real gap 的六大来源(系统性分类)

系统性分类(穷举式维度,而非随意举例):sim-to-real gap 按"差异发生在控制链的哪一环"穷举为六类。这个分类法的价值是给你一个"思考框架"——遇到新的炸机问题,逐个抽屉排查,而非随意猜。

仿真 ─────────────────────────────────────────────→ 真机
  │                                                    │
  ① 动力学参数 gap    ② 气动残差 gap                    │
  (质量/惯量/重心    (高速阻力/下洗/                   │
    与真机不符)        地效,仿真没建或建错)            │
  │                                                    │
  ③ 电机/执行器 gap   ④ 传感器 gap                      │
  (电机时间常数/      (IMU/相机噪声、偏置、            │
    ESC 延迟/桨叶气动)   畸变,仿真过于干净)            │
  │                                                    │
  ⑤ 延迟 gap          ⑥ 接触/外扰 gap                   │
  (通信/计算/控制     (桨叶触碰、风扰、                │
    回路延迟)           地面交互,仿真理想化)           │
来源 仿真里的样子 真机里的样子 缩小手段
① 动力学参数 用标称质量/惯量/重心 装了电池/相机后实际值有偏差 系统辨识(SysID) 测准真机参数 + 域随机化 覆盖偏差
② 气动残差 默认无气动或简化阻力 高速下阻力/下洗/地效显著 BEM/残差模型 建准 + 用真飞数据**拟合残差**
③ 电机/执行器 理想推力或简单一阶模型 ~25ms 时间常数 + ESC 延迟 + blade flapping **CTBR 接口**外包给固件(§P0.D.3)+ 域随机化电机参数
④ 传感器 干净的真值或弱噪声 IMU 偏置漂移、相机畸变/噪声/曝光 传感器噪声建模 + **域随机化**噪声/偏置/畸变
⑤ 延迟 零延迟或固定延迟 通信+计算+控制回路的可变延迟 延迟随机化 + 观测里加历史帧 + 训练时注入延迟
⑥ 接触/外扰 理想化或无 风扰、桨叶触碰、地面效应 外力随机化 + 训练时加随机推力扰动

逐类深入:每个抽屉怎么诊断、怎么治

① 动力学参数 gap——最基础、最该先治的

诊断:真机起飞后,对比"仿真悬停油门"和"真机悬停油门"——如果真机油门明显更高,说明真机更重(你的仿真质量偏小)。治理:先做系统辨识(SysID)把真机的质量、惯量、推力系数测准,让仿真的标称值对齐真机;再用域随机化(如质量 \(\pm\)20%)覆盖残余偏差和电池/载荷变化。

本质洞察(SysID vs 域随机化的分工):这两个手段不是二选一,而是配合——SysID 负责"对准中心",域随机化负责"覆盖范围"。SysID 把仿真的标称参数移到真机参数附近(减小系统性偏差),域随机化在这个中心周围撒一片分布(覆盖随机性偏差和你测不准的部分)。只做 SysID 不做 DR,策略对"测量误差"和"参数漂移"脆弱;只做 DR 不做 SysID,随机化范围要开得很大才能包住真机,导致策略保守、性能差。先 SysID 对准、再 DR 覆盖,是参数 gap 治理的黄金组合。

② 气动残差 gap——飞得越快越致命

诊断:低速悬停/慢飞时仿真和真机吻合,但一加速(>5 m/s)真机就"跟不上仿真预测"——这是气动残差(阻力、下洗、机身气动)在作祟。治理:① 用高保真气动模型(如 BEM 叶素动量理论)替换默认的无气动/简单阻力;② 更实用的是用真飞数据拟合一个"残差模型"——让仿真物理 = 标称模型 + 数据驱动的残差补偿。

跨章桥接(回顾 D8 敏捷飞行):D8 讲过,当四旋翼从 5 m/s 推到 100 km/h,气动残差从"可忽略的二阶噪声"变成"主导的一阶效应"。这正是 §P0.D.2 不可能三角的活例——慢飞任务气动 gap 小,可以用简单物理换并行;竞速任务气动 gap 大,必须上 BEM 高保真物理(即使牺牲并行)。任务的速度决定了气动保真度的必要性,进而决定了你在不可能三角里的取舍点。

③ 电机/执行器 gap——CTBR 已经帮你治了大半

诊断:策略输出剧烈/高频的动作,但真机的实际响应"软绵绵"跟不上——电机时间常数和 ESC 延迟把高频指令"滤掉"了。治理:最有效的是**用 CTBR 动作空间**(§P0.D.3)——把电机层的快变量外包给 2-8 kHz 固件 rate loop,策略根本不碰这些难仿的东西。剩余的(rate loop 本身的带宽限制)用电机参数域随机化 + 动作平滑奖励覆盖。

④ 传感器 gap——视觉策略的主战场

诊断:状态策略(用位置/速度)通常传感器 gap 小(动捕/GPS 真值较准);视觉策略 gap 大——仿真图像太干净,真机相机有畸变、噪声、曝光变化、运动模糊。治理:视觉域随机化(随机光照、纹理、相机内参/畸变/噪声)+ IMU 噪声建模(偏置漂移、随机游走)。目标是让策略学"结构/形状"而非"特定外观"(呼应 §P0.D.5 陷阱)。

⑤ 延迟 gap——最隐蔽、最容易被忽略

诊断:仿真里策略稳定,真机里出现"延迟诱发的震荡"——指令到执行有几十毫秒延迟,策略基于"过时的观测"做决策,形成正反馈震荡。治理:① 训练时注入随机延迟(observation delay + action delay);② 观测里加入历史帧,让策略"看到"延迟的影响并学会补偿;③ 用 CTBR 等高层接口减少策略对精确时序的依赖。

多视角理解(延迟为什么这么阴险): - 角度1(控制理论视角):延迟在频域里是相位滞后,吃掉系统的相位裕度,把原本稳定的闭环推向震荡甚至发散。这是经典控制的老问题,在 RL 里换了张皮但本质一样。 - 角度2(RL 视角):延迟破坏了"马尔可夫性"——当前观测不再充分描述系统状态(真实状态还包含"在途的指令")。策略基于不完整状态决策,自然次优甚至失稳。 两个角度指向同一个治理思路:把延迟"显式化"——要么在观测里加历史帧(恢复马尔可夫性),要么训练时注入延迟(让策略见过它)。不要假装延迟不存在。

⑥ 接触/外扰 gap——长尾但真实

诊断:大多数时候吻合,但偶发的外部扰动(一阵风、桨叶蹭到障碍、贴地飞行的地面效应)让真机偏离仿真预测。治理:训练时加随机外力推扰(random force perturbation)、随机风场;贴地任务专门建地面效应模型。

理论:域随机化——贯穿六类 gap 的总线手段

注意上面六类 gap 的治理手段里,"域随机化(DR)"反复出现——它是 sim-to-real 的总线手段。核心做法是每个(批)回合随机扰动物理参数:

def apply_domain_randomization(env, cfg):
    """每个回合 reset 时随机化物理参数——sim-to-real 的核心手段"""
    n = env.num_envs
    # ① 动力学参数
    env.mass = sample_uniform(cfg.mass_range, n)          # 质量 ±20%
    env.inertia = sample_uniform(cfg.inertia_range, n)    # 惯量 ±20%
    env.com_offset = sample_uniform(cfg.com_range, n)     # 重心偏移
    # ③ 电机参数
    env.motor_tau = sample_uniform(cfg.motor_tau_range, n)  # 电机时间常数
    env.thrust_coeff = sample_uniform(cfg.thrust_range, n)  # 推力系数
    # ④ 传感器
    env.imu_bias = sample_normal(cfg.imu_bias_std, n)     # IMU 偏置
    env.obs_noise_std = cfg.obs_noise_std                 # 观测噪声
    # ⑤ 延迟
    env.action_delay = sample_randint(cfg.delay_range, n) # 动作延迟步数
    # ⑥ 外扰
    env.ext_force = sample_uniform(cfg.force_range, n)    # 随机外力

本质洞察(域随机化的真正含义,呼应前置自测第 4 题):域随机化**不是让仿真更逼真,而是让策略更鲁棒**。真实世界只是随机化覆盖的参数分布中的**一个点**。这个表述的区别极其重要: - "让仿真逼真"是无止境的——你永远能找到更精细的建模,且不知道够不够。 - "让策略鲁棒"是可达且可量化的——只要随机化范围**包住**真实值,策略对范围内任意参数都鲁棒,自然包括真机这个点。

换句话说,域随机化把"精确建模真机"(难、不可达)这个问题,转化成"让策略对一片参数分布都能工作"(可达、可验证)。这是 sim-to-real 范式的根本智慧——不追求复制真实,而追求覆盖真实

对比性思维("不是 X 而是 Y"): - 域随机化**不是**为了模拟某个具体的真机,**而是**为了让策略对"任何落在分布内的真机"都能用。 - 缩小 sim-to-real gap **不是**靠"无限提高仿真保真度",**而是**靠"在敏感维度上系统辨识对准 + 域随机化覆盖"。 - 一个好的 sim-to-real 策略**不是**仿真性能最高的那个,**而是**在仿真性能和跨域鲁棒性之间取得平衡的那个。

理论-工程桥接:sim2sim 验证——上真机前的最后一道软件关

在烧钱上真机前,有一个低成本的中间验证:sim2sim——把在仿真器 A(训练用,如 Aerial Gym)训好的策略,拿到仿真器 B(验证用,如 gym-pybullet-drones 或 Flightmare)里跑。

本质洞察(sim2sim 为什么有效):sim2sim 利用了一个朴素但强大的逻辑——如果你的策略连"另一个仿真器"都迁移不过去,它绝无可能迁移到真机。仿真器 A 和 B 的物理模型有差异(不同的积分器、不同的气动假设),这个差异是真机 gap 的"廉价代用品"。策略能跨过 A→B 这道坎,是它能跨过 A→真机的必要条件(虽不充分)。sim2sim 让你用"零成本、零风险"的方式提前筛掉那些过拟合到训练仿真器的脆弱策略,把宝贵的真机时间留给真正有希望的策略。

完整的 sim-to-real 流水线因此是:

仿真器 A 训练(Aerial Gym,4096 并行,含域随机化)
  ↓ 导出 .pt / .onnx
仿真器 B 验证(gym-pybullet-drones / Flightmare,sim2sim)  ← 廉价软件关
  ↓ 通过
HITL 验证(真 Pixhawk + Gazebo)                          ← 测硬件算力约束
  ↓ 通过
真机小范围测试(限定动作范围、有安全网/Guard)              ← 见 §P0.D.8
  ↓ 逐步放开动作范围
真机完整部署

⚠️ 常见陷阱

💡 概念误区:认为"仿真中跑通 = 真机能用"
   新手想法:"仿真里策略表现很好,直接部署到真机应该没问题"
   实际上:sim-to-real gap 普遍存在且来自六个独立来源——动力学参数、气动残差、
          电机/执行器、传感器、延迟、接触外扰。仿真中"完美"的策略可能过拟合到
          仿真的具体参数,在真机上完全失效
   正确理解:仿真是快速迭代工具,不是真实环境的替代。部署前必须:① 系统辨识对准
            关键参数;② 域随机化覆盖偏差;③ sim2sim 验证;④ 真机小范围测试 +
            逐步放开动作范围。每一步都是为了系统性地填那六个抽屉里的 gap
💡 概念误区:以为域随机化范围开得越大越好("覆盖越宽越鲁棒")
   新手想法:"既然要覆盖真机,那随机化范围开到最大不就最保险了?"
   实际上:随机化范围过大会让任务"无解"或策略过度保守——如果质量随机化范围是
          ±90%,策略要同时应对极轻和极重两种极端,只能学一个"谁都不得罪"的
          平庸策略,性能大幅下降,甚至训不收敛
   正确理解:随机化范围应"恰好包住真机参数的不确定区间",不多不少。先用 SysID
            把不确定区间缩小(对准中心),再用略大于该区间的范围做 DR。范围由
            "你对真机参数的不确定度"决定,而非越大越好。鲁棒性和性能之间有权衡
🔧 配置陷阱:训练时不注入延迟,部署后被延迟诱发震荡
   错误做法:仿真训练时假设零延迟(观测立即可得、动作立即生效)
   现象:仿真完美,真机上出现持续的高频震荡,越快的动作震得越厉害
   根本原因:真机有几十毫秒的观测+控制延迟。零延迟训练的策略基于"即时观测"
            做激进决策,到真机上这些决策基于"过时观测",形成延迟正反馈震荡
   正确做法:训练时注入随机延迟(observation/action delay),并在观测里加历史帧
            让策略感知并补偿延迟。延迟是六大 gap 里最隐蔽的,务必显式建模——
            它不像质量偏差那样直观,但杀伤力极大

练习

  1. 【分析练习·gap 归因】 给定三个"仿真完美、真机失败"的症状,判断各自最可能属于六大 gap 的哪一类,并给出诊断方法:① 真机悬停油门比仿真高 15%;② 低速正常但一加速到 8 m/s 就掉高度;③ 策略在真机上以约 5 Hz 持续震荡。每个给出 gap 归类 + 诊断 + 治理手段。验收标准:归类正确且诊断方法可操作。预计 30 分钟。

  2. 【配置练习·域随机化范围扫描】 在你的悬停 RL 任务里,把质量域随机化范围依次设为 ±5%、±20%、±50%、±90%,各训一个策略,记录:① 训练能否收敛;② 最终仿真位置精度。验收标准:你应观察到范围过大(±90%)时性能明显下降甚至难收敛。用这个实验内化"随机化范围有最优区间,非越大越好"。预计 60 分钟。

  3. 【综合思考题·sim2sim 验证设计】 你用 Aerial Gym 训了一个悬停策略,想在上真机前用 gym-pybullet-drones 做 sim2sim 验证。设计这个验证流程:① 怎么把策略从 Aerial Gym 导出并加载到 gym-pybullet-drones?(提示:.onnx / .pt + 接口适配)② 两个仿真器的观测/动作定义可能不一致,怎么对齐?③ 什么样的 sim2sim 结果说明"可以上真机了",什么样说明"还得回去改"?预计 40 分钟。

  4. 【跨章综合题·端到端 sim-to-real 链路】 综合 §P0.D.3(CTBR)、§P0.D.6(并行训练)、§P0.D.7(gap + DR)三节,画出一条完整的"从零训练一个能上真机的悬停策略"的流水线图,标注每一环:用什么仿真器、什么动作空间、加哪些域随机化、怎么 sim2sim 验证、怎么逐步上真机。这是把本章 RL 相关内容串成网络的综合练习。预计 50 分钟。


§P0.D.8 Bridge/Adapter——一份代码跑仿真和真机 ⭐⭐⭐

模块功能与在管线中的位置

前面七节解决了"怎么搭各种仿真台子"和"怎么缩小 sim-to-real gap"。这一节解决一个工程组织问题:怎么让你写的算法代码,不重写就能在不同仿真器和真机之间切换? 答案是 Bridge/Adapter 设计模式——它把"当前跑在哪(仿真器 A / 仿真器 B / 真机)"收敛成一个可替换的底层适配器,让上层算法对此无感。这是 §P0.D.2"三层解耦反过来约束你的代码组织"的具体落地,也是 D8 Agilicious 架构的核心思想抽象。

动机:从"为每个目标写一套代码"的噩梦说起

设想你不用任何抽象,直接为每个仿真器写代码。你的控制器演进路径会是这样:

第 1 周:在 gym-pybullet-drones 里写控制器 → 调用 PyBullet 专有 API 读状态
第 3 周:要做视觉验证,换 Flightmare → 控制器全改,因为状态接口完全不同
第 6 周:要做 PX4 在环,换 Gazebo+PX4 → 又全改,状态走 MAVROS 话题
第 9 周:上真机 → 再改一遍,状态走真机 MAVLink

四套几乎相同的控制逻辑,分散在四份代码里。改一个控制 bug 要改四处,漏改一处就在某个目标上出错。这是没有抽象的必然结局——算法逻辑被"运行目标"的细节污染、绑死。

本质洞察:这个噩梦的根源,是把两件本该分离的事**焊在了一起**——"控制逻辑是什么"(算法)和"状态从哪来、指令往哪去"(目标接口)。Bridge/Adapter 模式的全部价值,就是把这两件事重新分开:算法只依赖一个抽象的状态/指令接口,"当前目标是谁"由一个可插拔的 Bridge 实现。一份算法代码,N 个 Bridge,零重写切换。

反面:如果算法直接耦合具体目标会怎样

反事实:假设你的 RL 策略推理代码里直接写死了 mavros.publish(AttitudeTarget(...)) 会怎样? → 想在 gym-pybullet-drones 里快速验证这个策略:不行,gym-pybullet-drones 不接 MAVROS,得改成它的 step() 接口。 → 想在 Flightmare 里测视觉:又得改成 Flightmare 的 CTBR 接口。 → 每换一个验证环境就改一次推理代码,改的过程中很容易引入"仿真版和真机版行为不一致"的 bug——而这种 bug 极难发现,因为你以为跑的是"同一个策略"。 结论:算法直接耦合目标,不仅是"麻烦",更是 sim-to-real 的隐性 gap 来源——"仿真用的代码"和"真机用的代码"不是同一份,它们之间的任何细微差异都是新的 gap。Bridge 模式从根上消除这类 gap:物理上保证仿真和真机跑的是同一份算法二进制。

理论:Agilicious 的 Pilot→Bridge→Guard 三层架构(回顾 D8)

D8 详细讲过 Agilicious(Science Robotics 2022)的架构,这里从"仿真集成"的角度重新审视它——它是 Bridge 模式在无人机领域的标杆实现:

┌─ Guard(独立安全管线,mocap/LIO 估计器)──────────────────┐
│  ├─ 若 Pilot 违反约束(位姿偏差/围栏/超时)→ Guard 接管   │
│  └─ Guard 独立运行,永远不依赖 Pilot 的估计器             │
├─ Pilot(用户实验管线)───────────────────────────────────┤
│  ├─ Pipeline = {Estimator, Sampler, Reference,          │
│  │              Controller, Bridge}                      │
│  ├─ Controller: MPC / DFBC / INDI / RL / 自定义          │
│  │   └─ 通过 Factory(YAML) 运行时切换                    │
│  └─ Estimator: VIO / mocap / LIO                        │
├─ Bridge(硬件/仿真适配层)★ 本节焦点 ────────────────────┤
│  ├─ RotorS / Flightmare / Agisim / 真机 UART            │
│  └─ 同一份 C++17 二进制,不重编即切换目标                 │
└──────────────────────────────────────────────────────────┘

Bridge 层就是这里的 Adapter——它定义一个统一接口("接收 CTBR 指令、返回状态"),然后为每个目标(RotorS / Flightmare / Agisim 仿真器 / 真机 UART)写一个实现。Pilot 里的控制器只认 Bridge 接口,通过 YAML 配置选用哪个 Bridge 实现,C++17 二进制一次编译,切换目标不重编。

本质洞察(Guard 为什么是独立管线):Guard 的存在揭示了 sim-to-real 的一个深层智慧——真机实验的安全网不能依赖被测对象本身。你的实验性 MPC/RL 控制器(Pilot)可能发散,如果安全检查也用 Pilot 的估计器,那 Pilot 崩了安全检查也崩。所以 Guard 用独立的估计器(mocap/LIO)独立运行,Pilot 一违反约束(位姿偏差/越界/超时),Guard 立即接管执行紧急悬停。这是 70 km/h 飞行时唯一的安全网,也是为什么 30+ 篇后续论文基于 Agilicious 做实验而没摧毁硬件。仿真里你可以随便崩(reset 即可),真机里 Guard 是你和炸机之间的最后一道墙。

代码走读:一个最小的 Bridge 抽象(教学示意)

下面用 Python 示意 Bridge 模式的骨架(真实的 Agilicious 是 C++17,这里抽象其精神):

from abc import ABC, abstractmethod

class DroneState:
    """统一的状态结构——算法只认这个,不认任何仿真器/真机的专有格式"""
    def __init__(self, pos, vel, rot_matrix, angvel):
        self.pos = pos              # 世界系位置
        self.vel = vel              # 世界系线速度
        self.rot_matrix = rot_matrix  # 姿态(旋转矩阵,无奇异)
        self.angvel = angvel        # 机体系角速度

class CTBRCommand:
    """统一的指令结构——CTBR(见 §P0.D.3)"""
    def __init__(self, thrust, body_rate):
        self.thrust = thrust        # 质量归一化集体推力
        self.body_rate = body_rate  # 机体三轴角速度 [ωx, ωy, ωz]

class Bridge(ABC):
    """适配层抽象基类:所有目标(仿真器/真机)实现同一接口"""
    @abstractmethod
    def get_state(self) -> DroneState: ...      # 拿统一状态
    @abstractmethod
    def send_command(self, cmd: CTBRCommand): ...  # 发统一指令

# ---- 具体 Bridge 实现:每个目标一个 ----
class PyBulletBridge(Bridge):
    def __init__(self, aviary): self.aviary = aviary
    def get_state(self):
        raw = self.aviary.get_state()           # PyBullet 专有格式
        return DroneState(raw[:3], raw[3:6], quat_to_mat(raw[6:10]), raw[10:13])
    def send_command(self, cmd):
        self.aviary.step(ctbr_to_rpm(cmd))      # 翻译成 PyBullet 要的 RPM

class MAVROSBridge(Bridge):
    """真机/PX4 SITL 适配——和 §P0.D.4 的 offboard 一致"""
    def __init__(self, mavros): self.mavros = mavros
    def get_state(self):
        odom = self.mavros.get_odometry()       # MAVROS 话题
        return DroneState(odom.pos, odom.vel, odom.rot, odom.angvel)
    def send_command(self, cmd):
        self.mavros.publish_attitude_target(    # AttitudeTarget 消息
            thrust=cmd.thrust, body_rate=cmd.body_rate)

# ---- 上层算法:完全不知道底下是仿真还是真机 ----
def control_loop(bridge: Bridge, policy):
    """同一份控制循环,跑仿真还是真机只取决于传入哪个 bridge"""
    while True:
        state = bridge.get_state()              # 只认 DroneState
        action = policy(state)                  # 算法逻辑,与目标无关
        bridge.send_command(CTBRCommand(action.thrust, action.body_rate))

# 切换目标 = 换一个 bridge 实例,control_loop 和 policy 一字不改:
control_loop(PyBulletBridge(aviary), policy)    # 跑仿真
# control_loop(MAVROSBridge(mavros), policy)    # 跑真机——同一份 policy!

这段代码的关键设计点

  • DroneState / CTBRCommand 是"通用货币":算法只和这两个结构打交道。PyBullet 返回的数组、MAVROS 返回的 odometry,都在各自的 Bridge 里翻译成 DroneState。算法看不到原始格式差异。
  • control_looppolicy 与目标完全解耦:切换仿真/真机,只换 Bridge 实例,这两个函数一字不改。这就是"一份代码跑仿真和真机"。
  • CTBR 作为统一指令的妙处(呼应 §P0.D.3):正因为仿真和真机都用 CTBR 接口,CTBRCommand 才能当通用货币。如果仿真用 RPM、真机用 CTBR,Bridge 接口就难以统一——接口一致性(§P0.D.3)是 Bridge 模式能成立的前提

多视角理解:Bridge 模式对照机械臂的 ros2_control

跨领域类比(标注边界):无人机的 Bridge 模式,和机械臂的 ros2_control 硬件抽象层(HardwareInterface)是同一个设计思想的两种实现像的地方:两者都把"算法逻辑"和"具体硬件/仿真"解耦——机械臂的控制器(如 JointTrajectoryController)只认 ros2_control 的标准接口(读关节状态、写关节指令),底下接的是 Gazebo 仿真还是真实机械臂,由一个可插拔的 HardwareInterface 实现决定。和无人机的 Bridge 切换仿真/真机一模一样的精神。 不像的地方:① 实现重量不同——ros2_control 是重型的 pluginlib 插件体系(工控级、强类型、生命周期管理完备),无人机的 Bridge(如 Agilicious)是轻量的 C++ Factory(嵌入式友好、header-only 倾向)。② 接口语义不同——机械臂接口是关节空间(position/velocity/effort),无人机是 CTBR(推力+角速度)。③ 实时性要求不同——机械臂 1kHz、无人机内环 2-8kHz。 不要把这个类比延伸成"两者可以互换框架"——它们的设计思想相通,但具体实现因领域约束(嵌入式 vs 工控、CTBR vs 关节)而高度不同。这正是本章开头"性质:部分共享"的含义:设计模式共享,具体实现完全不同

⚠️ 常见陷阱

⚠️ 编程陷阱:Bridge 抽象做了,但状态/指令的坐标系/单位约定不统一
   错误做法:PyBulletBridge 返回的角速度是世界系,MAVROSBridge 返回的是机体系,
            但都塞进同一个 DroneState.angvel 字段不加说明
   现象:同一份控制器在仿真里正常,切到真机角速度反馈"方向不对",控制器行为诡异
   根本原因:Bridge 统一了"数据结构",但没统一"数据语义"(坐标系、单位、方向约定)。
            DroneState 的字段格式一样,含义却不一样,比格式不一样更隐蔽
   正确做法:在 DroneState/CTBRCommand 的定义里明确写死语义契约(如"angvel 一律
            机体系、rad/s、右手系"),每个 Bridge 实现负责把原始数据转换到这个
            统一语义。语义契约比数据结构更重要——这是抽象的真正难点
💡 概念误区:以为"有了 Bridge 抽象,仿真和真机就完全一样了"
   新手想法:"既然一份代码跑仿真和真机,那仿真测过的真机肯定也对"
   实际上:Bridge 统一的是"软件接口",消除的是"代码不一致"这一类 gap。但它
          完全不触及 §P0.D.7 的六大物理 gap——仿真的电机模型、气动、传感器噪声
          和真机仍然不同。Bridge 让仿真和真机跑同一份代码,但它们的"物理世界"
          依然不同
   正确理解:Bridge 解决"代码层一致性",域随机化/SysID 解决"物理层 gap"。两者
            正交、缺一不可。有了 Bridge 还是要做 §P0.D.7 的全套 sim-to-real
            工作。Bridge 是必要条件,不是充分条件
🔧 配置陷阱:真机实验没有独立安全网(Guard),实验控制器一发散就炸机
   错误做法:把实验性的 RL/MPC 控制器直接接上真机,安全检查复用控制器自己的估计器
   现象:控制器发散时,依赖同一估计器的安全检查也失效,无人机失控坠毁
   根本原因:安全网依赖了"被测对象本身"——Pilot 崩了,挂在 Pilot 上的安全检查
            也跟着崩,形同虚设
   正确做法:仿照 Agilicious 的 Guard——用独立的估计器(mocap/LIO)、独立运行的
            安全管线监控 Pilot,一旦位姿偏差/越界/超时就接管紧急悬停。真机实验的
            安全网必须独立于被测对象。仿真里可以省略 Guard(崩了 reset 即可),
            真机绝不能省

练习

  1. 【管线搭建·实现一个 Bridge】 基于本节的 Bridge 抽象骨架,为 gym-pybullet-drones 实现一个完整的 PyBulletBridge(含 CTBR→RPM 的转换)。然后写一个最简单的 P 控制器(只用位置误差),通过 control_loop 驱动它悬停。验收标准:无人机能悬停在目标点。预计 60 分钟。

  2. 【对比练习·语义契约】 列举 DroneState 的每个字段(pos/vel/rot/angvel)可能存在的"语义歧义"(坐标系、单位、方向、原点)。为每个字段写一条明确的语义契约。验收标准:契约足够明确,任何人读了都能写出行为一致的 Bridge 实现。预计 25 分钟。

  3. 【综合思考题·Bridge vs ros2_control】 用一段话总结:无人机 Bridge 和机械臂 ros2_control 在"设计思想"上的共同点,以及在"具体实现"上的三个关键差异(实现重量、接口语义、实时性)。然后回答:为什么无人机不直接用 ros2_control,而要自己搞一套轻量 Bridge?(提示:嵌入式算力、C++ 版本、控制频率。)预计 30 分钟。


§P0.D.9 选型决策树与完整工作流——给项目选对工具 ⭐⭐

模块功能与在管线中的位置

这是全章的收口。前面八节给了全景(§P0.D.1)、取舍逻辑(§P0.D.2)、各路线的搭建(§P0.D.4-6)、sim-to-real(§P0.D.7)和代码组织(§P0.D.8)。这一节把它们浓缩成一个**可直接执行的选型决策树 + 工作流**——下次接到无人机项目,照着走就能选对仿真器、规划好从仿真到真机的完整路径。

动机:为什么需要一棵决策树

选型的难点不在"不知道有哪些工具"(§P0.D.1 的表已列全),而在"面对具体任务时,那么多维度怎么权衡"。决策树的价值是把"权衡"变成"顺序提问"——按固定顺序问几个关键问题,每个回答砍掉一批选项,最后收敛到 1-2 个候选。这比每次都把六个仿真器在七个维度上两两比较高效得多。

理论:无人机仿真器选型决策树

开始:你的核心任务是什么?
├─【飞控在环验证】(验证 PX4/ArduPilot 上的控制器/任务逻辑)
│    │
│    ├─ 要光真渲染(视觉感知也要验证)?
│    │    ├─ 是 → Pegasus(Isaac Sim):PX4 + 光真 PhysX
│    │    └─ 否 → Gazebo + PX4 SITL:物理保真闭环(§P0.D.4)★ 最常用
│    └─ 要复现一篇基于 RotorS 的老论文? → RotorS
├─【强化学习训练】(训练 RL 策略)
│    │
│    ├─ 状态策略(观测=位置/速度/姿态,不看图像)?
│    │    ├─ 要极致并行(百万级)/几何控制 → Aerial Gym(§P0.D.6)★
│    │    └─ 要 Isaac Lab 生态/多机/基准 → OmniDrones
│    │
│    └─ 视觉策略(观测含图像)?
│         ├─ 只用深度图(避障)→ Aerial Gym(GPU 光线投射深度)★
│         ├─ 要光真 RGB、合成场景够用 → OmniDrones / Flightmare
│         └─ 要部署到特定真实场景、极小渲染 gap → 3DGS(FiGS 系)
├─【入门 / 教学 / 快速原型】(学习、Crazyflie 原型、多机教学)
│    └─ gym-pybullet-drones:CPU 即可、多模式物理、教学友好(Quick Start)★
└─【VIO / 视觉竞速 / 人机交互研究】
     └─ Flightmare:Unity 光真 + 多模态传感器 + VR(§P0.D.5)

怎么用这棵树:从顶端"核心任务"开始,每个分叉问一个 yes/no 或选项问题,沿着匹配的分支往下走,叶子节点就是推荐工具(★ 标的是该分支最常用的)。注意一个项目可能走多个分支——比如"训 RL 策略 + 上 PX4 真机",先走【RL 训练】选 Aerial Gym 训练,再走【飞控在环】用 Gazebo+PX4 做部署前验证。树不是"只能选一个",而是"每个子任务选一个"。

理论:从仿真到真机的完整工作流(七步)

选好仿真器后,下面是把一个算法从零做到真机的标准工作流,串起本章所有知识点:

步骤 做什么 用什么(本章对应节) 验收标志
1 选型 按决策树定主训练仿真器 + 验证仿真器 §P0.D.1/9 选定 1 个训练器 + 1 个验证器
2 接口统一 确定动作空间(首选 CTBR)+ 搭 Bridge 抽象 §P0.D.3/8 算法只依赖统一 State/Command
3 环境搭建 配训练环境(num_envs/dt/decimation/obs/reward) §P0.D.6 Quick Start 跑通最小训练
4 域随机化 配 DR(六类 gap 对应的随机化项) §P0.D.7 训练含质量/电机/延迟/外扰随机化
5 训练 4096 并行 PPO,监控 FPS/reward/GPU 利用率 §P0.D.6 reward 收敛、GPU 利用率 90%+
6 sim2sim 验证 导出策略到验证仿真器跑 §P0.D.7 策略在第二个仿真器也能工作
7 真机部署 HITL → 真机小范围(带 Guard)→ 逐步放开 §P0.D.4/7/8 真机稳定,Guard 未触发

多视角理解:选型的两种心态

双重解读:面对选型,有两种心态,都对但适用场景不同: - 心态1(任务驱动,本节决策树):先想清楚任务要什么,再去找匹配的工具。适合**有明确目标**的工程项目——你知道要做视觉避障 RL,决策树直接带你到 Aerial Gym。 - 心态2(能力驱动,探索期):先熟悉手头工具的能力边界,再看能做什么。适合**探索期**——你刚入门,先用 gym-pybullet-drones 把 RL 全流程跑通,建立直觉,再根据"还差什么能力"去升级工具(如发现要并行就上 Aerial Gym)。

成熟的做法是两者结合:探索期用心态2 建立工具直觉(先把 Quick Start 跑通),项目期用心态1 精准选型(用决策树)。本章的结构正是这样安排的——前面让你建立全景和直觉,这里给你精准选型的决策树。

配置详解:选型时容易忽略的"隐性成本"

决策树看的是"能力匹配",但实际选型还要算几笔"隐性账":

隐性成本 说明 怎么权衡
硬件门槛 Isaac 系要 NVIDIA RTX + 大显存,不是人人有 没好显卡 → 入门先用 gym-pybullet-drones(CPU)
安装复杂度 Isaac Gym 要官网注册下载、Flightmare 要装 Unity 评估团队能投入的搭环境时间
维护状态 AirSim 已归档、Flightmare 停更(2021) 新项目优先选活跃维护的(Aerial Gym/OmniDrones/Pegasus)
社区与文档 用的人多 → 遇到坑好找答案 同等能力下,选社区活跃的
生态衔接 已在用 Isaac Lab 的足式项目 → OmniDrones 无缝 复用已有技术栈,减少学习成本

本质洞察(选型的终极原则):把能力匹配(决策树)和隐性成本(上表)合起来,选型的终极原则是——选"能完成任务的工具里,团队总成本最低的那个",而非"能力最强的那个"。一个能力稍弱但你团队已经熟悉、有好显卡、社区活跃的工具,往往比一个能力顶尖但要从零搭环境、显卡不够、文档稀缺的工具,能让项目跑得更快。这呼应了 §P0.D.1 的陷阱"星标数 ≠ 适配度"——最适合的工具,是任务能力和团队现实的交集,不是某个单一维度的最优。

⚠️ 常见陷阱

💡 概念误区:决策树走到一个叶子就认定"只能用这一个仿真器"
   新手想法:"决策树带我到了 Aerial Gym,那我整个项目就只用它"
   实际上:一个完整项目通常跨多个子任务,每个子任务可能走不同分支。典型的"训练
          +部署"项目要同时用训练仿真器(Aerial Gym)和验证/部署仿真器(gym-
          pybullet-drones 做 sim2sim、Gazebo+PX4 做飞控在环)
   正确理解:决策树是"为每个子任务选工具",不是"为整个项目选一个工具"。专业团队
            常态是手里 2-3 个仿真器按子任务切换——这正是 §P0.D.8 的 Bridge 抽象
            让多仿真器切换变得低成本的原因
🔧 配置陷阱:选了 Isaac 系仿真器才发现硬件不达标,项目卡死
   错误做法:看决策树推荐 Aerial Gym 就直接上,没先确认有没有 NVIDIA RTX 显卡
   现象:装到一半发现没合适的 GPU,或显存太小跑不了像样的 num_envs,项目停滞
   根本原因:只看了"能力匹配",没算"硬件门槛"这笔隐性账。Isaac 系强依赖 NVIDIA
            GPU,这是硬约束
   正确做法:选型时先过一遍隐性成本表,尤其硬件门槛。没有合适 GPU 时,要么先用
            CPU 路线(gym-pybullet-drones)做原型、要么租云 GPU、要么调整方案。
            硬件可行性应在选型第一步就确认,而非搭到一半才发现

练习

  1. 【选型练习·走决策树】 对下面四个真实项目,分别走一遍决策树,写出推荐的仿真器(可能不止一个)和理由:① 大学课程作业,教学生 RL 控制无人机悬停,机房只有普通 PC;② 实验室研究室内深度避障 RL,有 RTX 4090;③ 公司要在 PX4 上验证一个新的航点任务逻辑;④ 复现一篇 2017 年基于 Gazebo 的视觉伺服论文。验收标准:每个走到正确叶子且理由合理。预计 30 分钟。

  2. 【综合练习·完整工作流】 为"训练一个能上真机的室内悬停策略(有 RTX 显卡,最终上 Crazyflie)"项目,按七步工作流写出完整计划:每一步用什么工具、关键配置、验收标志。这是把全章串起来的综合练习。验收标准:七步齐全、每步对应到本章具体小节。预计 50 分钟。

  3. 【思考题·隐性成本权衡】 假设两个仿真器 A、B:A 能力略强但要从零搭环境(团队没人用过、要装 Unity)、B 能力够用且团队已熟悉。决策树倾向 A(能力匹配),但你最终选了 B。用"总成本最低"原则论证你的选择,并说明在什么情况下你会反过来选 A。预计 20 分钟。


本章常见误解汇总

下表汇总全章出现的核心误解,供快速回顾。每一条都是新手最容易建立的错误心智模型——读完本章后,你应该能对每一条给出"正确理解"的解释。

# 误解 正确理解 对应节
1 选星标最高/最流行的仿真器准没错 选型只看"任务-能力匹配度",星标是次要参考 §P0.D.1
2 一个仿真器能覆盖入门到 RL 全流程 六大仿真器分属三条技术栈,依赖互斥,须分环境 §P0.D.1
3 渲染质量高的仿真器物理也一定准 渲染和物理是解耦的两层,正交评估 §P0.D.1/2
4 提高仿真保真度总是好的、越高越好 保真度按任务敏感维度匹配,盲目堆是"伪精确" §P0.D.2
5 动作空间越底层(越接近电机)越精确越好 sim-to-real 要鲁棒性不要仿真内精确性,CTBR 故意不底层 §P0.D.3
6 SITL 跑通 = 真机一定能飞 SITL 验证软件层,物理 gap(六大来源)覆盖不到 §P0.D.4/7
7 渲染保真路线必须放弃物理保真 渲染和物理正交,可选"两者都要"的工具(牺牲并行) §P0.D.5
8 3DGS 重建的场景可直接做物理碰撞仿真 3DGS 是渲染表示,不含碰撞几何,要另配物理 §P0.D.5
9 仿真中跑通 = 真机能用 gap 来自六个独立来源,须 SysID+DR+sim2sim+渐进部署 §P0.D.7
10 域随机化范围越大越鲁棒 范围过大致策略保守/不收敛,应"恰好包住"不确定区间 §P0.D.7
11 有了 Bridge 抽象仿真和真机就完全一样 Bridge 解决代码一致性,物理 gap 仍需 DR/SysID §P0.D.8
12 决策树走到一个叶子就只用那一个仿真器 决策树是"为每个子任务选工具",项目常用多个 §P0.D.9

本章小结

本章不教任何控制或规划算法,而是教"怎么搭好让算法跑起来的仿真台子,并让仿真里的东西能搬到真机"。一句话串起全章:仿真器是三层解耦(物理/渲染/控制接口)的,这三层在 GPU 资源上构成不可能三角,所以没有最好只有最匹配的仿真器;选对工具、用对动作空间(CTBR)、系统治理 sim-to-real gap 的六大来源、用 Bridge 抽象统一代码——这是从仿真走到真机不崩的完整方法论。

术语速查表

术语 中英对照 定义 首见
SITL 软件在环(Software-In-The-Loop) 飞控固件运行在 PC 上、与仿真器闭环,验证软件逻辑 §P0.D.4
HITL 硬件在环(Hardware-In-The-Loop) 真机固件跑在真 Pixhawk 上、吃仿真传感器,验证硬件约束 §P0.D.4
CTBR 集体推力+机体角速度(Collective Thrust & Body Rates) 4D 中层动作空间(归一化推力 + 三轴角速度),sim-to-real 最鲁棒 §P0.D.3
SRT 单旋翼推力(Single-Rotor Thrust) 4D 底层动作空间(每个桨的推力),sim-to-real 难 §P0.D.3
RPM 转速指令(Revolutions Per Minute) 直接给电机目标转速,最底层动作空间 §P0.D.3
MAVLink 微型飞行器通信链路协议 无人机领域事实标准的轻量二进制消息协议 §P0.D.4
offboard 板外控制模式 PX4 允许外部程序通过 setpoint 流接管控制的模式 §P0.D.4
lockstep 锁步模式 仿真与飞控严格同步推进,保证确定性可复现 §P0.D.4
BEM 叶素动量理论(Blade Element Momentum) 高保真气动模型,用于高速飞行的气动残差建模 §P0.D.5
3DGS 三维高斯泼溅(3D Gaussian Splatting) 从真实照片重建可渲染场景孪生的新型渲染表示 §P0.D.5
num_envs 并行环境数 GPU 一次步进的独立环境数量,决定采样吞吐与显存 §P0.D.6
decimation 控制降采样因子 物理步与控制步的比值,解耦物理精度与决策频率 §P0.D.6
asymmetric actor-critic 非对称演员-评论家 critic 可用比 actor 更多的特权信息(如全局状态)训练 §P0.D.6
DR 域随机化(Domain Randomization) 训练时随机扰动物理参数,让策略对参数分布鲁棒 §P0.D.7
SysID 系统辨识(System Identification) 用真飞数据标定仿真参数,"对准中心" §P0.D.7
sim2sim 仿真到仿真验证 把策略从训练仿真器迁到验证仿真器,真机 gap 的廉价代用品 §P0.D.7
Bridge/Adapter 桥接/适配器模式 把"运行目标"收敛成可插拔适配器,一份代码跑仿真和真机 §P0.D.8
Guard 守卫(独立安全管线) 用独立估计器监控实验控制器,越界即接管,真机安全网 §P0.D.8

知识点总表

编号 知识点 核心要点 对应节 难度
K1 仿真器四代演进 G1 Gazebo→G2 光真→G3 GPU 并行→G4 3DGS,每代补上代短板 §P0.D.1
K2 六大仿真器选型矩阵 物理/渲染/并行/接口/飞控五维对比,从"最适场景"反查 §P0.D.1
K3 三问法快速定位新仿真器 看渲染引擎/并行规模/控制接口三问,给它在地图上定位 §P0.D.1
K4 三层解耦架构 物理/渲染/控制接口三层可独立替换,约束你的代码组织 §P0.D.2 ⭐⭐
K5 不可能三角 物理保真/渲染真实/并行规模抢同一份 GPU,最多选两个 §P0.D.2 ⭐⭐
K6 按需关层 状态策略关渲染换速度,由最终部署形态倒推关哪层 §P0.D.2 ⭐⭐
K7 CTBR/SRT/RPM 三接口 策略接入控制链的三个高度,决定 sim-to-real 难度 §P0.D.3 ⭐⭐⭐
K8 CTBR 为什么赢 把难仿的快变量外包给固件 rate loop,策略只学仿得准的刚体层 §P0.D.3 ⭐⭐⭐
K9 质量归一化推力 CTBR 推力用 m/s² 而非牛顿,与质量解耦,换机型不失效 §P0.D.3 ⭐⭐⭐
K10 Gazebo+PX4 SITL 闭环 真飞控固件在环,验证 EKF2/commander/故障保护等软件逻辑 §P0.D.4 ⭐⭐
K11 SITL vs HITL 数据注入点深浅不同:SITL 测软件,HITL 测硬件算力约束 §P0.D.4 ⭐⭐
K12 lockstep 模式 仿真按步走非按墙钟走,保证确定性可复现 §P0.D.4 ⭐⭐
K13 offboard 协议约束 切 offboard 前须先发 setpoint,频率须 >2Hz 否则触发保护 §P0.D.4 ⭐⭐
K14 Flightmare 双引擎解耦 ZeroMQ 连接渲染(230Hz)和物理(200kHz),物理简单渲染光真 §P0.D.5 ⭐⭐⭐
K15 渲染 gap 与视觉过拟合 视觉策略主战场在像素不在物理,低真渲染致"仿真捷径" §P0.D.5 ⭐⭐⭐
K16 3DGS 范式反转 从"仿真逼近真实"反转为"从真实生成仿真",渲染 gap 天然小 §P0.D.5 ⭐⭐⭐
K17 GPU 并行采样吞吐 全程零拷贝留显存,4096 环境把周级训练压到分钟级 §P0.D.6 ⭐⭐⭐
K18 num_envs 取值 GPU 显存与并行效率的甜点,与 PPO 批次配置耦合 §P0.D.6 ⭐⭐⭐
K19 sim_dt 与 decimation 联动 解耦物理精度与决策频率,改 dt 必须同步改 decimation §P0.D.6 ⭐⭐⭐
K20 观测用旋转矩阵 9D 旋转矩阵无奇异无双覆盖,比欧拉角/四元数对网络友好 §P0.D.6 ⭐⭐⭐
K21 奖励设计与动作平滑 exp 形奖励+速度正则+动作平滑项,平滑项是 sim-to-real 桥 §P0.D.6 ⭐⭐⭐
K22 sim-to-real gap 六大来源 动力学/气动/电机/传感器/延迟/接触,逐抽屉诊断治理 §P0.D.7 ⭐⭐⭐⭐
K23 SysID + DR 黄金组合 SysID 对准中心、DR 覆盖范围,缺一不可 §P0.D.7 ⭐⭐⭐⭐
K24 域随机化的真正含义 不是让仿真逼真,而是让策略对参数分布鲁棒,覆盖真实而非复制 §P0.D.7 ⭐⭐⭐⭐
K25 sim2sim 验证 跨仿真器迁移是真机 gap 的廉价代用品,筛掉脆弱策略 §P0.D.7 ⭐⭐⭐⭐
K26 Bridge/Adapter 模式 算法只依赖统一 State/Command,目标由可插拔 Bridge 实现 §P0.D.8 ⭐⭐⭐
K27 语义契约 Bridge 统一数据结构还不够,必须统一坐标系/单位/方向语义 §P0.D.8 ⭐⭐⭐
K28 Guard 独立安全网 真机安全网不能依赖被测对象,用独立估计器独立运行 §P0.D.8 ⭐⭐⭐
K29 选型决策树 按核心任务顺序提问,为每个子任务收敛到 1-2 个候选 §P0.D.9 ⭐⭐
K30 选型隐性成本 能力匹配之外算硬件/安装/维护/社区/生态账,选总成本最低 §P0.D.9 ⭐⭐

累积项目:本章新增模块

无人机累积项目总览:本方向的累积项目是"从零搭一个完整的四旋翼自主飞行栈",每章加一个模块。前序章节已完成微分平坦控制(D1/D2)、轨迹生成(D3/D5/D6)、感知规划(D7/D8)、RL 敏捷飞行(D9)。本章(P0.D,作为 Part 0 的工程基座)为整个项目补上**仿真与 sim-to-real 基础设施**——这是所有上层算法的运行底座。

本章新增模块:sim_infra/——统一仿真基础设施

project_drone_stack/
├── ...(前序章节模块:control/, planning/, perception/, rl/)
└── sim_infra/                      ← 本章新增
    ├── bridges/                    ← §P0.D.8 Bridge 抽象
    │   ├── bridge_base.py          ← Bridge ABC + DroneState/CTBRCommand
    │   ├── pybullet_bridge.py      ← gym-pybullet-drones 适配(入门/sim2sim)
    │   ├── mavros_bridge.py        ← PX4 SITL/真机适配(§P0.D.4)
    │   └── aerialgym_bridge.py     ← Aerial Gym 适配(RL 训练)
    ├── envs/                       ← §P0.D.6 RL 环境配置
    │   ├── hover_task_config.py    ← num_envs/dt/decimation/obs/reward 全配置
    │   └── domain_randomization.py ← §P0.D.7 六类 gap 的 DR 配置
    ├── sim2sim/                    ← §P0.D.7 跨仿真器验证
    │   └── validate.py             ← 加载策略到验证仿真器跑
    └── sitl/                       ← §P0.D.4 PX4 SITL
        └── takeoff_offboard.py     ← MAVSDK offboard 起飞悬停脚本

本章项目任务(按工作流逐步搭建):

  1. 搭 Bridge 抽象(§P0.D.8):实现 bridge_base.pyDroneState/CTBRCommand/Bridge ABC,写明语义契约。实现 pybullet_bridge.py 并用 P 控制器验证悬停。
  2. 搭 RL 环境配置(§P0.D.6):把悬停任务的全套配置(num_envs/sim_dt/decimation/obs/reward/termination)写成 hover_task_config.py,每个配置项加注释说明取值依据。
  3. 搭域随机化(§P0.D.7):实现 domain_randomization.py,覆盖六类 gap 对应的随机化项。
  4. 搭 SITL 脚本(§P0.D.4):跑通 takeoff_offboard.py,让无人机在 PX4 SITL 里起飞悬停降落。
  5. 搭 sim2sim 验证(§P0.D.7):实现 validate.py,把(D9 章训的或本章训的)策略导出并在 pybullet_bridge 里跑,验证跨仿真器迁移。

项目里程碑:完成后,你的项目就有了一个"算法无关、目标可切换"的仿真底座——上层的控制器(D1/D2)、RL 策略(D9)都能通过同一套 Bridge 在 gym-pybullet-drones / PX4 SITL / Aerial Gym 之间零重写切换。这是后续所有真机实验的基础设施。


延伸阅读

论文(按主题分类,标注难度)

仿真器本身: - ⭐⭐⭐ Song et al., Flightmare: A Flexible Quadrotor Simulator, CoRL 2020 — 双引擎解耦架构的奠基论文,必读 - ⭐⭐⭐ Kulkarni et al., Aerial Gym Simulator: A Framework for Highly Parallelized Simulation of Aerial Robots, 2025(arXiv 2503.01471)— GPU 并行 + SE(3) 几何控制 - ⭐⭐⭐ Vivekanandan et al., Pegasus Simulator: An Isaac Sim Framework for Multiple Aerial Vehicles, 2023(arXiv 2307.05263)— Isaac Sim + PX4 集成 - ⭐⭐⭐ Xu et al., OmniDrones: An Efficient and Flexible Platform for RL in Drone Control, RA-L 2024 — Isaac Sim RL 基准

动作空间与 sim-to-real: - ⭐⭐⭐⭐ Kaufmann et al., A Benchmark Comparison of Learned Control Policies for Agile Quadrotor Flight, ICRA 2022 — CTBR vs SRT 的权威基准,理解动作空间选择的必读 - ⭐⭐⭐⭐ What Matters in Learning A Zero-Shot Sim-to-Real RL Policy for Quadrotor Control? 2024(arXiv 2412.11764,SimpleFlight)— sim-to-real 系统性消融研究 - ⭐⭐⭐⭐ Song et al., Reaching the Limit in Autonomous Racing: Optimal Control vs. RL, Science Robotics 2023 — 高速飞行的 sim-to-real

敏捷飞行平台(回顾 D8): - ⭐⭐⭐⭐ Foehn et al., Agilicious: Open-Source and Open-Hardware Agile Quadrotor for Vision-Based Flight, Science Robotics 2022 — Pilot/Bridge/Guard 架构

下一代渲染: - ⭐⭐⭐⭐ Low et al., SOUS VIDE: Cooking Visual Drone Navigation Policies in a Gaussian Splatting Vacuum, RA-L 2025(FiGS)— 3DGS 仿真渲染

仓库与文档

项目精读清单(呼应骨架)

  • gym-pybullet-dronesgym_pybullet_drones/envs/BaseAviary.py — 理解 PyBullet 四旋翼的 _physics() 步进与多模式物理
  • Flightmareflightlib/include/flightlib/envs/vec_env.hppVecEnv<T> 模板化向量化环境
  • OmniDronesomni_drones/envs/single/hover.py — Isaac Lab 风格环境定义
  • Agiliciousagilib/include/agilib/pilot/pilot.hpp — Pilot/Pipeline/Guard 三层架构的 C++17 实现(回顾 D8)

本章与后续章节的关系

后续章节 关系 本章铺垫的知识点
D1/D2 微分平坦与几何控制 控制器是仿真要"装进去"的东西 仿真三层解耦中的"控制接口层";CTBR 与几何控制的关系(K7/K8)
D3/D5/D6 轨迹生成 生成的轨迹要在仿真里验证 SITL 闭环验证轨迹跟踪(K10);sim2sim 验证(K25)
D7/D8 感知规划与敏捷平台 视觉感知依赖渲染保真,平台依赖 Bridge 渲染保真路线(K14-16);Bridge/Guard 架构(K26-28)
D9 RL 敏捷飞行与 sim-to-real 本章是 D9 的工程基座 并行 RL 工厂(K17-21);sim-to-real gap 六大来源(K22-25)
D10 集群协同规划 多机仿真依赖并行能力 num_envs 并行(K18);多机仿真器选型(K2)

本章的定位(Part 0 的工程基座):P0.D 是无人机方向 Part 0 的唯一章节,定位是"所有上层算法的运行底座"。后续 D1-D12 的每一个算法(控制、规划、感知、RL、集群),都要在本章搭的仿真台子上验证、都要面对本章讲的 sim-to-real gap。先有台子,才有戏唱——这就是把仿真章放在 Part 0 最前面的原因。


🔧 故障排查手册

下表覆盖五个高频故障场景,重点是环境配置和版本兼容问题(工程实践类型的排查重灾区)。遇到问题先按"症状"定位,再走"排查步骤"。

场景 症状 可能原因 排查步骤 相关节
F1 Gazebo 启动失败 make px4_sitl gz_x500 报 gz-sim 找不到/版本不兼容 ① Ubuntu 20.04 装了 Harmonic(不支持);② Gazebo 版本与 PX4 不匹配;③ 环境变量未刷新 1) 确认 OS:lsb_release -a,20.04 须用 gazebo-classic_iris target;2) 22.04 确认 gz sim --version 是 Harmonic;3) 重跑 bash Tools/setup/ubuntu.sh 后**重启终端** §P0.D.4
F2 offboard 切换即退出 飞机刚进 offboard 就 fallback 到 hold/land,日志 "Offboard ... stopped" ① 切换前没先发 setpoint;② setpoint 频率 <2Hz;③ 未通过预飞检查(无定位) 1) 确认 offboard.start() 前已发 \(\ge\)1 个 setpoint;2) 控制循环频率提到 20-50Hz;3) 等 health.is_global_position_ok 为真再 arm §P0.D.4
F3 RL 训练异常慢 GPU 训练 FPS 远低于预期,nvidia-smi 显示 GPU 利用率个位数 ① 训练循环里有 .cpu()/.item()/print(tensor);② Python 端有阻塞瓶颈;③ num_envs 太小 1) 搜训练循环里所有 .cpu()/.numpy()/.item(),删掉或改 TensorBoard;2) 用 torch.profiler 看瓶颈;3) 加大 num_envs(在显存允许内) §P0.D.6
F4 CUDA out of memory 启动或跑几步后报 OOM ① num_envs 太大;② 视觉任务相机分辨率太高;③ 显存被其他进程占 1) nvidia-smi 看显存占用和其他进程;2) num_envs 从小往大试(512→1024→…);3) 视觉任务降分辨率或降 num_envs(通常比状态任务小一量级) §P0.D.6
F5 依赖冲突/import 崩溃 pip install 报版本冲突,或 import isaacgym/omni_drones 崩 ① 多个仿真器装进同一环境;② Isaac 版本与 Pegasus/OmniDrones 错配;③ PyTorch 与 CUDA 不匹配 1) 按用途切分 conda 环境,别一锅炖;2) 严格按版本兼容表配对(Isaac 5.1↔Pegasus v5.1);3) python -c "import torch; print(torch.cuda.is_available())" 验证 PyTorch-CUDA 匹配 §P0.D.1

排查总原则:环境配置类故障(F1/F5)占新手问题的大多数,根因几乎都是"版本错配"或"环境混装"。养成两个习惯能避开 80% 的坑——① 严格按版本兼容表配对、锁精确小版本;② 按用途切分独立环境(入门/SITL/RL 各一个),永不混装。运行时故障(F2/F3/F4)则要善用日志和监控工具(PX4 终端日志、TensorBoard、nvidia-smi)定位,而非盲改。


版本信息速查

本章涉及的所有工具/库/框架版本号汇总(撰写时的推荐配置,使用时请对照各项目最新文档):

工具/库 推荐版本 关键依赖约束 备注
Ubuntu 22.04 LTS Gazebo Harmonic 与 Isaac 的共同要求;20.04 仅能跑 Gazebo Classic
Python 3.10 Isaac Gym 锁 3.8,Isaac Lab 主推 3.10 不同仿真器对 Python 版本要求不一,分环境隔离
PyTorch 2.1.0 须与 CUDA 严格匹配 torch.cuda.is_available() 验证
CUDA 11.8 / 12.x 与 PyTorch 和 NVIDIA 驱动匹配 见 v8 Ch36
PX4-Autopilot v1.15 v1.14 起默认仿真器切到 Gazebo(gz)
Gazebo Harmonic(gz-harmonic) 仅 Ubuntu 22.04+ 20.04 用 Gazebo Classic 11
ROS 2 Humble Ubuntu 22.04 当前 LTS 主力;MAVROS 走 ROS 2
MAVSDK-Python 最新稳定版 offboard 控制 SDK
QGroundControl 最新稳定版 地面站,下载 AppImage
gym-pybullet-drones 1.0.0 pybullet>=3.2gymnasium>=0.28stable-baselines3>=2.0 入门首选
Flightmare master(2021) Unity 2020.1.10f1、C++14 已停止维护
Isaac Sim 4.5 / 5.1 Pegasus v5.1↔Isaac 5.1,v4.5.1↔Isaac 4.5
Isaac Gym Preview 4(1.0rc4) Python 3.8 Aerial Gym 依赖,NVIDIA 已停更
Isaac Lab 与 Isaac Sim 配对 旧称 Orbit,OmniDrones 依赖
Aerial Gym Simulator main(2025) Isaac Gym Preview 4 + Python 3.8 RL 路线主推
OmniDrones 0.1 / devel Isaac Sim + Isaac Lab RL 基准研究
Pegasus Simulator v5.1.0 / v4.5.1 对应 Isaac 5.1 / 4.5 Isaac Sim + PX4 集成
RotorS master Gazebo Classic + ROS 1/2 经典论文复现
stable-baselines3 2.3.0 gymnasium API 入门 RL 算法库

版本锁定再强调:仿真器生态版本耦合极强,务必锁到精确小版本并按用途隔离环境。Isaac 生态尤其敏感——Isaac Sim 大版本间的 Python 扩展 API 有 breaking changes,Pegasus/OmniDrones 必须配对应版本,错配会在 import 阶段直接崩(见故障 F5)。建议用 pip freeze > requirements.lock 固化每个环境,并在 README 记录"本项目在 Isaac X.Y + PyTorch A.B + CUDA C.D 上验证通过"。