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

Linearization, Trim & Hover — the design model

Freeze the quad at hover, linearize it, and read the poles every later controller is built on.
~75 min 🧩 New script examples/wk4_analysis.py 📦 Deliverable: pole map + rank Graded checkpoint

← Back to the Week 4 slides Course home

Goal Use quadsim.analysis to linearize the nonlinear model about hover, obtaining \( (A,B) \). Plot the open-loop poles and see them sit at the origin (a chain of integrators — marginally stable, so feedback is mandatory), then confirm the system is controllable with \( \operatorname{rank}\mathcal C = 12 \). This \( (A,B) \) is the exact model Weeks 7 (LQR) and 11–12 (MPC) reuse.

1 · The 5-minute recap

Hover trim is the quad's one equilibrium: level (\( \boldsymbol\eta=\mathbf 0 \)), at rest (\( \mathbf v=\boldsymbol\omega=\mathbf 0 \)), thrust = weight (\( T=mg \), along \(+\)body-z, which is world-up in our z-up / ENU convention), and zero torque. By construction the state-derivative vanishes there:

\[ \mathbf u_{eq}=\begin{bmatrix} mg & 0 & 0 & 0 \end{bmatrix}^{\!\top},\qquad f(\mathbf x_{eq},\mathbf u_{eq})=\mathbf 0. \]

Write \( \mathbf x=\mathbf x_{eq}+\delta\mathbf x \), \( \mathbf u=\mathbf u_{eq}+\delta\mathbf u \) and keep first order. analysis.linearize does this with central finite differences of the real nonlinear \( f= \)state_derivative:

\[ \dot{\delta\mathbf x}\approx A\,\delta\mathbf x + B\,\delta\mathbf u,\qquad A=\tfrac{\partial f}{\partial \mathbf x}\big|_{eq}\ (12\times12),\quad B=\tfrac{\partial f}{\partial \mathbf u}\big|_{eq}\ (12\times4). \]

The poles are \( \operatorname{eig}(A) \); at hover several are exactly zero (integrators) ⇒ marginally stable, not asymptotically stable. The fix is feedback. First check it is even fixable — controllability:

\[ \mathcal C=\big[\,B\ \ AB\ \ A^2B\ \cdots\ A^{11}B\,\big],\qquad \operatorname{rank}\mathcal C \overset{!}{=} 12. \]

Convention guardrail Everything here follows CONVENTIONS.md: z-up / ENU (gravity \(-z\), thrust \(+\)body-z), intrinsic ZYX Euler, the X-frame mixer (every motor contributes to roll and pitch — not plus-frame), scalar-first quaternions. Because linearize differentiates the real state_derivative, \( (A,B) \) inherit these signs automatically — there is no separate matrix to get wrong.

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 today lives in quadsim/analysis.py (read it — it is short and well commented) and the reference walk-through examples/04_analysis.py. You will write a small new script, examples/wk4_analysis.py, that isolates the Week-4 pieces.

3 · Build the linear model

Work through the steps. Each adds a few lines to examples/wk4_analysis.py. The functions you need — hover_equilibrium, linearize, poles, controllability — all live in quadsim.analysis.

1 Find the hover trim & sanity-check it equilibrium

Get \( (\mathbf x_{eq},\mathbf u_{eq}) \) and confirm the state-derivative really is zero there (this is what "equilibrium" means):

import numpy as np
from quadsim import QuadParams, analysis as an
from quadsim.dynamics import state_derivative

p = QuadParams()
x_eq, u_eq = an.hover_equilibrium(p)          # u_eq = [p.weight, 0, 0, 0]
print("max |f(x_eq,u_eq)| =", np.max(np.abs(state_derivative(x_eq, u_eq, p))))
2 Linearize about hover → \( (A,B) \) the core

One call differentiates the real nonlinear model by central differences. Check the shapes:

