Case Study: Designing and Tuning a PID Descent-Rate Controller
"A controller is a machine for turning a wish into a force — and then correcting the force until the wish comes true."
Executive Summary
In the first case study we took a finished GN&C loop apart. Here we build one on paper: a control law from scratch. Our lunar lander (Track B) is in terminal descent and must hold a commanded vertical speed — a steady $2\ \text{m/s}$ sink — while a modeling error keeps trying to push it off. We will design the controller the way a GN&C engineer does: start with the simplest thing (proportional only), discover its failure by hand-tracing it, add the integral term to cure that failure, add the derivative term to tame the transient, and finish by confronting the messy realities — actuator saturation, integral windup, loop rate — that separate a textbook law from flight software. Along the way we will see the PID controller do, in miniature, the whole job of §27.4, and produce a design note ready for the Mission Design Review. All numbers are illustrative (Tier 3), chosen so the arithmetic is clean; the method is exactly the real one.
Skills applied
- Modeling a plant and a disturbance, and choosing a control structure (§27.1, §27.4).
- Hand-tracing a proportional loop and diagnosing steady-state droop (§27.4).
- Adding integral action to eliminate droop and derivative action to damp overshoot (§27.4).
- Reasoning about stability, saturation, and windup, and writing a control approach for the MDR (§27.4–27.5).
Background
The requirement
Our lander is in the final tens of metres above the lunar surface. Guidance (§27.3) has commanded a constant descent rate of $v_{sp} = -2\ \text{m/s}$ (down is negative). Control's job: throttle the engine to hold that rate precisely, so the vehicle neither slams down nor drifts back up wasting propellant.
The plant, and a gravity feedforward
The vertical motion obeys $\dot v = a_T - g$, where $a_T$ is the thrust acceleration we command and $g \approx 1.62\ \text{m/s}^2$ is lunar gravity. A naive controller would have to discover that it must push with $a_T = g$ just to hover — a waste. Instead we use a feedforward: command $a_T = g_{\text{ff}} + u$, where $g_{\text{ff}}$ is our best estimate of gravity and $u$ is the PID trim around it. If the feedforward were perfect, gravity would cancel and the plant would reduce to $\dot v = u$. In discrete form, with time step $\Delta t = 1\ \text{s}$:
$$ v_{n+1} = v_n + (u_n - d)\,\Delta t, $$
where $d$ collects everything the feedforward gets wrong — a gravity mis-estimate, a thrust calibration error, an unmodeled mass. We take a residual disturbance of $d = 0.2\ \text{m/s}^2$ (the vehicle is being pushed to sink $0.2\ \text{m/s}^2$ faster than the feedforward accounts for). The error the controller sees is $e_n = v_{sp} - v_n$. This is deliberately the same structure as the pointing loop of §27.4 — a first-order plant against a constant disturbance — because the lesson is the same, and now it is flying a lander.
Phase 1: Try the simplest thing — proportional only
Start with $u_n = K_p\,e_n$, $K_p = 0.5$, beginning exactly on target at $v_0 = -2.0\ \text{m/s}$:
| step $n$ | $v_n$ (m/s) | $e_n = v_{sp}-v_n$ | $u_n = 0.5\,e_n$ | $v_{n+1} = v_n + (u_n - 0.2)$ |
|---|---|---|---|---|
| 0 | −2.000 | 0.000 | 0.000 | −2.200 |
| 1 | −2.200 | 0.200 | 0.100 | −2.300 |
| 2 | −2.300 | 0.300 | 0.150 | −2.350 |
| 3 | −2.350 | 0.350 | 0.175 | −2.375 |
| 4 | −2.375 | 0.375 | 0.1875 | −2.3875 |
The descent rate sags from the commanded $-2.0$ toward $-2.4\ \text{m/s}$ and sticks there. Why $-2.4$? At steady state $v$ stops changing, so $u = d$, i.e. $K_p e_{ss} = 0.2$, giving $e_{ss} = 0.2/0.5 = 0.4$ and $v_{ss} = v_{sp} - e_{ss} = -2.4\ \text{m/s}$. This is the proportional droop of §27.4: the controller can only fight the disturbance by tolerating a standing error large enough to generate the needed trim. For a lander, a $0.4\ \text{m/s}$ faster-than-commanded sink is not cosmetic — over the final descent it is a harder touchdown and a propellant surprise. Proportional alone is not good enough. We do not fix this by cranking $K_p$ — §27.4's Find the Error showed that road leads to oscillation and, past the stability limit, divergence. We fix it with memory.
Phase 2: Add integral action to kill the droop
Add $u_n = K_p e_n + K_i \sum_{k\le n} e_k\,\Delta t$ with $K_p = 0.5$, $K_i = 0.5$:
| step $n$ | $v_n$ | $e_n$ | $\sum e$ | $u_n = 0.5 e_n + 0.5\sum e$ | $v_{n+1}$ |
|---|---|---|---|---|---|
| 0 | −2.000 | 0.000 | 0.000 | 0.000 | −2.200 |
| 1 | −2.200 | 0.200 | 0.200 | 0.200 | −2.200 |
| 2 | −2.200 | 0.200 | 0.400 | 0.300 | −2.100 |
| 3 | −2.100 | 0.100 | 0.500 | 0.300 | −2.000 |
| 4 | −2.000 | 0.000 | 0.500 | 0.250 | −1.950 |
| 5 | −1.950 | −0.050 | 0.450 | 0.200 | −1.950 |
The descent rate climbs back to the commanded $-2.0\ \text{m/s}$ by step 3, with a small overshoot to $-1.95$ afterward. Notice what the integral term settled at: $u \to 0.2$, exactly the value needed to cancel the $d = 0.2\ \text{m/s}^2$ disturbance with zero error. The integral remembered the accumulated miss and kept pushing until the miss was gone — precisely its §27.4 job, now visibly saving the landing. The price is the overshoot: the vehicle briefly rises to a $-1.95\ \text{m/s}$ sink before settling. To damp that, we reach for the third term.
Phase 3: Add derivative action to damp the overshoot
The derivative term $K_d\,(e_n - e_{n-1})/\Delta t$ responds to how fast the error is changing, easing off before the vehicle overshoots. In our PI trace the error swung from $+0.1$ at step 3 to $-0.05$ at step 5 — a negative-going rate the derivative term would have opposed, trimming thrust down a touch early and shaving the overshoot. Concretely, at step 4 the error rate is $(e_4 - e_3)/\Delta t = (0 - 0.1)/1 = -0.1$; with a modest $K_d = 0.4$ the derivative contributes $0.4 \times (-0.1) = -0.04$ to $u$, nudging the command down and softening the rebound.
Here is the physical payoff, our theme orbital mechanics is beautiful (theme 3): the proportional term is a spring (restoring force $\propto$ displacement from the setpoint) and the derivative term is a damper (force $\propto$ rate). Together they make the descent-rate error obey the damped-oscillator equation $\ddot e + 2\zeta\omega_n\dot e + \omega_n^2 e = 0$ — the same second-order physics as a car suspension — with the stiffness set by $K_p$ and the damping by $K_d$. We choose the ride: tune $K_d$ toward critical damping ($\zeta \approx 1$) and the lander settles onto the commanded rate as smoothly as a well-damped door closer, fast but without a bounce. Feedback lets us synthesize the physics we wish the vehicle had.
Phase 4: Tuning, and the realities that bite
Choosing the three gains — tuning — is the craft of control. The intuition from our traces generalizes:
| Symptom | Likely cause | Adjustment |
|---|---|---|
| Sluggish, slow to reach the setpoint | $K_p$ too low | raise $K_p$ (watch for overshoot) |
| Persistent steady-state droop | no/weak integral | raise $K_i$ (watch for windup, oscillation) |
| Overshoot and ringing | too little damping | raise $K_d$ (watch for noise amplification) |
| Fast, growing oscillation | $K_p$ (or $K_i$) past the stability limit | lower gains |
But a lander is not a clean first-order plant, and three realities from §27.4–27.5 must be designed for:
- Actuator saturation. The engine throttles only over a finite band (a real descent engine might run, say, $10\%$–$65\%$ of full thrust). The controller cannot command $a_T$ outside that band; if it tries, the output clips, and the loop temporarily loses authority.
- Integral windup. While the engine is saturated and the error persists, the integral term keeps accumulating — winding up huge — and when the error finally reverses, that stored charge overshoots badly. Real controllers clamp the integral (anti-windup) whenever the actuator saturates.
- Loop rate and noise. A real descent loop runs at tens of hertz, not once per second, and the altitude and velocity fed to it come from the noisy, radar-corrected navigation filter of §27.2 — so the derivative term must be filtered, and the loop rate must comfortably beat the fastest disturbance (slosh, thrust transients).
Phase 5: The design note and the code
Our finished control approach, ready for the MDR: three-axis stabilized lander; terminal vertical-rate held by a PID loop (gravity feedforward + $K_p, K_i, K_d$) commanding the throttleable descent engine; integral action to reject gravity/thrust mis-modeling; anti-windup clamp on the throttle limits; loop at tens of hertz on the filtered navigation state; horizontal position and attitude on separate nested loops using RCS thrusters. The code below reproduces both hand-traces exactly.
class PID:
"""Illustrative discrete PID controller (teaching aid, not a canonical astrotools module)."""
def __init__(self, kp, ki, kd, dt):
self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt
self.integral, self.prev_error = 0.0, 0.0
def step(self, error):
self.integral += error * self.dt
derivative = (error - self.prev_error) / self.dt
self.prev_error = error
return self.kp * error + self.ki * self.integral + self.kd * derivative
def run(kp, ki, kd, steps, v_sp=-2.0, v0=-2.0, d=0.2, dt=1.0):
ctrl = PID(kp, ki, kd, dt)
v, out = v0, []
for _ in range(steps):
u = ctrl.step(v_sp - v) # error = commanded rate - actual rate
v = v + (u - d) * dt # first-order plant with residual disturbance d
out.append(round(v, 4))
return out
print(run(0.5, 0.0, 0.0, 5)) # proportional only -> droops toward -2.4
print(run(0.5, 0.5, 0.0, 6)) # PI -> returns to -2.0
# Expected output:
# [-2.2, -2.3, -2.35, -2.375, -2.3875]
# [-2.2, -2.2, -2.1, -2.0, -1.95, -1.95]
The proportional run sags toward the $-2.4\ \text{m/s}$ droop; the PI run returns to the commanded $-2.0\ \text{m/s}$ — the code confirming the arithmetic we did by hand, and the design we will fly.
Discussion Questions
- Explain physically why the gravity feedforward is worth including, given that the integral term could eventually supply the hover thrust on its own. What does the feedforward buy in transient performance?
- In the proportional trace the droop was exactly $-2.4\ \text{m/s}$. Derive $v_{ss}$ from the steady-state condition $u = d$, and predict how the droop would change if $K_p$ were doubled to $1.0$.
- Why does the integral term, and not a larger $K_p$, cure the droop without destabilizing the loop? Tie your answer to the stability limit found in §27.4.
- Describe a scenario in this descent where the throttle saturates, and trace qualitatively what integral windup would do to the touchdown if you did not clamp it.
Your Turn: Extensions
- Option A (design). Retune for a faster settle: pick $K_p$ and $K_i$ that bring the descent rate within $0.05\ \text{m/s}$ of command in three steps, and hand-trace to check you have not introduced a growing oscillation (keep the effective proportional multiplier inside the stability band of §27.4).
- Option B (computation). Add an anti-windup clamp to the
PIDclass: limit the output to a throttle band $[u_{\min}, u_{\max}]$ and stop the integral from accumulating while clamped. Hand-trace one saturated step to show the integral no longer runs away. (Do not run it; add# Expected output:.) - Option C (your mission). Adapt this loop to your track's hardest control task — a comsat holding arcminute pointing (Track A), a Mars-orbiter insertion burn (Track C), or an asteroid station-keeping hop (Track D) — and record the control structure, chosen actuators, and a first gain guess in your MDR.
Key Takeaways
- Design by discovering failure. Start with proportional, hand-trace it, and see the droop; the controller's structure is dictated by the failures you find, not chosen up front.
- Integral kills steady-state error; derivative damps the transient. The PI trace returned the lander to its commanded $-2.0\ \text{m/s}$; a derivative term shaves the overshoot, making a virtual spring–damper you tune to critical damping.
- The law is the easy part; the realities are the job. Saturation, integral windup, loop rate, and sensor noise are what a real design must handle — and what a naive PID ignores.
- A control law is a line in the MDR. "PID vertical-rate hold with gravity feedforward and anti-windup, at tens of hertz on the filtered state" is a real, defensible spacecraft control approach.