无人机执行器故障注入与控制退化观测
本模块在 MuJoCo 四旋翼仿真底座 上增加可编程的执行器与传感器故障注入层, 把"完全失效、推力衰减、卡死、渐进退化"四种桨故障和 IMU 零偏/噪声/丢包变成可以一键复现的仿真条件, 并给出统一的飞行指标(存活率、最低高度、故障到坠地延时、触地速度、控制能量、偏航角速度)。
它对应课程任务的任务①(在虚拟环境中对无人机进行物理仿真,并通过键盘对机器人进行运动控制), 同时是后续「神经容错飞行」系列模块(故障检测、容错控制、故障迫降、端到端策略)的数据发生器与评测台。
为什么先做这一层
故障注入必须发生在"控制器下发推力"和"桨真正出力"之间,才能对上层算法保持透明: 同一个控制器既可以在健康条件下飞,也可以在单桨失效条件下飞,两者的差别只来自故障算子。 这样后续模块换控制器(几何控制器 / 加权重分配 / 神经网络策略)时,评测台一行都不用改。
1. 问题:单桨失效到底有多严重
四旋翼在悬停时四条旋翼的总推力约等于重力,单条桨承担 25% 的升力。一旦某条桨停转, 剩余三条桨即使按原指令继续出力,合力方向也会立刻偏斜,同时产生一个无法抵消的滚转(或俯仰)力矩。
实测(open_ground 场景,悬停 1.5 m, 注入故障,仿真 8 s):
| 情形 | 最低高度 | 故障→坠地 | 触地速度 | 结论 |
|---|---|---|---|---|
| 健康 | 1.500 m | 不坠地 | — | 基线稳定 |
| 桨推力衰减到 40% | 1.208 m | 不坠地 | — | 控制器有余量吸收 |
| 桨卡死在 60% 推力 | −113.0 m | 0.85 s | — | 不再响应指令 |
| 单桨完全失效(无补偿) | −109.8 m | 0.64 s | 5.13 m/s | 必坠 |
| 单桨完全失效 + 损失推力均摊给其余三桨 | −67.9 m | 0.68 s | — | 朴素补偿救不回来 |
| 渐进退化(0.25/s) | −33.3 m | 3.46 s | — | 缓慢恶化,可预测 |

两条结论决定了整个选题方向:①完全失效必然坠机,现象稳定可复现,非常适合做对照演示; ②简单的"把丢掉的推力分给其他桨"完全无效——这说明缺口需要检测 + 加权重分配 + 残差补偿这类 真正的方法去填,而不是调参数。