A, B = an.linearize(p)                        # about hover by default
print("A:", A.shape, " B:", B.shape)   # (12, 12)  (12, 4)
3 Open-loop poles & count those at the origin eig(A)

The poles are the eigenvalues of \(A\). Count how many sit on the imaginary axis (\( \operatorname{Re}\approx0 \)) — those are the integrators that make hover only marginally stable:

ol = an.poles(A)
n_origin = int(np.sum(np.abs(ol.real) < 1e-6))
print(f"{n_origin} open-loop poles at the origin (marginally stable)")
4 Controllability rank — must be 12 prerequisite

Build \( \mathcal C \) and take its rank. Full rank means the four-component wrench can steer all twelve states — the green light for LQR/MPC:

C, rank = an.controllability(A, B)
print(f"controllability rank {rank}/12")
assert rank == 12, "system is not controllable — check A,B"
5 Plot the pole map deliverable

Use the built-in plotter. It shades the unstable (\( \operatorname{Re}>0 \)) half-plane and marks the imaginary axis — the stability boundary. Pass a list of eigenvalue arrays:

from quadsim import plotting as viz
viz.plot_poles([ol], labels=["open loop (hover)"],
               save="wk4_poles.png", show=False)
print("saved wk4_poles.png")
Stuck? Reveal the full examples/wk4_analysis.py
Try first You learn the model by assembling it. Peek only after a genuine attempt.
# examples/wk4_analysis.py — Week 4: linearize about hover, poles, controllability
import numpy as np
from quadsim import QuadParams, analysis as an
from quadsim.dynamics import state_derivative
from quadsim import plotting as viz

p = QuadParams()

# 1) hover trim — state-derivative must vanish
x_eq, u_eq = an.hover_equilibrium(p)
print("max |f(x_eq,u_eq)| =", np.max(np.abs(state_derivative(x_eq, u_eq, p))))

# 2) linearize the real nonlinear model about hover
A, B = an.linearize(p)
print("A:", A.shape, " B:", B.shape)        # (12,12) (12,4)

# 3) open-loop poles
ol = an.poles(A)
n_origin = int(np.sum(np.abs(ol.real) < 1e-6))
print(f"{n_origin} poles at the origin (marginally stable)")

# 4) controllability
C, rank = an.controllability(A, B)
print(f"controllability rank {rank}/12")
assert rank == 12

# 5) deliverable: the pole map
viz.plot_poles([ol], labels=["open loop (hover)"],
               save="wk4_poles.png", show=False)
print("saved wk4_poles.png")

4 · Run & verify

Save the script above as examples/wk4_analysis.py, then run it:

export PYTHONPATH=.
python examples/wk4_analysis.py
Expected — your graded checkpoint ⭐

For the bigger picture, run the reference walk-through — it does the same Week-4 analysis, then goes on to design the Week-7 LQR gain on this very \( (A,B) \) and flies a figure-eight (a preview of where this leads):

python examples/04_analysis.py

Self-check

5 · Going further (optional)

6 · Deliverable & submission

CriterionWhat we look forWeight
Correct linear modellinearize used about hover; \(A\) (12×12), \(B\) (12×4); equilibrium verified40%
Poles & controllabilityThe wk4_poles.png map + printed origin-pole count and rank = 1235%
UnderstandingOne short paragraph: why hover poles are marginally stable & why rank 12 matters25%

📤 Submit via the MUST LMS

This submission is graded (5%). Full requirements & rubric: Lab 1 assignment brief.

What
wk4_analysis.py + wk4_poles.png + a 3–5 line note (one PDF or zip)
Filename
MLTE03_Wk4_<studentID>.zip
Where
MUST LMS → MLTE03 → Week 4 Lab dropbox LMS link — TO FILL
Deadline
date / time — TO FILL (before Week 5)

Submissions are handled entirely in the MUST LMS — nothing is uploaded to this site.

Next week → Week 5: attitude control. You take this marginally-stable model and close the first feedback loop — attitude — pulling the rotational poles into the left half-plane.