Semester project, EPFL — DECODE lab, Spring 2026. Student: Niels Rigaud (SCIPER 341284) · Supervisors: Simone Baratto, Nicolas Kirsch, Daniele Martinelli · Professor: G. Ferrari Trecate.
This project carries the meta-learning quadcopter controller of Goffin (previous
semester, simulation-only, see MetaLearning-Control-Quadcopters-main/) to a real Crazyflie, with an explicit sim-to-real
validation. The main additions are: a simulator made faithful to the Crazyflie by
porting its cascade PID firmware from C to Python, a hybrid NN–PID
controller (the network replaces only the outer two PID loops), and MAML
applied to an offset-mass disturbance, trained in simulation and flown on
hardware.
Read the report first. The full write-up is
Report_Latex_Overleaf/main.pdf. Throughout this README, "§N" refers to a section of that report. The folder map below tells you which directory produced which part of the report.
Everything (simulation, training, real-drone flights, plotting) runs in a single conda environment on Python 3.13:
conda create -n venv_cf_313 python=3.13
conda activate venv_cf_313
pip install -r requirements.txtrequirements.txt pins every library the code uses (versions this project was
validated against):
-e ./gym-pybullet-drones— the vendored simulator, installed editable; its own install pullspybullet,gymnasium,scipy,Pillow.torch,numpy,matplotlib,PyYAML,pybullet— training, differentiable dynamics, plotting, and the YAML task configs.cflib,cfclient— real-drone radio comms and the config client; only needed to fly the physical Crazyflie (fly_*_cf_pid.py --mode attitude).
maml_lib/ and crazyflie_firmware/ are local packages imported by name
(e.g. from maml_lib import ..., from crazyflie_firmware.firmware import ...).
Run scripts from the repository root so these resolve, e.g.:
python training_MAML/train_maml.py ...
python circle_comparison_simu_and_real/cf_firmware_pid_sim.py ...Meta-training was run on the Izar GPU cluster. The run_*.sh files in
training_MAML/ are SLURM batch scripts; on the cluster they use a separate venv
venv_MAML_SCITAS and are submitted with sbatch:
module load gcc python
source venv_MAML_SCITAS/bin/activate
pip install --no-cache-dir <lib> # always --no-cache-dir on Izar
sbatch training_MAML/run_maml_izar_offdirmag.shSCITAS docs: https://scitas-doc.epfl.ch/.
| Directory | Report § | Role |
|---|---|---|
gym-pybullet-drones/ |
§3.1 | PyBullet drone simulator (vendored, editable). DYN physics mode + offset-mass support. |
compare_models_openloop/ |
§3.2 | Open-loop comparison of gym-pybullet dynamics vs Goffin's model. |
crazyflie_firmware/ |
§2.2, §3.3 | Crazyflie cascade PID firmware ported C→Python (used in sim and as the inner loops). |
circle_comparison_simu_and_real/ |
§4 | Sim-to-real validation: circle trajectory, Flowdeck vs Lighthouse. |
training_regulation_simu_and_real/ |
§5 | Classic-regulation NN–PID controller: training, sim, real flights. |
maml_lib/ |
§2.5, §6 | Differentiable MAML library (dynamics, PID chain, policy, cost, tasks). |
training_MAML/ |
§6 | MAML for offset-mass compensation: training, PyBullet eval, real flights. |
MetaLearning-Control-Quadcopters-main/ |
§1.2 | Goffin's prior work (theory + simulation-only MAML). Reference, not run here. |
Report_Latex_Overleaf/ |
— | The report itself (main.tex, main.pdf, images/). |
ANGLE_CONVENTIONS.md |
§2.1 | Reference note on body rates vs Euler-angle derivatives and sign conventions. |
Pipeline order (mirrors the report): simulator → model comparison → firmware port → sim-to-real circle → regulation NN → MAML.
Vendored copy of gym-pybullet-drones, installed editable. We use the DYN physics mode (explicit rigid-body dynamics with quaternion orientation integration) rather than the default controller.
Touch-points added/used by this project (in gym_pybullet_drones/envs/BaseAviary.py):
_integrateQ— quaternion orientation integration (avoids gimbal lock).set_offset_mass— Steiner-corrected inertia + shifted CoM for thePhysics.DYN_OFFSETmode used during MAML offset-mass training/eval.
You normally don't edit this folder; the project drives it from the outside.
Shows that gym-pybullet's dynamics and Goffin's model agree in open loop.
goffin_dynamics.py— Goffin's nonlinear + linearized model re-implemented, andgym_state_to_goffin, the only explicit body-rate → Euler-rate converter in the repo (seeANGLE_CONVENTIONS.md§3.1).run_dynamics_comparison.py— runs the same open-loop input through PYB/DYN and Goffin's nonlinear/linearized models and plots trajectories + single-step error.results_comparison/— output figures (the report usescross_*plots).
python compare_models_openloop/run_dynamics_comparison.pyThe real Crazyflie's 4-loop cascade PID (position → velocity → attitude → rate → motors), ported from the official C firmware to Python. This is what makes the closed-loop simulation faithful to the hardware, and it provides the inner loops (attitude + rate) that the NN feeds into.
constants.py— gains, rates, saturations, timing (ATTITUDE_RATE, etc.).firmware.py— the PID controllers and power distribution.- Imported by the regulation and MAML flight/sim scripts; not run directly.
Commands a circle trajectory in simulation and on the real drone, and compares the two. Motivates the switch from Flowdeck (optical-flow jitter) to Lighthouse.
cf_firmware_pid_sim.py— the driver: runs the sim (and processes real logs), produces the comparison / top-view / jitter / metrics figures.flights/— recorded logs:flight_{flowdeck,lighthouse}_<timestamp>.npz(the20260523pair backs the report figures).metrics/— per-flight numeric metrics and discrepancies (*.json), the numbers behind the §4.3 error discussion.
Figures were removed to keep the repo clean (they live in the report's images/);
regenerate them from the kept logs with cf_firmware_pid_sim.py.
The hybrid NN–PID controller that regulates the drone to a setpoint. The network replaces the outer position/velocity loops (outputs roll/pitch/thrust + yaw-rate setpoints), the cascade PID handles attitude/rate/motors.
train_nn_cf_pid.py— trains the policy against a differentiable near-hover model (BPTT, single-step horizonT=1, curriculum reset, observation-noise domain randomisation). Savestrained_cf_pid_T1_ch200_h64_ep<E>_<tag>.pt.fly_nn_cf_pid.py— deploys the four-phase pipeline (PID takeoff → hover → NN regulation → PID landing) in--mode sim(PyBullet) or--mode attitude(real drone); logs toflights/flight_{sim,real}_<timestamp>.npz.plot_report_figures.py— evaluates a checkpoint on the 27-point init cube → the §5.3 regulation figures.plot_sim_real_overlay.py— overlays a sim vs a real flight → the §5.4 figures (defaults to the latest pair inflights/).- Deployed model:
trained_cf_pid_T1_ch200_h64_ep50_noisy.pt(the report's §5 results; 50 epochs, terminal weight 50, noise scale 2.0).
# train (defaults ≈ report hyper-params)
python training_regulation_simu_and_real/train_nn_cf_pid.py --tag noisy
# deploy in sim (--mode sim), then on the real drone (--mode attitude)
python training_regulation_simu_and_real/fly_nn_cf_pid.py --mode sim --weights training_regulation_simu_and_real/trained_cf_pid_T1_ch200_h64_ep50_noisy.pt
python training_regulation_simu_and_real/fly_nn_cf_pid.py --mode attitude --weights training_regulation_simu_and_real/trained_cf_pid_T1_ch200_h64_ep50_noisy.pt
# report figures
python training_regulation_simu_and_real/plot_report_figures.py --weights trained_cf_pid_T1_ch200_h64_ep50_noisy.pt
python training_regulation_simu_and_real/plot_sim_real_overlay.pyReusable, fully differentiable pieces that training_MAML/ composes. Everything is
batched over tasks and drones.
| Module | Role |
|---|---|
config.py |
Physical constants / timing for the CF2X (mass-baseline). |
mass_params.py |
Per-task mass parameters: total mass, CoM offset, Steiner inertia. |
dynamics.py |
Differentiable nonlinear + linearized dynamics (offset-mass aware). |
pid_chain.py |
Differentiable firmware-like attitude+rate PID (inner loops). |
policy.py |
Single-step (T=1) policy MLP. |
rollout.py |
Differentiable closed-loop rollout (NN queried every step). |
cost.py |
Quadratic per-trajectory trajectory cost (batched). |
maml.py |
MAML (Finn et al. 2017) inner/outer loop + multi-task baseline. |
tasks/ |
Task distributions: uniform_mass, composite (mixtures). |
Meta-training and evaluation for the offset-mass task, built on maml_lib and the
crazyflie_firmware inner loops.
train_maml.py/train_baseline.py— meta-training (MAML) and the joint-training baseline. Task distribution comes entirely from the YAML files (single source of truth):tasks_offdirmag.yaml,target_offdirmag.yaml.run_maml_izar_offdirmag.sh/run_baseline_izar_offdirmag.sh— the exact SLURM invocations that produced the report models.test_maml_pybullet.py— few-shot adaptation + closed-loop PyBullet evaluation.compare_pybullet.py— the four-controller PyBullet comparison (firmware PID / baseline / baseline-adapted / MAML-adapted) → §6.3 figure + metrics CSV.eval_gap.py— held-out adaptation-gap curves.plot_report_curves.py— convergence / adaptation figures for §6.fly_maml_cf_pid.py— real-drone deployment of the four controllers with a taped payload →flights/maml_{sim,real}_<ctrl>_<mass>g_<timestamp>.npz.plot_real_compare.py— overlays the four real flights → §6.4 figure.- Models:
maml_nonlinear_h64_o1_n6_izar_offdirmag_ep500.pt(MAML meta-init) andbaseline_nonlinear_h64_n6_izar_offdirmag_ep500.pt(joint baseline). Theoffdirmagtask (crossed offset direction × magnitude) is the configuration where MAML beats the joint baseline.
# meta-train (locally; on Izar use the run_*.sh)
python training_MAML/train_maml.py --tasks-config training_MAML/tasks_offdirmag.yaml --target-config training_MAML/target_offdirmag.yaml ...
python training_MAML/train_baseline.py --tasks-config training_MAML/tasks_offdirmag.yaml --target-config training_MAML/target_offdirmag.yaml ...
# evaluate & compare in PyBullet
python training_MAML/compare_pybullet.py
# flights: --maml-ckpt is required; --mode sim (PyBullet) or --mode attitude (real drone);
# one --controller per run (pid | base | base-adapt | maml)
python training_MAML/fly_maml_cf_pid.py --maml-ckpt training_MAML/maml_nonlinear_h64_o1_n6_izar_offdirmag_ep500.pt --controller maml --mass 8.21 --mode attitude
# overlay the four real flights
python training_MAML/plot_real_compare.py --pid ... --base ... --base-adapt ... --maml ... --mass 8.21flights/ naming: maml_real_<controller>_<mass>g_<timestamp>.npz, controllers
pid / base / base-adapt / maml. Only the 20260611 real flights are
post-yaw-fix (see §4 below) — earlier real MAML logs predate that fix.
Goffin's repository (theory, LQR-vs-NN and meta-learning notebooks, all in simulation). Kept for reference and to reproduce the prior-work baseline; not part of this project's runtime.
main.tex (source), main.bib (IEEE bibliography), images/ (all figures, as
self-contained copies), main.pdf (compiled). Build with latexmk -pdf main.tex.
Read ANGLE_CONVENTIONS.md before touching the dynamics or
the sim↔real bridge. The essentials:
- State vector (code order):
[x, vx, y, vy, z, vz, φ, p, θ, q, ψ, r]— slots 7/9/11 hold body rates(p,q,r), not Euler-angle derivatives. They coincide only near hover; the training simulators make that small-angle approximation. - Pitch sign: PyBullet uses pitch-positive = nose down; the firmware uses nose up. At every PyBullet↔firmware/real boundary, θ and q are negated; roll and yaw are not.
- Yaw-rate sign (important): the Crazyflie
send_setpointyawrate argument has the opposite sign to the sim/training convention. Real-flight scripts negate the network's yaw-rate command (send_setpoint(roll, -pitch, -yaw_rate, thrust)). A missing negation here was the bug that initially sent the payload-laden drone to the ceiling; it was fixed for the2026-06-11MAML flights.
Generated figures were deleted from the working folders to keep the repo lean; the
report's copies live in Report_Latex_Overleaf/images/ and every figure is
regenerable from the kept models, logs, and scripts:
| Report figures | Regenerate with |
|---|---|
| §3.2 model comparison | compare_models_openloop/run_dynamics_comparison.py |
| §4 circle sim-vs-real | circle_comparison_simu_and_real/cf_firmware_pid_sim.py |
| §5.3 / §5.4 regulation | plot_report_figures.py / plot_sim_real_overlay.py |
| §6.1–6.3 MAML | plot_report_curves.py, compare_pybullet.py, eval_gap.py |
| §6.4 real MAML | plot_real_compare.py (on the 20260611 flights) |
The report's conclusion (§7) lists the open directions. The most actionable, with their code entry points:
- Harder task distribution — a centred payload is nearly rejected by the inner
integral loops; strongly offset / asymmetric / time-varying masses give MAML a
regime where it matters (
training_MAML/tasks_*.yaml,maml_lib/tasks/). - Closed-loop thrust on the real drone — the network emits an open-loop
feed-forward hover thrust; adding an integral/estimated hover-thrust channel would
close the last steady-state gap to the firmware cascade
(
training_regulation_simu_and_real/,training_MAML/fly_maml_cf_pid.py). - Model the communication latency in training — the off-board NN acts one radio
round-trip after the state it saw; delaying observations/actions in the simulator
would let the policy learn to anticipate it (
maml_lib/rollout.py,training_regulation_simu_and_real/train_nn_cf_pid.py). - Exact orientation kinematics — the training dynamics conflate
(p,q,r)with(φ̇,θ̇,ψ̇); differentiating through the exact quaternion dynamics would tighten the model (maml_lib/dynamics.py,train_nn_cf_pid.py; seeANGLE_CONVENTIONS.md).