2. 系统架构
GeometricController / 自定义策略"] -->|"u_cmd (4 桨推力)"| B["FaultInjector
故障算子"] B -->|"u_act = g(t)·u_cmd + b(t)"| C["Quadrotor
MuJoCo 动力学"] C -->|"qpos / qvel"| D["传感器
IMU / 相机 / 激光"] C -->|"QuadrotorState"| A B -.->|"g(t) 增益真值"| E["FlightResult
指标与对比表"] C -.-> E
模块只做两件事:
- 在控制回路上插入一个乘性/加性故障算子(上面
B); - 把每次飞行的完整轨迹记录下来并折算成指标(下面
E)。
其余(动力学、场景、传感器、控制器)全部复用底座,一行都不改。
3. 故障模型
把第 条桨的实际推力写成"指令 × 增益 + 偏置":
四种桨故障用不同的 组合表达:
| 类型 | 增益 | 偏置 | 物理含义 |
|---|---|---|---|
loss |
桨停转(电机/电调损坏、桨叶脱落) | ||
degradation |
推力只能出到原来的 (桨叶损伤、电压跌落) | ||
stuck |
电调卡在固定占空比,不再响应指令 | ||
progressive |
每秒钟损失 的推力(缓慢恶化) |
其中注入过渡为
是注入时刻(onset), 是过渡时长(ramp; 时故障瞬间生效)。
传感器故障走的是另一条路:它不改推力,而是篡改控制器读到的 IMU 角速度。实现分两步——先按过渡加零偏/噪声/丢包,再把被污染的角速度积分成控制器用的姿态估计:
| 参数 | 作用 |
|---|---|
gyro_bias |
角速度零偏(默认 0.1 rad/s),叠加在滚转通道 |
noise_scale |
噪声放大倍数,实际噪声 = SENSOR_NOISE_BASE(0.02 rad/s)× (noise_scale − 1) |
dropout |
丢包率,按概率把读数替换为上一帧 |
onset / ramp |
与旋翼故障共用同一套分段生效规则, 即上式的 |
零偏本身不会让机身倾斜——它先污染姿态估计,控制器按错误的姿态做反馈,真实姿态才慢慢偏、位置跟着漂;零偏再大就真的飞不回来。这一步是"传感器故障必须接进控制回路"的关键:只改读数、不积分姿态的话,0.5 rad/s 的零偏 8 s 只漂 1 cm,等于没有故障。
4. 源码解析
4.1 故障算子 fault.py
核心是一个不可变的故障描述加一个无状态计算器:
@dataclass(frozen=True)
class RotorFault:
rotor: int # 0~3
kind: str # loss / degradation / stuck / progressive
onset: float = 2.0 # 注入时刻 (s)
severity: float = 0.0
ramp: float = 0.0
def gain(self, t, max_thrust) -> float: ...
def offset(self, t, max_thrust) -> float: ...
FaultInjector 把多条故障合成两个长度为 4 的向量,再一次性作用到指令上:
def apply(self, thrusts, t):
g = self.gains(t) # (4,)
b = self.offsets(t) # (4,)
return np.clip(g * thrusts + b, 0.0, self.max_thrust)
为什么用 np.clip
故障后的推力必须落在物理可达范围内。stuck 的偏置加上指令放大后很容易超过
,不裁剪会得到一个不存在于真实电调上的推力,指标也就失真了。
4.2 闭环运行器 runner.py
主循环只有五行,故障注入插在中间:
for k in range(n_steps):
t = k * cfg.dt
u_cmd = ctrl.act(quad, target, v_d, a_d, cfg.yaw_d) # 控制器出指令
u_act = injector.apply(u_cmd, t) # 故障算子改写指令
quad.apply_rotor_thrust(u_act) # 施加到模型
quad.step(1) # 推进物理
st = quad.state() # 观测
控制器接口统一为
act(quad, p_d, v_d, a_d, yaw_d) -> np.ndarray # 形状 (4,),单位 N
因此基线几何控制器、加权重分配控制器、神经网络策略可以同台对比,换控制器不改仿真代码。
传感器故障注入则通过一个只读代理插进来,让控制器看到的对象和真机不一样:
class _SensedQuad: # 冒充 Quadrotor 交给控制器
def state(self):
w = injector.sense_omega(real.omega, t, rng, self._prev) # 篡改角速度
return replace(real.state(), omega=w, quat=self._quat_hat) # 姿态由 ω 积分
_quat_hat 只在传感器故障生效后开始用 积分(健康时不启用,所以基线保持 0 漂移);
控制器每拍拿到的是"带零偏的角速度 + 漂移的姿态估计",而物理推进仍然用真实状态——这样故障才真的
闭环进了控制律,而不是只写在结果表里。
控制器的"期望"和"实际"分开记录
FlightResult.desired 存控制器算出来的指令,FlightResult.thrusts 存故障之后的真实推力,
两者之差就是"分配器想给"和"真给到"的差,是后面做容错分配的直接信号。
4.3 指标 metrics.py
CRASH_ALTITUDE = 0.15 # 低于 0.15 m 记为坠地
SAFE_TOUCHDOWN_SPEED = 0.5 # 触地速度 < 0.5 m/s 记为安全着陆
六个核心指标:
值得单独说明:单桨完全失效后,四旋翼必须放弃偏航控制,靠机体自转来平衡 反扭矩才能维持姿态(Mueller & D'Andrea, ICRA 2014)。所以"存活且偏航角速度稳定在非零常数" 是容错控制真的按机制工作的证据,而不是靠调参凑出来的指标。
4.4 用例矩阵 scenario.py
5 个物理场景 × 7 种故障 × seeds
open_ground / obstacle_field / corridor / gate_track(8 门) / urban_block(14 楼)
healthy / single_loss / dual_loss / partial_degradation / stuck / progressive / sensor_bias
make_case() 在不显式指定时,用 seed 决定故障桨位(0~3)与注入时刻(1.5~3.5 s),
保证同一 seed 完全可复现;training_matrix() 自动剔除 healthy,直接作为后续模块的训练集来源。
5. 运行步骤
5.1 安装依赖
pip install "mujoco>=3.0" numpy pillow matplotlib pytest
底座 src/air/uav_mujoco_sim 不需要安装,源码树里可见即可(_compat.py 会自动定位)。
5.2 不用 ROS 直接跑
cd src/air/uav_actuator_fault_sim
python main.py --selftest # 自检 14 项
python main.py --demo # 健康 vs 6 种故障对比表
python main.py --eval results/comparison.csv # 完整矩阵 35 条
python main.py --gif results/fault_comparison.gif # 并排对比动图
Windows 双击 main.bat、Linux/Mac 执行 ./main.sh,无参数时默认 --demo。
5.3 键盘直飞(任务①要求的人机交互)
python main.py --keyboard # w/s 前后、a/d 左右、r/f 升降、q/e 偏航、空格清零
python main.py --keyboard --fault single_loss --onset 5 # 边飞边注入单桨失效
5.4 ROS2 Humble 启动
colcon build --packages-select uav_mujoco_sim uav_actuator_fault_sim --parallel-workers 4
source install/setup.bash
ros2 launch uav_actuator_fault_sim fault_sim.launch.py
launch 参数:scene / seed / fault / rotor / onset / severity / controller / seconds。
| 方向 | 话题 | 类型 |
|---|---|---|
| 发布 | /uav/odom |
nav_msgs/Odometry |
| 发布 | /uav/status |
std_msgs/String(READY / HOVER / FAULT / CRASHED / DONE) |
| 发布 | /uav/fault |
std_msgs/String |
| 发布 | /uav/health |
std_msgs/Float32MultiArray(4 桨增益真值) |
| 订阅 | /uav/cmd_vel |
geometry_msgs/Twist(机体系,0.5 s 超时自动悬停) |
| 订阅 | /uav/goal |
geometry_msgs/Point |
/uav/health 是给下游检测器用的监督标签:故障检测网络在训练时直接从这里取真值,
不需要额外标注流程。
6. 复现结果
python main.py --eval results/comparison.csv(5 场景 × 7 故障,每项 1 个 seed):
故障 样本 存活率 平均最低高度m 平均坠地延时s 平均水平漂移m 平均位置RMSE
-------------------------------------------------------------------------------------------------
healthy 5 1.00 1.50 0.00 0.000 0.00
single_loss 5 0.00 -77.17 0.64 7.042 28.35
dual_loss 5 0.00 -109.00 0.56 0.395 38.95
partial_degradation 5 1.00 1.21 0.00 0.151 0.37
stuck 5 0.00 -77.30 0.85 23.811 28.00
progressive 5 0.00 -16.17 3.46 1.905 4.24
sensor_bias 5 1.00 1.50 0.00 0.254 0.11
-------------------------------------------------------------------------------------------------
合计 35 0.43 -39.35 1.38 4.794 14.29
健康与推力衰减全部存活;三种"推力来源被切断"的故障(完全失效 / 双桨失效 / 卡死)全部坠机, 且坠机时间都在 1 s 以内;渐进退化因为推力是慢慢掉的,坠机被推迟到 3.4 s。
sensor_bias 的存活率和最低高度与 healthy 一样(它不改推力,机身不会掉),差异只体现在
水平漂移 0.254 m vs 0.000 m 和位置 RMSE 0.11 vs 0.00 上——汇总表补这两列正是为了让它可见。
零偏幅值越小越接近健康,加到 0.2 rad/s 以上就会坠机:
| 陀螺零偏 rad/s | 水平漂移 m(8 s 悬停) | 结果 |
|---|---|---|
| 0.02 | 0.065 | 存活 |
| 0.05 | 0.160 | 存活 |
| 0.10 | 0.309 | 存活 |
| 0.15 | 1.565 | 存活,最低高度降到 1.36 m |
| 0.20 | 6.497 | 坠机,故障后 5.7 s |
| 0.30 | 23.126 | 坠机,故障后 4.38 s |

单元测试与闭环测试:
python -m pytest test -q # 60 passed
python main.py --selftest # 19/19 通过
7. 局限与后续
- 目前故障只作用于桨推力与 IMU,没有做电机电压跌落、桨叶不对称等更细的模型。
- 故障注入是开环的(按时间表触发),没有做"基于检测结果自动触发"的闭环。
- 评测只覆盖悬停/简单机动,尚未接入航点跟踪与门穿越任务——这些留给后续模块
(
uav_rotor_fault_detector_nn、uav_neural_fault_tolerant_control、uav_fault_emergency_landing、uav_fault_e2e_depth_policy)。
8. 参考
- Mueller, M. W., & D'Andrea, R. Stability and control of a quadrocopter despite the complete loss of one, two, or three propellers. IEEE ICRA, 2014.
- 本仓库另见:MuJoCo 四旋翼仿真与 ROS2 桥接底座、 无人机键盘控制、强化学习对比实验(PPO/SAC/TD3)。