Day 55: (Dis)armed and (not) dangerous, props to everyone!
The project is complete and successful! I officially have designed, built, and wired up an octocopter,
trained an RL policy to sustain flight through single- and dual-motor failures, and deployed it zero-shot
to
the real drone -- compiled directly into ArduPilot's C++ (at the AP_Motors level), with no companion
computer and no fine-tuning after simulation. On hardware, it goes on to survive not only single and
dual motor failures, but also
out-of-domain triple and quad failures.
dual failure (happens during zoom in), for a total of 3 dead motors.
The final controller is a small neural network -- a two-hidden-layer MLP (128 units each, ~43k parameters) -- running at 50 Hz onboard the drone's own flight controller. It reads the last 10 state frames (~0.2 s of history): attitude, angular rates, position, velocity, and one 8-value channel giving each motor's current thrust level, where any failed motor is forced to zero. That zero is a supplied fault signal -- the policy is told which motors are out rather than sensing it (in these tests the failures are commanded, so no ESC/RPM telemetry is needed); its job is to fly given that information. It outputs eight per-motor commands as small adjustments around hover. It was trained entirely in sim (MuJoCo + PPO via PufferLib) with a fixed mixture of zero-, single-, and dual-failure episodes present from the start, under a curriculum that ramps domain randomization from nominal to full. The one architectural trick is a symmetry-averaging head: at every step the network runs its weights twice -- once on the true state, once on that state relabeled by a 180° rotation of the airframe -- and averages the two, which makes it exactly equivariant to that rotation (to floating-point precision, at zero extra parameters).
The policy was only ever trained on 0, 1, or 2 dead motors (the fault mix is
(0.1, 0.2,
0.7) for 0/1/2), yet on the real airframe it held station through triple and quad
failures too. Zero-shot generalization past the training
envelope, on real hardware, is honestly more than I set out to prove -- I only ever asked the policy for
dual-failure recovery, and the airframe cleared double that. I assumed the reason it stretched so far was
a quirk in how the policy learned -- its habit of collapsing to four motors, described below. Testing that
later showed the opposite: the collapse cost it out-of-distribution survival rather than buying it
(see the edit note below). It generalized in spite of that quirk, not because of it.
In many cases, my octocopter had taught itself to fly with less motors than it has available. It drives itself down to roughly four motors and holds pose there, doing this even in certain trials of nominal hover, with all eight motors perfectly healthy. When four motors die, the drone is often in the regime it would choose to live in anyway.
fly as a quad, despite only one motor loss.
This isn't one bug in one place, it's a handful of design choices/mistakes composing into a strategy the optimizer was completely free to take:
- Nothing in the reward rewards using all eight motors. My reward is pose-only -- altitude, tilt, yaw, rate, position. Holding the target pose on 8 motors and holding it on 4 score identically, so parking a healthy motor is free.
- 70% of its life was already a reduced-motor drone. With dual failures weighted at 0.7, the policy spent most of training flying six motors, not eight -- and it's told which are dead (I ended up going with explicit fault detection in the final policy). Its behavioral center of mass is a degraded octo. It never learned "fly the octo, patch around faults"; it learned "find a surviving subset and fly that." Nominal 8-motor hover is the rare case, not the default.
- Hovering an octo on all 8 motors is over-actuated -- 8 thrusts against 4 constraints (lift + 3 torques), an infinite space of equally-valid allocations, with yaw as the weak, ill-conditioned axis (it makes ~11× less torque per newton than roll/pitch). That floppy null space is hard to learn a crisp policy for, so the optimizer collapses it to a minimal four-motor set. Not a quadcopter, though: the four it keeps are yaw-balanced (two CW, two CCW), but no opposing pair is ever among them. Two sit 45° apart and two ~135° apart, and the adjacent pair -- whose thrust vectors reinforce into a large roll/pitch moment -- runs at roughly half the thrust of the spread pair. That ~2:1 split cuts the residual roll/pitch moment from ~0.56 to ~0.014 N·m. And because my pose reward pays for tightness of hover, that configuration measurably scores higher.
Edit (Jul 31, 2026): In the interest of training the truly most effective policy, I went back and added a reward for using every available motor. Retrained from scratch with everything else held identical -- same dynamics, same pose reward weights, same(0.1, 0.2, 0.7)fault mix, same network, same hyperparameters, same 20M steps -- plus one per-step term:
r_usage = −wu · (1 − n_active / n_available)
n_available counts only healthy motors, so a motor the sim has killed is never held against the policy; n_active counts healthy motors commanded above 5% of hover throttle, the same threshold I use to measure the collapse. wu = 0.55, set by measuring what collapsing buys: dropping 8 motors to 4 gains ~0.134 pose reward per step, so the penalty has to beat that. The result holds all eight motors in hover (20/20 episodes) for 3.6% less hover precision (0.9484 vs 0.9838 mean per-step pose reward), and survives out-of-distribution failures substantially better. Both policies flown on every physically survivable failure set -- feasibility decided by a static control-allocation LP, so unrecoverable geometries count against neither -- under full domain randomization, identical sets and seeds:
Motors lost Trained on? Original (deployed on 7-23) policy Revised (trained on 7-31) policy Difference 0 (hover) yes 100.0% 100.0% – 1 yes 100.0% 100.0% – 2 yes 92.9% 97.1% +4.2 pp 3 no 90.0% 99.0% +9.0 pp 4 no 64.0% 85.0% +21.0 pp
I consider this project a huge success, because the end goals were fully met, no matter how the
final policy got there. The honest caveat is that this is textbook reward-hacking: the agent
maximized exactly the objective I
specified, which was admittedly faulty. A great lesson for the future, and I'm choosing to not beat myself
up over it :D
The core
claim is done and confirmed on real hardware: design → build → train → zero-shot deploy,
surviving failures it was never trained for. I do plan to revisit this drone and do further projects with
it (maybe computer vision!), but the work is done for now. I'm feeling incredibly proud and happy of
what's been accomplished. Thank you to everyone who followed along. Props to everyone who helped along the
way
(pun very much intended). 🚁