Quadcopter MIMO — Neural-LQR Controller with Real-Time 3D Animation
Files: examples/advanced/06_quadcopter_mimo/
What this example shows
A complete real-time simulation of a 12-state quadcopter controlled by a physics-informed Neural-LQR — and visualised simultaneously in a 3D PyVista scene and a live matplotlib telemetry window. Before the simulation starts, a tkinter configuration GUI lets you choose the reference trajectory, tune its parameters, and set the simulation duration.
| Concept | Detail |
|---|---|
| MIMO LTI modelling | 12-state linearised hover model built with synapsys.api.ss() |
| MIMO LQR | synapsys.algorithms.lqr() on a 12-state / 4-input plant |
| Residual Neural-LQR | δu = −K·e + MLP(e) — residual zeroed at init → starts at optimal LQR |
| Heading-aligned yaw | Drone rotates to face direction of travel — |
| Config GUI | tkinter dialog: simulation time, altitude, reference trajectory & its parameters |
| PyVista 3D | Real-time drone pose animation + trajectory trail at 50 Hz |
| matplotlib telemetry | Position tracking, Euler angles, control inputs — live at 10 Hz |
Physical model — linearised hover
A quadcopter has highly non-linear dynamics, but in the neighbourhood of hover (zero velocity, level attitude) a first-order Taylor expansion yields a useful linear time-invariant (LTI) model valid for small perturbations (|φ|, |θ| ≤ 15°).
State and input vectors
where are roll, pitch, yaw; are body angular rates; and represents deviations from hover equilibrium (hovering at ).
Linearised A and B matrices
The state equation at hover is:
The key gravity-coupling terms are (forward acceleration from pitch ) and (lateral acceleration from roll ):
ẍ = +g·θ (pitch forward → accelerate in x)
ÿ = −g·φ (roll right → accelerate in −y)
The input matrix maps each control channel to its dynamical effect:
Physical parameters (500 mm racing quad):
| Symbol | Value | Meaning |
|---|---|---|
| 0.500 kg | Total mass | |
| 4.856 × 10⁻³ kg·m² | Roll / pitch inertia | |
| 8.801 × 10⁻³ kg·m² | Yaw inertia | |
| 0.175 m | Centre-to-motor arm |
The continuous-time model is built with synapsys.api.ss() and discretised at 100 Hz with synapsys.api.c2d() using the zero-order-hold (ZOH) method:
from synapsys.api import ss, c2d
A, B, C, D = build_matrices() # from quadcopter_dynamics.py
sys_c = ss(A, B, C, D) # continuous-time LTI
sys_d = c2d(sys_c, dt=0.01) # ZOH discretisation at 100 Hz
LQR design
The Linear Quadratic Regulator minimises the infinite-horizon cost:
where is the tracking error. The optimal gain matrix is found by solving the Algebraic Riccati Equation (ARE):
Weight matrices used in this example:
Q = diag([20, 20, 30, # x, y, z — position errors
3, 3, 8, # φ, θ, ψ — attitude (yaw weighted more)
2, 2, 4, # ẋ, ẏ, ż — linear velocity
0.5, 0.5, 1]) # p, q, r — angular rates
R = diag([0.5, 3.0, 3.0, 5.0]) # δF, τφ, τθ, τψ
from synapsys.algorithms import lqr
K, P = lqr(A, B, Q, R)
# K.shape == (4, 12)
# All closed-loop eigenvalues have Re < 0 ✓
The resulting yields a closed-loop system with all eigenvalues in the left half-plane (Re < 0), confirming asymptotic stability.
Neural-LQR controller
Residual architecture
The controller extends LQR with a learnable residual term:
The MLP has architecture 12 → 64 → 32 → 4 with Tanh activations. At initialisation the output layer weights and biases are zeroed, so the network starts as pure LQR and the residual can be trained later via RL or imitation learning without changing any API.
class NeuralLQR(nn.Module):
def __init__(self, K_np):
super().__init__()
self.register_buffer("K", torch.tensor(K_np, dtype=torch.float32))
self.residual = nn.Sequential(
nn.Linear(12, 64), nn.Tanh(),
nn.Linear(64, 32), nn.Tanh(),
nn.Linear(32, 4), # ← zeroed at init
)
with torch.no_grad():
nn.init.zeros_(self.residual[4].weight)
nn.init.zeros_(self.residual[4].bias)
def forward(self, e):
return -(e @ self.K.T) + self.residual(e)
Why this design?
| Property | Benefit |
|---|---|
| Stability at t = 0 | LQR baseline is provably stable; residual adds nothing until trained |
| Smooth fine-tuning | RL or IL can adjust the residual without destabilising the loop |
| Interpretable fallback | Remove/zero the MLP → revert to known-optimal LQR at any time |
Reference trajectories
Three reference trajectories are available via the config GUI:
Figure-8 — Lemniscate of Bernoulli
Default: m, rad/s.
Circle
Default: m, rad/s.
Hover (static)
All three trajectories share the takeoff phase: the drone climbs from the ground to hover altitude during the first seconds before tracking begins.
Heading-aligned yaw control
During trajectory tracking the drone automatically rotates to face the direction it is moving. At each simulation step the desired yaw is computed from the current velocity vector:
This reference is injected directly into x_ref[5] before the LQR error is computed, so the existing gain matrix handles yaw correction with no structural changes.
Two edge cases are handled explicitly:
| Condition | Behaviour |
|---|---|
| Speed |