MLTE03 · Week 7 · Lab (second half of session)

LQR Optimal Control — one cost, one gain

Design an optimal full-state gain on the Week-4 hover model, then prove it stabilizes the quad.
~75 min 🧩 Use quadsim.analysis.lqr 📦 Deliverable: pole map + PID-vs-LQR note Graded checkpoint

← Back to the Week 7 slides Course home

Goal Take the linear hover model from Week 4 and design an LQR gain \( K \) so the input \( \mathbf{u}=-K\mathbf{x} \) minimizes \( \int (\mathbf{x}^\top Q\mathbf{x}+\mathbf{u}^\top R\mathbf{u})\,dt \). By the end you will have a pole map showing the open-loop poles dragged firmly into the left half-plane, and a short PID-vs-LQR comparison on the same step.

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.

Convention check Same frames as all term (see 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
Try first You learn LQR by wiring \(Q,R\) yourself. Peek only after a genuine attempt.
# 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
Expected — your graded checkpoint ⭐

Self-check

5 · Going further (optional)

6 · Deliverable & submission

CriterionWhat we look forWeight
Correct LQR designSensible \(Q,R\); lqr(A,B,Q,R) used; all closed-loop poles Re < 0 (max ≈ −1.74)50%
Pole mapThe wk7_poles.png overlay: open-loop at origin, LQR poles pulled left25%
PID-vs-LQR noteA 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.

What
wk7_lqr.py + wk7_poles.png + a 3–5 line PID-vs-LQR note (one PDF or zip)
Filename
MLTE03_Wk7_<studentID>.zip
Where
MUST LMS → MLTE03 → Week 7 Lab dropbox LMS link — TO FILL
Deadline
date / time — TO FILL (before Week 8)

Submissions 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.