1 · The 5-minute recap
About hover the quad is a linear system \( \dot{\mathbf{x}}=A\mathbf{x}+B\mathbf{u} \), where
\( \mathbf{x} \) is the 12-state deviation and \( \mathbf{u}=[\,\delta T,\tau_x,\tau_y,\tau_z\,] \) the wrench
deviation. \(A\) is \(12\times12\), \(B\) is \(12\times4\) — exactly the matrices
analysis.linearize(p) returned in Week 4.
LQR chooses the input that minimizes a quadratic cost; the answer is linear full-state feedback:
\[ J = \int_0^\infty \big(\mathbf{x}^\top Q\,\mathbf{x} + \mathbf{u}^\top R\,\mathbf{u}\big)\,dt, \qquad \mathbf{u} = -K\mathbf{x}, \qquad K = R^{-1}B^\top P \]
where \( P=P^\top\succeq 0 \) solves the continuous-time algebraic Riccati equation (CARE):
\[ A^\top P + P A - P B R^{-1} B^\top P + Q = 0 \]
The closed loop is \( \dot{\mathbf{x}}=(A-BK)\mathbf{x} \); LQR guarantees every eigenvalue of \(A-BK\) has \( \mathrm{Re}<0 \). \(Q\) penalizes state error (bigger ⇒ faster, more effort), \(R\) penalizes control effort (bigger ⇒ gentler). Only the ratio \(Q/R\) matters.
simulator/CONVENTIONS.md): z-up / ENU (gravity \(-z\),
thrust \(+\)body-\(z\)), ZYX intrinsic Euler, the X-frame mixer, scalar-first quaternions.
Here \( \mathbf{x},\mathbf{u} \) are deviations from the hover trim — apply the gain as
\( \mathbf{u}=\mathbf{u}_{\text{hover}}-K(\mathbf{x}-\mathbf{x}_{\text{eq}}) \).
2 · Setup (2 min)
From the simulator/ directory, in your virtual environment:
# one-time python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt # every session — so `import quadsim` works export PYTHONPATH=. # Windows: set PYTHONPATH=.
Everything you need lives in quadsim/analysis.py (linearize, poles,
care, lqr) and quadsim/plotting.py (plot_poles). The reference
walkthrough is examples/04_analysis.py — open it alongside this sheet.
3 · Design the LQR gain
Work through the steps. You'll write a short script examples/wk7_lqr.py that linearizes the
model, designs \(K\), and renders the pole map. The state order is
[p(3), v(3), (φ,θ,ψ), ω(3)]; the wrench is [T, τx, τy, τz].
1 Linearize the hover model reuse Week 4
Get the same \((A,B)\) you analyzed in Week 4, and look at the open-loop poles — they sit at the origin (pure integrators), so the open loop is only marginally stable.
import numpy as np from quadsim import QuadParams, analysis as an p = QuadParams() A, B = an.linearize(p) # A: 12x12, B: 12x4 — the hover linear model ol = an.poles(A) # open-loop eigenvalues print("open-loop max Re(pole) =", ol.real.max())
2 Choose the cost weights \(Q,R\) the design knobs
Diagonal weights — one number per state and per channel. Start from the values in
examples/04_analysis.py: position & attitude weighted 10, their rates 1; cheap thrust,
costly torques so the motors aren't slammed.
Q = np.diag([10, 10, 10, 1, 1, 1, 10, 10, 10, 1, 1, 1.0]) # pos, vel, euler, rates R = np.diag([1.0, 10, 10, 10]) # [T, tau_x, tau_y, tau_z]
\(R\) must be positive definite (every entry > 0) — you always pay something for effort.
3 Solve for the gain the core
One call solves CARE (via the Hamiltonian eigenvector method, pure NumPy) and returns the gain, the cost matrix \(P\), and the closed-loop poles \( \mathrm{eig}(A-BK) \):
K, P, cl = an.lqr(A, B, Q, R) # u = -K x ; cl = closed-loop poles print("closed-loop max Re(pole) =", cl.real.max()) # ~ -1.74 print("stable:", np.all(cl.real < 0))
4 Plot open-loop vs closed-loop poles the deliverable
Overlay both pole sets. The shaded region is the unstable half-plane; LQR drags the origin poles left, clear of the imaginary axis.
from quadsim import plotting as viz viz.plot_poles([ol, cl], labels=["open loop", "LQR closed loop"], save="wk7_poles.png", show=False) print("saved wk7_poles.png")
5 Feel the \(Q/R\) trade-off tune
Re-run with the torque cost \(R\) scaled up and down and watch the rightmost pole move:
for scale in (0.1, 1.0, 10.0):
_, _, cl_s = an.lqr(A, B, Q, scale * R)
print(f"R x{scale:>4}: max Re = {cl_s.real.max():7.3f}")Smaller \(R\) (cheaper effort) ⇒ poles further left (faster) but larger \(K\), more motor demand. Larger \(R\) ⇒ gentler, slower. Only the ratio \(Q/R\) matters.
✓ Stuck? Reveal the full wk7_lqr.py
# examples/wk7_lqr.py — Week 7 checkpoint: LQR pole map on the hover model import numpy as np from quadsim import QuadParams, analysis as an from quadsim import plotting as viz p = QuadParams() A, B = an.linearize(p) ol = an.poles(A) Q = np.diag([10, 10, 10, 1, 1, 1, 10, 10, 10, 1, 1, 1.0]) R = np.diag([1.0, 10, 10, 10]) K, P, cl = an.lqr(A, B, Q, R) print(f"open-loop max Re(pole) = {ol.real.max():7.3f}") print(f"closed-loop max Re(pole) = {cl.real.max():7.3f} stable={np.all(cl.real < 0)}") viz.plot_poles([ol, cl], labels=["open loop", "LQR closed loop"], save="wk7_poles.png", show=False) print("saved wk7_poles.png")
4 · Run & verify
Run your script — and the shared reference example, which does the same design and also flies a figure-eight with the cascade controller:
export PYTHONPATH=.
python examples/wk7_lqr.py
python examples/04_analysis.py # reference: prints the same LQR line, saves analysis_poles.png- Open-loop max Re(pole) ≈ 0 — poles sit at the origin (marginally stable, needs feedback).
- Closed-loop max Re(pole) ≈ −1.74 and all poles have Re < 0 — LQR stabilizes it.
wk7_poles.pngshows the open-loop poles at the origin and the LQR poles pulled left, clear of the imaginary axis.
Self-check
5 · Going further (optional)
- PID vs LQR, head to head. From a 20° tilt, run your Week-5 PID and the LQR law on the same start. Compare overshoot, settling time, and control effort \( \int \|\mathbf{u}\|^2\,dt \) — one short paragraph.
- Bryson's rule. Set \(Q_{ii}=1/x_{i,\max}^2\) and \(R_{jj}=1/u_{j,\max}^2\) from your own "acceptable" limits, and see how the pole map shifts.
- Controllability. Call
an.controllability(A, B)and confirm rank 12 — the precondition that makes LQR stabilizing in the first place.
6 · Deliverable & submission
| Criterion | What we look for | Weight |
|---|---|---|
| Correct LQR design | Sensible \(Q,R\); lqr(A,B,Q,R) used; all closed-loop poles Re < 0 (max ≈ −1.74) | 50% |
| Pole map | The wk7_poles.png overlay: open-loop at origin, LQR poles pulled left | 25% |
| PID-vs-LQR note | A short paragraph comparing overshoot / settling / effort, with the right intuition on \(Q,R\) | 25% |
📤 Submit via the MUST LMS
⭐ This submission is graded (5%). Full requirements & rubric: Lab 4 assignment brief.
wk7_lqr.py + wk7_poles.png + a 3–5 line PID-vs-LQR note (one PDF or zip)MLTE03_Wk7_<studentID>.zipSubmissions are handled entirely in the MUST LMS — nothing is uploaded to this site.
Reference: MIT 16.323 (How), Lec 3–4 —
ocw.mit.edu/courses/16-323;
conventions in simulator/CONVENTIONS.md.
Next week → Week 8: model-predictive control — LQR with a horizon and input limits.