1 · The 5-minute recap
A reference trajectory is not just a point — it carries position and its derivatives: \( \mathbf p_r(t),\ \mathbf v_r(t),\ \mathbf a_r(t) \). The figure-eight is the classic multirotor benchmark (world is z-up / ENU, so height is \(+z\)):
\[ \mathbf p_r(t)=\begin{bmatrix} a\sin\omega t \\ b\sin 2\omega t \\ h \end{bmatrix},\qquad \omega=\frac{2\pi}{T},\qquad \mathbf v_r=\dot{\mathbf p}_r,\quad \mathbf a_r=\ddot{\mathbf p}_r. \]
Chasing a moving target with feedback alone always lags. The cure is feedforward: feed the reference acceleration straight into the desired world acceleration, so feedback only corrects what's left:
\[ \mathbf a_{\text{des}} \;=\; \mathbf a_r \;+\; K_p\,(\mathbf p_r-\mathbf p)\;+\;K_d\,(\mathbf v_r-\mathbf v). \]
That is exactly the line already in quadsim/controllers/cascade_pid.py:
a_des = acc_ref + self.kp_pos * e_pos + self.kd_pos * e_vel. Thrust then solves the vertical
accel, \( T=m\,(g+a_{\text{des},z}) \) — gravity is \(-z\), thrust is \(+\)body-\(z\).
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=.
This week you mostly run the reference autopilot (CascadePID) on a trajectory rather than
editing it — the goal is to see feedforward and wind in action and to read the tracking numbers. The
ready-made example is examples/03_figure8.py.
3 · Track the figure-eight
Work through the steps. The reference generators live in quadsim/trajectories.py; each returns
a function ref(t) → dict(pos, vel, acc, yaw) you can hand straight to sim.run(...).
1 Build the reference trajectory trajectories.py
The figure-eight carries pos, vel and acc — that's what makes feedforward possible:
from quadsim import trajectories as traj ref = traj.figure_eight(a=1.0, b=0.5, height=1.5, period=10.0) # ref(t) -> {"pos": [...], "vel": [...], "acc": [...], "yaw": 0.0}
2 Fly it with the reference autopilot no wind
Start at the trajectory's height and run for two full periods (20 s). CascadePID already uses
the acc/vel feedforward, so tracking should be tight:
from quadsim import Simulator, QuadParams from quadsim.controllers import CascadePID from quadsim.dynamics import hover_state sim = Simulator(QuadParams()) log = sim.run(x0=hover_state(position=(0, 0, 1.5)), controller=CascadePID(sim.params), reference=ref, t_final=20.0) print(f"no wind : RMSE = {log.position_rmse():.3f} m") # ~0.037 m
3 Add wind & re-measure disturbance
Wind is an extra world-frame force (N) passed to sim.run(..., wind=...). A constant gust along
world-x lets feedback (not feedforward) earn its keep — the RMSE should rise:
import numpy as np wind = lambda t: np.array([2.0, 0.0, 0.0]) # 2 N along world +x log_w = sim.run(x0=hover_state(position=(0, 0, 1.5)), controller=CascadePID(sim.params), reference=ref, t_final=20.0, wind=wind) print(f"with wind: RMSE = {log_w.position_rmse():.3f} m")
4 Render the strobe & the tracking-error plot deliverable
Two figures from quadsim.plotting: the long-exposure 3-D strobe (X-frame drawn at
intervals, colour = time, reference dashed) and the tracking error (per-axis + norm + RMSE):
from quadsim.plotting import plot_pose_strobe, plot_tracking_error plot_pose_strobe(log, save="wk9_strobe.png", show=False) plot_tracking_error(log, save="wk9_tracking.png", show=False)
The error plot prints the RMSE in its title — that's the number you report.
✓ The fast path: one shell command
examples/03_figure8.py already wires all of the above. Read it once, then run it both ways.export PYTHONPATH=. python examples/03_figure8.py --plot # no wind → ~0.037 m python examples/03_figure8.py --plot --wind 2.0 # with wind → ~0.48 m
It prints trajectory tracking RMSE: ... m for each run and saves a 3-D path PNG. Use the
script for the RMSE numbers, and the short snippet above to add the strobe figure for your report.
4 · Run & verify
Save this as examples/wk9_trajectory.py. It flies the figure-eight twice — clean air and a wind
gust — prints both RMSEs, and saves the two deliverable figures:
# examples/wk9_trajectory.py — Week 9 checkpoint: track a figure-eight, no-wind vs wind import numpy as np from quadsim import Simulator, QuadParams from quadsim.controllers import CascadePID from quadsim.dynamics import hover_state from quadsim import trajectories as traj from quadsim.plotting import plot_pose_strobe, plot_tracking_error sim = Simulator(QuadParams()) ref = traj.figure_eight(a=1.0, b=0.5, height=1.5, period=10.0) # 1) no wind log = sim.run(x0=hover_state(position=(0, 0, 1.5)), controller=CascadePID(sim.params), reference=ref, t_final=20.0) print(f"no wind : RMSE = {log.position_rmse():.3f} m") # 2) constant 2 N world-x wind wind = lambda t: np.array([2.0, 0.0, 0.0]) log_w = sim.run(x0=hover_state(position=(0, 0, 1.5)), controller=CascadePID(sim.params), reference=ref, t_final=20.0, wind=wind) print(f"with wind: RMSE = {log_w.position_rmse():.3f} m") # 3) deliverable figures (from the no-wind run) plot_pose_strobe(log, save="wk9_strobe.png", show=False) plot_tracking_error(log, save="wk9_tracking.png", show=False) print("saved wk9_strobe.png and wk9_tracking.png")
export PYTHONPATH=. python examples/wk9_trajectory.py
- No-wind RMSE ≈ 0.037 m — the feedforward keeps the quad on the curve.
- With-wind RMSE is clearly larger than the no-wind value (feedback alone fights the gust).
wk9_strobe.pngshows the quad tracing both lobes of the eight, flown path hugging the dashed reference.wk9_tracking.pngshows bounded per-axis error and the RMSE line.
Self-check
5 · Going further (optional)
- Kill the feedforward. Temporarily ignore
acc/velin the outer loop (pure P/D) and re-measure — watch the RMSE jump. This is the whole argument for feedforward. - Speed it up. Halve the
period(faster eight). Tracking error grows with target speed — by how much? - Other references. Swap in
traj.waypoints([...])ortraj.step(...)and compare how the same autopilot copes with a non-smooth target. - Midterm warm-up. For each arrow in the Wk 1–9 map, write the one convention hazard that lives there (NED↔ENU sign, plus↔X mixer, quaternion order, gimbal lock at \( \theta=\pm90° \)).
6 · Deliverable & submission
| Criterion | What we look for | Weight |
|---|---|---|
| Tracking result | No-wind figure-eight RMSE ≈ 0.037 m; the strobe shows the path on the reference | 40% |
| Wind comparison | With-wind RMSE reported and clearly larger; tracking-error plot included | 30% |
| Understanding | One short paragraph: why feedforward beats pure feedback, and which conventions (z-up, X-frame) you kept | 30% |
📤 Submit via the MUST LMS
⭐ This submission is graded (5%). Full requirements & rubric: Lab 6 assignment brief.
wk9_strobe.png + wk9_tracking.png + the two RMSE numbers + a 3–5 line note (one PDF or zip)MLTE03_Wk9_<studentID>.zipSubmissions are handled entirely in the MUST LMS — nothing is uploaded to this site.
Next week → Week 10: the intelligent-control half begins. The
~0.037 m figure-eight RMSE you just measured is the baseline that MPC and learning-based controllers
will try to beat. Reference: Beard & McLain UAVbook (mavsim_public) and simulator/CONVENTIONS.md.